mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Merge pull request #5566 from halfdan/refactor-middleware
Middleware Refactor
This commit is contained in:
commit
0f954f385d
19 changed files with 1460 additions and 1000 deletions
|
@ -1,66 +1,70 @@
|
|||
var passport = require('passport'),
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
models = require('../models');
|
||||
var BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
models = require('../models'),
|
||||
strategies;
|
||||
|
||||
/**
|
||||
* ClientPasswordStrategy
|
||||
*
|
||||
* This strategy is used to authenticate registered OAuth clients. It is
|
||||
* employed to protect the `token` endpoint, which consumers use to obtain
|
||||
* access tokens. The OAuth 2.0 specification suggests that clients use the
|
||||
* HTTP Basic scheme to authenticate (not implemented yet).
|
||||
strategies = {
|
||||
|
||||
* Use of the client password strategy is implemented to support ember-simple-auth.
|
||||
*/
|
||||
passport.use(new ClientPasswordStrategy(
|
||||
function strategy(clientId, clientSecret, done) {
|
||||
models.Client.forge({slug: clientId})
|
||||
.fetch()
|
||||
.then(function then(model) {
|
||||
if (model) {
|
||||
var client = model.toJSON();
|
||||
if (client.secret === clientSecret) {
|
||||
return done(null, client);
|
||||
/**
|
||||
* ClientPasswordStrategy
|
||||
*
|
||||
* This strategy is used to authenticate registered OAuth clients. It is
|
||||
* employed to protect the `token` endpoint, which consumers use to obtain
|
||||
* access tokens. The OAuth 2.0 specification suggests that clients use the
|
||||
* HTTP Basic scheme to authenticate (not implemented yet).
|
||||
* Use of the client password strategy is implemented to support ember-simple-auth.
|
||||
*/
|
||||
clientPasswordStrategy: new ClientPasswordStrategy(
|
||||
function strategy(clientId, clientSecret, done) {
|
||||
models.Client.forge({slug: clientId})
|
||||
.fetch()
|
||||
.then(function then(model) {
|
||||
if (model) {
|
||||
var client = model.toJSON();
|
||||
if (client.secret === clientSecret) {
|
||||
return done(null, client);
|
||||
}
|
||||
}
|
||||
}
|
||||
return done(null, false);
|
||||
});
|
||||
}
|
||||
));
|
||||
return done(null, false);
|
||||
});
|
||||
}
|
||||
),
|
||||
|
||||
/**
|
||||
* BearerStrategy
|
||||
*
|
||||
* This strategy is used to authenticate users based on an access token (aka a
|
||||
* bearer token). The user must have previously authorized a client
|
||||
* application, which is issued an access token to make requests on behalf of
|
||||
* the authorizing user.
|
||||
*/
|
||||
passport.use(new BearerStrategy(
|
||||
function strategy(accessToken, done) {
|
||||
models.Accesstoken.forge({token: accessToken})
|
||||
.fetch()
|
||||
.then(function then(model) {
|
||||
if (model) {
|
||||
var token = model.toJSON();
|
||||
if (token.expires > Date.now()) {
|
||||
models.User.forge({id: token.user_id})
|
||||
.fetch()
|
||||
.then(function then(model) {
|
||||
if (model) {
|
||||
var user = model.toJSON(),
|
||||
info = {scope: '*'};
|
||||
return done(null, {id: user.id}, info);
|
||||
}
|
||||
/**
|
||||
* BearerStrategy
|
||||
*
|
||||
* This strategy is used to authenticate users based on an access token (aka a
|
||||
* bearer token). The user must have previously authorized a client
|
||||
* application, which is issued an access token to make requests on behalf of
|
||||
* the authorizing user.
|
||||
*/
|
||||
bearerStrategy: new BearerStrategy(
|
||||
function strategy(accessToken, done) {
|
||||
models.Accesstoken.forge({token: accessToken})
|
||||
.fetch()
|
||||
.then(function then(model) {
|
||||
if (model) {
|
||||
var token = model.toJSON();
|
||||
if (token.expires > Date.now()) {
|
||||
models.User.forge({id: token.user_id})
|
||||
.fetch()
|
||||
.then(function then(model) {
|
||||
if (model) {
|
||||
var user = model.toJSON(),
|
||||
info = {scope: '*'};
|
||||
return done(null, {id: user.id}, info);
|
||||
}
|
||||
return done(null, false);
|
||||
});
|
||||
} else {
|
||||
return done(null, false);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
module.exports = strategies;
|
||||
|
|
48
core/server/middleware/authenticate.js
Normal file
48
core/server/middleware/authenticate.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
var passport = require('passport'),
|
||||
apiErrorHandlers = require('./api-error-handlers');
|
||||
|
||||
// ### 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
|
||||
function authenticate(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 replace(match, a) {
|
||||
return a;
|
||||
});
|
||||
|
||||
if (subPath.indexOf('/ghost/api/') === 0
|
||||
&& (path.indexOf('/ghost/api/v0.1/authentication/') !== 0
|
||||
|| (path.indexOf('/ghost/api/v0.1/authentication/setup/') === 0 && req.method === 'PUT'))) {
|
||||
return passport.authenticate('bearer', {session: false, failWithError: true},
|
||||
function authenticate(err, user, info) {
|
||||
if (err) {
|
||||
return next(err); // will generate a 500 error
|
||||
}
|
||||
// Generate a JSON response reflecting authentication status
|
||||
if (!user) {
|
||||
var error = {
|
||||
code: 401,
|
||||
errorType: 'NoPermissionError',
|
||||
message: 'Please Sign In'
|
||||
};
|
||||
|
||||
return apiErrorHandlers.errorHandler(error, req, res, next);
|
||||
}
|
||||
// TODO: figure out, why user & authInfo is lost
|
||||
req.authInfo = info;
|
||||
req.user = user;
|
||||
return next(null, user, info);
|
||||
}
|
||||
)(req, res, next);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = authenticate;
|
72
core/server/middleware/check-ssl.js
Normal file
72
core/server/middleware/check-ssl.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
var config = require('../config'),
|
||||
url = require('url');
|
||||
|
||||
function isSSLrequired(isAdmin, configUrl, forceAdminSSL) {
|
||||
var forceSSL = url.parse(configUrl).protocol === 'https:' ? true : false;
|
||||
if (forceSSL || (isAdmin && forceAdminSSL)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The guts of checkSSL. Indicate forbidden or redirect according to configuration.
|
||||
// Required args: forceAdminSSL, url and urlSSL should be passed from config. reqURL from req.url
|
||||
function sslForbiddenOrRedirect(opt) {
|
||||
var forceAdminSSL = opt.forceAdminSSL,
|
||||
reqUrl = opt.reqUrl, // expected to be relative-to-root
|
||||
baseUrl = url.parse(opt.configUrlSSL || opt.configUrl),
|
||||
response = {
|
||||
// Check if forceAdminSSL: { redirect: false } is set, which means
|
||||
// we should just deny non-SSL access rather than redirect
|
||||
isForbidden: (forceAdminSSL && forceAdminSSL.redirect !== undefined && !forceAdminSSL.redirect),
|
||||
|
||||
// Append the request path to the base configuration path, trimming out a double "//"
|
||||
redirectPathname: function redirectPathname() {
|
||||
var pathname = baseUrl.path;
|
||||
if (reqUrl[0] === '/' && pathname[pathname.length - 1] === '/') {
|
||||
pathname += reqUrl.slice(1);
|
||||
} else {
|
||||
pathname += reqUrl;
|
||||
}
|
||||
return pathname;
|
||||
},
|
||||
redirectUrl: function redirectUrl(query) {
|
||||
return url.format({
|
||||
protocol: 'https:',
|
||||
hostname: baseUrl.hostname,
|
||||
port: baseUrl.port,
|
||||
pathname: this.redirectPathname(),
|
||||
query: query
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check to see if we should use SSL
|
||||
// and redirect if needed
|
||||
function checkSSL(req, res, next) {
|
||||
if (isSSLrequired(res.isAdmin, config.url, config.forceAdminSSL)) {
|
||||
if (!req.secure) {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: config.forceAdminSSL,
|
||||
configUrlSSL: config.urlSSL,
|
||||
configUrl: config.url,
|
||||
reqUrl: req.url
|
||||
});
|
||||
|
||||
if (response.isForbidden) {
|
||||
return res.sendStatus(403);
|
||||
} else {
|
||||
return res.redirect(301, response.redirectUrl(req.query));
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = checkSSL;
|
||||
// SSL helper functions are exported primarily for unit testing.
|
||||
module.exports.isSSLrequired = isSSLrequired;
|
||||
module.exports.sslForbiddenOrRedirect = sslForbiddenOrRedirect;
|
|
@ -1,196 +1,53 @@
|
|||
// # Custom Middleware
|
||||
// The following custom middleware functions cannot yet be unit tested, and as such are kept separate from
|
||||
// the testable custom middleware functions in middleware.js
|
||||
var bodyParser = require('body-parser'),
|
||||
config = require('../config'),
|
||||
errors = require('../errors'),
|
||||
express = require('express'),
|
||||
logger = require('morgan'),
|
||||
path = require('path'),
|
||||
routes = require('../routes'),
|
||||
slashes = require('connect-slashes'),
|
||||
storage = require('../storage'),
|
||||
passport = require('passport'),
|
||||
oauth2orize = require('oauth2orize'),
|
||||
utils = require('../utils'),
|
||||
sitemapHandler = require('../data/xml/sitemap/handler'),
|
||||
|
||||
var api = require('../api'),
|
||||
bodyParser = require('body-parser'),
|
||||
config = require('../config'),
|
||||
crypto = require('crypto'),
|
||||
errors = require('../errors'),
|
||||
express = require('express'),
|
||||
fs = require('fs'),
|
||||
hbs = require('express-hbs'),
|
||||
logger = require('morgan'),
|
||||
middleware = require('./middleware'),
|
||||
path = require('path'),
|
||||
routes = require('../routes'),
|
||||
slashes = require('connect-slashes'),
|
||||
storage = require('../storage'),
|
||||
_ = require('lodash'),
|
||||
passport = require('passport'),
|
||||
oauth = require('./oauth'),
|
||||
oauth2orize = require('oauth2orize'),
|
||||
authStrategies = require('./auth-strategies'),
|
||||
utils = require('../utils'),
|
||||
sitemapHandler = require('../data/xml/sitemap/handler'),
|
||||
decideIsAdmin = require('./decide-is-admin'),
|
||||
uncapitalise = require('./uncapitalise'),
|
||||
apiErrorHandlers = require('./api-error-handlers'),
|
||||
authenticate = require('./authenticate'),
|
||||
authStrategies = require('./auth-strategies'),
|
||||
busboy = require('./ghost-busboy'),
|
||||
clientAuth = require('./client-auth'),
|
||||
cacheControl = require('./cache-control'),
|
||||
checkSSL = require('./check-ssl'),
|
||||
decideIsAdmin = require('./decide-is-admin'),
|
||||
privateBlogging = require('./private-blogging'),
|
||||
redirectToSetup = require('./redirect-to-setup'),
|
||||
serveSharedFile = require('./serve-shared-file'),
|
||||
spamPrevention = require('./spam-prevention'),
|
||||
staticTheme = require('./static-theme'),
|
||||
uncapitalise = require('./uncapitalise'),
|
||||
oauth = require('./oauth'),
|
||||
|
||||
themeHandler = require('./theme-handler'),
|
||||
privateBlogging = require('./private-blogging'),
|
||||
|
||||
blogApp,
|
||||
middleware,
|
||||
setupMiddleware;
|
||||
|
||||
// ##Custom Middleware
|
||||
|
||||
// ### GhostLocals Middleware
|
||||
// Expose the standard locals that every external page should have available,
|
||||
// separating between the theme and the admin
|
||||
function ghostLocals(req, res, next) {
|
||||
// Make sure we have a locals value.
|
||||
res.locals = res.locals || {};
|
||||
res.locals.version = config.ghostVersion;
|
||||
res.locals.safeVersion = config.ghostVersion.match(/^(\d+\.)?(\d+)/)[0];
|
||||
// relative path from the URL
|
||||
res.locals.relativeUrl = req.path;
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
// ### Activate Theme
|
||||
// Helper for manageAdminAndTheme
|
||||
function activateTheme(activeTheme) {
|
||||
var hbsOptions,
|
||||
themePartials = path.join(config.paths.themePath, activeTheme, 'partials');
|
||||
|
||||
// clear the view cache
|
||||
blogApp.cache = {};
|
||||
|
||||
// set view engine
|
||||
hbsOptions = {
|
||||
partialsDir: [config.paths.helperTemplates],
|
||||
onCompile: function onCompile(exhbs, source) {
|
||||
return exhbs.handlebars.compile(source, {preventIndent: true});
|
||||
}
|
||||
};
|
||||
|
||||
fs.stat(themePartials, function stat(err, stats) {
|
||||
// Check that the theme has a partials directory before trying to use it
|
||||
if (!err && stats && stats.isDirectory()) {
|
||||
hbsOptions.partialsDir.push(themePartials);
|
||||
}
|
||||
});
|
||||
|
||||
blogApp.engine('hbs', hbs.express3(hbsOptions));
|
||||
|
||||
// Update user error template
|
||||
errors.updateActiveTheme(activeTheme);
|
||||
|
||||
// Set active theme variable on the express server
|
||||
blogApp.set('activeTheme', activeTheme);
|
||||
}
|
||||
|
||||
// ### configHbsForContext Middleware
|
||||
// Setup handlebars for the current context (admin or theme)
|
||||
function configHbsForContext(req, res, next) {
|
||||
var themeData = config.theme;
|
||||
if (req.secure && config.urlSSL) {
|
||||
// For secure requests override .url property with the SSL version
|
||||
themeData = _.clone(themeData);
|
||||
themeData.url = config.urlSSL.replace(/\/$/, '');
|
||||
middleware = {
|
||||
busboy: busboy,
|
||||
cacheControl: cacheControl,
|
||||
spamPrevention: spamPrevention,
|
||||
privateBlogging: privateBlogging,
|
||||
api: {
|
||||
addClientSecret: clientAuth.addClientSecret,
|
||||
cacheOauthServer: clientAuth.cacheOauthServer,
|
||||
authenticateClient: clientAuth.authenticateClient,
|
||||
generateAccessToken: clientAuth.generateAccessToken,
|
||||
errorHandler: apiErrorHandlers.errorHandler
|
||||
}
|
||||
|
||||
hbs.updateTemplateOptions({data: {blog: themeData}});
|
||||
|
||||
if (config.paths.themePath && blogApp.get('activeTheme')) {
|
||||
blogApp.set('views', path.join(config.paths.themePath, blogApp.get('activeTheme')));
|
||||
}
|
||||
|
||||
// Pass 'secure' flag to the view engine
|
||||
// so that templates can choose 'url' vs 'urlSSL'
|
||||
res.locals.secure = req.secure;
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
// ### updateActiveTheme
|
||||
// Updates the blogApp's activeTheme variable and subsequently
|
||||
// activates that theme's views with the hbs templating engine if it
|
||||
// is not yet activated.
|
||||
function updateActiveTheme(req, res, next) {
|
||||
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function then(response) {
|
||||
var activeTheme = response.settings[0];
|
||||
|
||||
// Check if the theme changed
|
||||
if (activeTheme.value !== blogApp.get('activeTheme')) {
|
||||
// Change theme
|
||||
if (!config.paths.availableThemes.hasOwnProperty(activeTheme.value)) {
|
||||
if (!res.isAdmin) {
|
||||
// Throw an error if the theme is not available, but not on the admin UI
|
||||
return errors.throwError('The currently active theme ' + activeTheme.value + ' is missing.');
|
||||
} else {
|
||||
// At this point the activated theme is not present and the current
|
||||
// request is for the admin client. In order to allow the user access
|
||||
// to the admin client we set an hbs instance on the app so that middleware
|
||||
// processing can continue.
|
||||
blogApp.engine('hbs', hbs.express3());
|
||||
errors.logWarn('The currently active theme "' + activeTheme.value + '" is missing.');
|
||||
|
||||
return next();
|
||||
}
|
||||
} else {
|
||||
activateTheme(activeTheme.value);
|
||||
}
|
||||
}
|
||||
next();
|
||||
}).catch(function handleError(err) {
|
||||
// Trying to start up without the active theme present, setup a simple hbs instance
|
||||
// and render an error page straight away.
|
||||
blogApp.engine('hbs', hbs.express3());
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
|
||||
// Redirect to setup if no user exists
|
||||
function redirectToSetup(req, res, next) {
|
||||
/*jslint unparam:true*/
|
||||
|
||||
api.authentication.isSetup().then(function then(exists) {
|
||||
if (!exists.setup[0].status && !req.path.match(/\/setup\//)) {
|
||||
return res.redirect(config.paths.subdir + '/ghost/setup/');
|
||||
}
|
||||
next();
|
||||
}).catch(function handleError(err) {
|
||||
return next(new Error(err));
|
||||
});
|
||||
}
|
||||
|
||||
// ### ServeSharedFile Middleware
|
||||
// Handles requests to robots.txt and favicon.ico (and caches them)
|
||||
function serveSharedFile(file, type, maxAge) {
|
||||
var content,
|
||||
filePath = path.join(config.paths.corePath, 'shared', file),
|
||||
re = /(\{\{blog-url\}\})/g;
|
||||
|
||||
return function serveSharedFile(req, res, next) {
|
||||
if (req.url === '/' + file) {
|
||||
if (content) {
|
||||
res.writeHead(200, content.headers);
|
||||
res.end(content.body);
|
||||
} else {
|
||||
fs.readFile(filePath, function readFile(err, buf) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (type === 'text/xsl' || type === 'text/plain') {
|
||||
buf = buf.toString().replace(re, config.url.replace(/\/$/, ''));
|
||||
}
|
||||
content = {
|
||||
headers: {
|
||||
'Content-Type': type,
|
||||
'Content-Length': buf.length,
|
||||
ETag: '"' + crypto.createHash('md5').update(buf, 'utf8').digest('hex') + '"',
|
||||
'Cache-Control': 'public, max-age=' + maxAge
|
||||
},
|
||||
body: buf
|
||||
};
|
||||
res.writeHead(200, content.headers);
|
||||
res.end(content.body);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
||||
var logging = config.logging,
|
||||
|
@ -198,13 +55,13 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
|||
oauthServer = oauth2orize.createServer();
|
||||
|
||||
// silence JSHint without disabling unused check for the whole file
|
||||
authStrategies = authStrategies;
|
||||
passport.use(authStrategies.clientPasswordStrategy);
|
||||
passport.use(authStrategies.bearerStrategy);
|
||||
|
||||
// Cache express server instance
|
||||
blogApp = blogAppInstance;
|
||||
middleware.cacheBlogApp(blogApp);
|
||||
middleware.api.cacheOauthServer(oauthServer);
|
||||
oauth.init(oauthServer, middleware.spamPrevention.resetCounter);
|
||||
oauth.init(oauthServer, spamPrevention.resetCounter);
|
||||
|
||||
// Make sure 'req.secure' is valid for proxied requests
|
||||
// (X-Forwarded-Proto header will be checked, if present)
|
||||
|
@ -229,8 +86,8 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
|||
|
||||
// First determine whether we're serving admin or theme content
|
||||
blogApp.use(decideIsAdmin);
|
||||
blogApp.use(updateActiveTheme);
|
||||
blogApp.use(configHbsForContext);
|
||||
blogApp.use(themeHandler(blogApp).updateActiveTheme);
|
||||
blogApp.use(themeHandler(blogApp).configHbsForContext);
|
||||
|
||||
// Admin only config
|
||||
blogApp.use('/ghost', express['static'](config.paths.clientAssets, {maxAge: utils.ONE_YEAR_MS}));
|
||||
|
@ -239,15 +96,15 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
|||
// NOTE: Importantly this is _after_ the check above for admin-theme static resources,
|
||||
// which do not need HTTPS. In fact, if HTTPS is forced on them, then 404 page might
|
||||
// not display properly when HTTPS is not available!
|
||||
blogApp.use(middleware.checkSSL);
|
||||
blogApp.use(checkSSL);
|
||||
adminApp.set('views', config.paths.adminViews);
|
||||
|
||||
// Theme only config
|
||||
blogApp.use(middleware.staticTheme());
|
||||
blogApp.use(staticTheme());
|
||||
|
||||
// Check if password protected blog
|
||||
blogApp.use(middleware.checkIsPrivate); // check if the blog is protected
|
||||
blogApp.use(middleware.filterPrivateRoutes);
|
||||
blogApp.use(privateBlogging.checkIsPrivate); // check if the blog is protected
|
||||
blogApp.use(privateBlogging.filterPrivateRoutes);
|
||||
|
||||
// Serve sitemap.xsl file
|
||||
blogApp.use(serveSharedFile('sitemap.xsl', 'text/xsl', utils.ONE_DAY_S));
|
||||
|
@ -274,24 +131,24 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
|||
|
||||
// ### Caching
|
||||
// Blog frontend is cacheable
|
||||
blogApp.use(middleware.cacheControl('public'));
|
||||
blogApp.use(cacheControl('public'));
|
||||
// Admin shouldn't be cached
|
||||
adminApp.use(middleware.cacheControl('private'));
|
||||
adminApp.use(cacheControl('private'));
|
||||
// API shouldn't be cached
|
||||
blogApp.use(routes.apiBaseUri, middleware.cacheControl('private'));
|
||||
blogApp.use(routes.apiBaseUri, cacheControl('private'));
|
||||
|
||||
// enable authentication
|
||||
blogApp.use(middleware.authenticate);
|
||||
blogApp.use(authenticate);
|
||||
|
||||
// local data
|
||||
blogApp.use(ghostLocals);
|
||||
blogApp.use(themeHandler(blogApp).ghostLocals);
|
||||
|
||||
// ### Routing
|
||||
// Set up API routes
|
||||
blogApp.use(routes.apiBaseUri, routes.api(middleware));
|
||||
|
||||
// Mount admin express app to /ghost and set up routes
|
||||
adminApp.use(middleware.redirectToSetup);
|
||||
adminApp.use(redirectToSetup);
|
||||
adminApp.use(routes.admin());
|
||||
blogApp.use('/ghost', adminApp);
|
||||
|
||||
|
@ -309,5 +166,3 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
|||
module.exports = setupMiddleware;
|
||||
// Export middleware functions directly
|
||||
module.exports.middleware = middleware;
|
||||
// Expose middleware functions in this file as well
|
||||
module.exports.middleware.redirectToSetup = redirectToSetup;
|
||||
|
|
|
@ -1,322 +0,0 @@
|
|||
// # Custom Middleware
|
||||
// The following custom middleware functions are all unit testable, and have accompanying unit tests in
|
||||
// middleware_spec.js
|
||||
|
||||
var _ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
express = require('express'),
|
||||
config = require('../config'),
|
||||
crypto = require('crypto'),
|
||||
path = require('path'),
|
||||
api = require('../api'),
|
||||
passport = require('passport'),
|
||||
Promise = require('bluebird'),
|
||||
errors = require('../errors'),
|
||||
session = require('cookie-session'),
|
||||
url = require('url'),
|
||||
utils = require('../utils'),
|
||||
|
||||
busboy = require('./ghost-busboy'),
|
||||
cacheControl = require('./cache-control'),
|
||||
spamPrevention = require('./spam-prevention'),
|
||||
clientAuth = require('./client-auth'),
|
||||
apiErrorHandlers = require('./api-error-handlers'),
|
||||
|
||||
middleware,
|
||||
blogApp;
|
||||
|
||||
function isBlackListedFileType(file) {
|
||||
var blackListedFileTypes = ['.hbs', '.md', '.json'],
|
||||
ext = path.extname(file);
|
||||
return _.contains(blackListedFileTypes, ext);
|
||||
}
|
||||
|
||||
function cacheBlogApp(app) {
|
||||
blogApp = app;
|
||||
}
|
||||
|
||||
function isSSLrequired(isAdmin, configUrl, forceAdminSSL) {
|
||||
var forceSSL = url.parse(configUrl).protocol === 'https:' ? true : false;
|
||||
if (forceSSL || (isAdmin && forceAdminSSL)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The guts of checkSSL. Indicate forbidden or redirect according to configuration.
|
||||
// Required args: forceAdminSSL, url and urlSSL should be passed from config. reqURL from req.url
|
||||
function sslForbiddenOrRedirect(opt) {
|
||||
var forceAdminSSL = opt.forceAdminSSL,
|
||||
reqUrl = opt.reqUrl, // expected to be relative-to-root
|
||||
baseUrl = url.parse(opt.configUrlSSL || opt.configUrl),
|
||||
response = {
|
||||
// Check if forceAdminSSL: { redirect: false } is set, which means
|
||||
// we should just deny non-SSL access rather than redirect
|
||||
isForbidden: (forceAdminSSL && forceAdminSSL.redirect !== undefined && !forceAdminSSL.redirect),
|
||||
|
||||
// Append the request path to the base configuration path, trimming out a double "//"
|
||||
redirectPathname: function redirectPathname() {
|
||||
var pathname = baseUrl.path;
|
||||
if (reqUrl[0] === '/' && pathname[pathname.length - 1] === '/') {
|
||||
pathname += reqUrl.slice(1);
|
||||
} else {
|
||||
pathname += reqUrl;
|
||||
}
|
||||
return pathname;
|
||||
},
|
||||
redirectUrl: function redirectUrl(query) {
|
||||
return url.format({
|
||||
protocol: 'https:',
|
||||
hostname: baseUrl.hostname,
|
||||
port: baseUrl.port,
|
||||
pathname: this.redirectPathname(),
|
||||
query: query
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function verifySessionHash(salt, hash) {
|
||||
if (!salt || !hash) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return api.settings.read({context: {internal: true}, key: 'password'}).then(function then(response) {
|
||||
var hasher = crypto.createHash('sha256');
|
||||
|
||||
hasher.update(response.settings[0].value + salt, 'utf8');
|
||||
|
||||
return hasher.digest('hex') === hash;
|
||||
});
|
||||
}
|
||||
|
||||
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 authenticate(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 replace(match, a) {
|
||||
return a;
|
||||
});
|
||||
|
||||
if (subPath.indexOf('/ghost/api/') === 0
|
||||
&& (path.indexOf('/ghost/api/v0.1/authentication/') !== 0
|
||||
|| (path.indexOf('/ghost/api/v0.1/authentication/setup/') === 0 && req.method === 'PUT'))) {
|
||||
return passport.authenticate('bearer', {session: false, failWithError: true},
|
||||
function authenticate(err, user, info) {
|
||||
if (err) {
|
||||
return next(err); // will generate a 500 error
|
||||
}
|
||||
// Generate a JSON response reflecting authentication status
|
||||
if (!user) {
|
||||
var error = {
|
||||
code: 401,
|
||||
errorType: 'NoPermissionError',
|
||||
message: 'Please Sign In'
|
||||
};
|
||||
|
||||
return apiErrorHandlers.errorHandler(error, req, res, next);
|
||||
}
|
||||
// TODO: figure out, why user & authInfo is lost
|
||||
req.authInfo = info;
|
||||
req.user = user;
|
||||
return next(null, user, info);
|
||||
}
|
||||
)(req, res, next);
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
// ### whenEnabled Middleware
|
||||
// Selectively use middleware
|
||||
// From https://github.com/senchalabs/connect/issues/676#issuecomment-9569658
|
||||
whenEnabled: function whenEnabled(setting, fn) {
|
||||
return function settingEnabled(req, res, next) {
|
||||
// Set from server/middleware/index.js for now
|
||||
if (blogApp.enabled(setting)) {
|
||||
fn(req, res, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
staticTheme: function staticTheme() {
|
||||
return function blackListStatic(req, res, next) {
|
||||
if (isBlackListedFileType(req.url)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return middleware.forwardToExpressStatic(req, res, next);
|
||||
};
|
||||
},
|
||||
|
||||
// to allow unit testing
|
||||
forwardToExpressStatic: function forwardToExpressStatic(req, res, next) {
|
||||
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function then(response) {
|
||||
var activeTheme = response.settings[0];
|
||||
|
||||
express['static'](path.join(config.paths.themePath, activeTheme.value), {maxAge: utils.ONE_YEAR_MS})(req, res, next);
|
||||
});
|
||||
},
|
||||
|
||||
// Check to see if we should use SSL
|
||||
// and redirect if needed
|
||||
checkSSL: function checkSSL(req, res, next) {
|
||||
if (isSSLrequired(res.isAdmin, config.url, config.forceAdminSSL)) {
|
||||
if (!req.secure) {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: config.forceAdminSSL,
|
||||
configUrlSSL: config.urlSSL,
|
||||
configUrl: config.url,
|
||||
reqUrl: req.url
|
||||
});
|
||||
|
||||
if (response.isForbidden) {
|
||||
return res.sendStatus(403);
|
||||
} else {
|
||||
return res.redirect(301, response.redirectUrl(req.query));
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
checkIsPrivate: function checkIsPrivate(req, res, next) {
|
||||
return api.settings.read({context: {internal: true}, key: 'isPrivate'}).then(function then(response) {
|
||||
var pass = response.settings[0];
|
||||
|
||||
if (_.isEmpty(pass.value) || pass.value === 'false') {
|
||||
res.isPrivateBlog = false;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.isPrivateBlog = true;
|
||||
|
||||
return session({
|
||||
maxAge: utils.ONE_MONTH_MS,
|
||||
signed: false
|
||||
})(req, res, next);
|
||||
});
|
||||
},
|
||||
|
||||
filterPrivateRoutes: function filterPrivateRoutes(req, res, next) {
|
||||
if (res.isAdmin || !res.isPrivateBlog || req.url.lastIndexOf('/private/', 0) === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// take care of rss and sitemap 404s
|
||||
if (req.url.lastIndexOf('/rss', 0) === 0 || req.url.lastIndexOf('/sitemap', 0) === 0) {
|
||||
return errors.error404(req, res, next);
|
||||
} else if (req.url.lastIndexOf('/robots.txt', 0) === 0) {
|
||||
fs.readFile(path.join(config.paths.corePath, 'shared', 'private-robots.txt'), function readFile(err, buf) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': buf.length,
|
||||
'Cache-Control': 'public, max-age=' + utils.ONE_HOUR_MS
|
||||
});
|
||||
res.end(buf);
|
||||
});
|
||||
} else {
|
||||
return middleware.authenticatePrivateSession(req, res, next);
|
||||
}
|
||||
},
|
||||
|
||||
authenticatePrivateSession: function authenticatePrivateSession(req, res, next) {
|
||||
var hash = req.session.token || '',
|
||||
salt = req.session.salt || '',
|
||||
url;
|
||||
|
||||
return verifySessionHash(salt, hash).then(function then(isVerified) {
|
||||
if (isVerified) {
|
||||
return next();
|
||||
} else {
|
||||
url = config.urlFor({relativeUrl: '/private/'});
|
||||
url += req.url === '/' ? '' : '?r=' + encodeURIComponent(req.url);
|
||||
return res.redirect(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// This is here so a call to /private/ after a session is verified will redirect to home;
|
||||
isPrivateSessionAuth: function isPrivateSessionAuth(req, res, next) {
|
||||
if (!res.isPrivateBlog) {
|
||||
return res.redirect(config.urlFor('home', true));
|
||||
}
|
||||
|
||||
var hash = req.session.token || '',
|
||||
salt = req.session.salt || '';
|
||||
|
||||
return verifySessionHash(salt, hash).then(function then(isVerified) {
|
||||
if (isVerified) {
|
||||
// redirect to home if user is already authenticated
|
||||
return res.redirect(config.urlFor('home', true));
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
authenticateProtection: function authenticateProtection(req, res, next) {
|
||||
// if errors have been generated from the previous call
|
||||
if (res.error) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var bodyPass = req.body.password;
|
||||
|
||||
return api.settings.read({context: {internal: true}, key: 'password'}).then(function then(response) {
|
||||
var pass = response.settings[0],
|
||||
hasher = crypto.createHash('sha256'),
|
||||
salt = Date.now().toString(),
|
||||
forward = req.query && req.query.r ? req.query.r : '/';
|
||||
|
||||
if (pass.value === bodyPass) {
|
||||
hasher.update(bodyPass + salt, 'utf8');
|
||||
req.session.token = hasher.digest('hex');
|
||||
req.session.salt = salt;
|
||||
|
||||
return res.redirect(config.urlFor({relativeUrl: decodeURIComponent(forward)}));
|
||||
} else {
|
||||
res.error = {
|
||||
message: 'Wrong password'
|
||||
};
|
||||
return next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
busboy: busboy,
|
||||
cacheControl: cacheControl,
|
||||
spamPrevention: spamPrevention
|
||||
};
|
||||
|
||||
module.exports = middleware;
|
||||
module.exports.cacheBlogApp = cacheBlogApp;
|
||||
|
||||
module.exports.api = {
|
||||
addClientSecret: clientAuth.addClientSecret,
|
||||
cacheOauthServer: clientAuth.cacheOauthServer,
|
||||
authenticateClient: clientAuth.authenticateClient,
|
||||
generateAccessToken: clientAuth.generateAccessToken,
|
||||
methodNotAllowed: apiErrorHandlers.methodNotAllowed,
|
||||
errorHandler: apiErrorHandlers.errorHandler
|
||||
};
|
||||
|
||||
// SSL helper functions are exported primarily for unity testing.
|
||||
module.exports.isSSLrequired = isSSLrequired;
|
||||
module.exports.sslForbiddenOrRedirect = sslForbiddenOrRedirect;
|
136
core/server/middleware/private-blogging.js
Normal file
136
core/server/middleware/private-blogging.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
var _ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
config = require('../config'),
|
||||
crypto = require('crypto'),
|
||||
path = require('path'),
|
||||
api = require('../api'),
|
||||
Promise = require('bluebird'),
|
||||
errors = require('../errors'),
|
||||
session = require('cookie-session'),
|
||||
utils = require('../utils'),
|
||||
private;
|
||||
|
||||
function verifySessionHash(salt, hash) {
|
||||
if (!salt || !hash) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return api.settings.read({context: {internal: true}, key: 'password'}).then(function then(response) {
|
||||
var hasher = crypto.createHash('sha256');
|
||||
|
||||
hasher.update(response.settings[0].value + salt, 'utf8');
|
||||
|
||||
return hasher.digest('hex') === hash;
|
||||
});
|
||||
}
|
||||
|
||||
private = {
|
||||
checkIsPrivate: function checkIsPrivate(req, res, next) {
|
||||
return api.settings.read({context: {internal: true}, key: 'isPrivate'}).then(function then(response) {
|
||||
var pass = response.settings[0];
|
||||
|
||||
if (_.isEmpty(pass.value) || pass.value === 'false') {
|
||||
res.isPrivateBlog = false;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.isPrivateBlog = true;
|
||||
|
||||
return session({
|
||||
maxAge: utils.ONE_MONTH_MS,
|
||||
signed: false
|
||||
})(req, res, next);
|
||||
});
|
||||
},
|
||||
|
||||
filterPrivateRoutes: function filterPrivateRoutes(req, res, next) {
|
||||
if (res.isAdmin || !res.isPrivateBlog || req.url.lastIndexOf('/private/', 0) === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// take care of rss and sitemap 404s
|
||||
if (req.url.lastIndexOf('/rss', 0) === 0 || req.url.lastIndexOf('/sitemap', 0) === 0) {
|
||||
return errors.error404(req, res, next);
|
||||
} else if (req.url.lastIndexOf('/robots.txt', 0) === 0) {
|
||||
fs.readFile(path.join(config.paths.corePath, 'shared', 'private-robots.txt'), function readFile(err, buf) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': buf.length,
|
||||
'Cache-Control': 'public, max-age=' + utils.ONE_HOUR_MS
|
||||
});
|
||||
res.end(buf);
|
||||
});
|
||||
} else {
|
||||
return private.authenticatePrivateSession(req, res, next);
|
||||
}
|
||||
},
|
||||
|
||||
authenticatePrivateSession: function authenticatePrivateSession(req, res, next) {
|
||||
var hash = req.session.token || '',
|
||||
salt = req.session.salt || '',
|
||||
url;
|
||||
|
||||
return verifySessionHash(salt, hash).then(function then(isVerified) {
|
||||
if (isVerified) {
|
||||
return next();
|
||||
} else {
|
||||
url = config.urlFor({relativeUrl: '/private/'});
|
||||
url += req.url === '/' ? '' : '?r=' + encodeURIComponent(req.url);
|
||||
return res.redirect(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// This is here so a call to /private/ after a session is verified will redirect to home;
|
||||
isPrivateSessionAuth: function isPrivateSessionAuth(req, res, next) {
|
||||
if (!res.isPrivateBlog) {
|
||||
return res.redirect(config.urlFor('home', true));
|
||||
}
|
||||
|
||||
var hash = req.session.token || '',
|
||||
salt = req.session.salt || '';
|
||||
|
||||
return verifySessionHash(salt, hash).then(function then(isVerified) {
|
||||
if (isVerified) {
|
||||
// redirect to home if user is already authenticated
|
||||
return res.redirect(config.urlFor('home', true));
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
authenticateProtection: function authenticateProtection(req, res, next) {
|
||||
// if errors have been generated from the previous call
|
||||
if (res.error) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var bodyPass = req.body.password;
|
||||
|
||||
return api.settings.read({context: {internal: true}, key: 'password'}).then(function then(response) {
|
||||
var pass = response.settings[0],
|
||||
hasher = crypto.createHash('sha256'),
|
||||
salt = Date.now().toString(),
|
||||
forward = req.query && req.query.r ? req.query.r : '/';
|
||||
|
||||
if (pass.value === bodyPass) {
|
||||
hasher.update(bodyPass + salt, 'utf8');
|
||||
req.session.token = hasher.digest('hex');
|
||||
req.session.salt = salt;
|
||||
|
||||
return res.redirect(config.urlFor({relativeUrl: decodeURIComponent(forward)}));
|
||||
} else {
|
||||
res.error = {
|
||||
message: 'Wrong password'
|
||||
};
|
||||
return next();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = private;
|
16
core/server/middleware/redirect-to-setup.js
Normal file
16
core/server/middleware/redirect-to-setup.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
var api = require('../api'),
|
||||
config = require('../config');
|
||||
|
||||
// Redirect to setup if no user exists
|
||||
function redirectToSetup(req, res, next) {
|
||||
api.authentication.isSetup().then(function then(exists) {
|
||||
if (!exists.setup[0].status && !req.path.match(/\/setup\//)) {
|
||||
return res.redirect(config.paths.subdir + '/ghost/setup/');
|
||||
}
|
||||
next();
|
||||
}).catch(function handleError(err) {
|
||||
return next(new Error(err));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = redirectToSetup;
|
45
core/server/middleware/serve-shared-file.js
Normal file
45
core/server/middleware/serve-shared-file.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
var crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
config = require('../config');
|
||||
|
||||
// ### ServeSharedFile Middleware
|
||||
// Handles requests to robots.txt and favicon.ico (and caches them)
|
||||
function serveSharedFile(file, type, maxAge) {
|
||||
var content,
|
||||
filePath = path.join(config.paths.corePath, 'shared', file),
|
||||
re = /(\{\{blog-url\}\})/g;
|
||||
|
||||
return function serveSharedFile(req, res, next) {
|
||||
if (req.url === '/' + file) {
|
||||
if (content) {
|
||||
res.writeHead(200, content.headers);
|
||||
res.end(content.body);
|
||||
} else {
|
||||
fs.readFile(filePath, function readFile(err, buf) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (type === 'text/xsl' || type === 'text/plain') {
|
||||
buf = buf.toString().replace(re, config.url.replace(/\/$/, ''));
|
||||
}
|
||||
content = {
|
||||
headers: {
|
||||
'Content-Type': type,
|
||||
'Content-Length': buf.length,
|
||||
ETag: '"' + crypto.createHash('md5').update(buf, 'utf8').digest('hex') + '"',
|
||||
'Cache-Control': 'public, max-age=' + maxAge
|
||||
},
|
||||
body: buf
|
||||
};
|
||||
res.writeHead(200, content.headers);
|
||||
res.end(content.body);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = serveSharedFile;
|
31
core/server/middleware/static-theme.js
Normal file
31
core/server/middleware/static-theme.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
var _ = require('lodash'),
|
||||
express = require('express'),
|
||||
path = require('path'),
|
||||
api = require('../api'),
|
||||
config = require('../config'),
|
||||
utils = require('../utils');
|
||||
|
||||
function isBlackListedFileType(file) {
|
||||
var blackListedFileTypes = ['.hbs', '.md', '.json'],
|
||||
ext = path.extname(file);
|
||||
return _.contains(blackListedFileTypes, ext);
|
||||
}
|
||||
|
||||
function forwardToExpressStatic(req, res, next) {
|
||||
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function then(response) {
|
||||
var activeTheme = response.settings[0];
|
||||
|
||||
express['static'](path.join(config.paths.themePath, activeTheme.value), {maxAge: utils.ONE_YEAR_MS})(req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
function staticTheme() {
|
||||
return function blackListStatic(req, res, next) {
|
||||
if (isBlackListedFileType(req.url)) {
|
||||
return next();
|
||||
}
|
||||
return forwardToExpressStatic(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = staticTheme;
|
124
core/server/middleware/theme-handler.js
Normal file
124
core/server/middleware/theme-handler.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
var _ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
hbs = require('express-hbs'),
|
||||
api = require('../api'),
|
||||
config = require('../config'),
|
||||
errors = require('../errors'),
|
||||
themeHandler;
|
||||
|
||||
themeHandler = function (blogApp) {
|
||||
var handlerMethods = {
|
||||
// ### GhostLocals Middleware
|
||||
// Expose the standard locals that every external page should have available,
|
||||
// separating between the theme and the admin
|
||||
ghostLocals: function ghostLocals(req, res, next) {
|
||||
// Make sure we have a locals value.
|
||||
res.locals = res.locals || {};
|
||||
res.locals.version = config.ghostVersion;
|
||||
res.locals.safeVersion = config.ghostVersion.match(/^(\d+\.)?(\d+)/)[0];
|
||||
// relative path from the URL
|
||||
res.locals.relativeUrl = req.path;
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
// ### configHbsForContext Middleware
|
||||
// Setup handlebars for the current context (admin or theme)
|
||||
configHbsForContext: function configHbsForContext(req, res, next) {
|
||||
var themeData = config.theme;
|
||||
if (req.secure && config.urlSSL) {
|
||||
// For secure requests override .url property with the SSL version
|
||||
themeData = _.clone(themeData);
|
||||
themeData.url = config.urlSSL.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
hbs.updateTemplateOptions({data: {blog: themeData}});
|
||||
|
||||
if (config.paths.themePath && blogApp.get('activeTheme')) {
|
||||
blogApp.set('views', path.join(config.paths.themePath, blogApp.get('activeTheme')));
|
||||
}
|
||||
|
||||
// Pass 'secure' flag to the view engine
|
||||
// so that templates can choose 'url' vs 'urlSSL'
|
||||
res.locals.secure = req.secure;
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
// ### Activate Theme
|
||||
// Helper for updateActiveTheme
|
||||
activateTheme: function activateTheme(activeTheme) {
|
||||
var hbsOptions,
|
||||
themePartials = path.join(config.paths.themePath, activeTheme, 'partials');
|
||||
|
||||
// clear the view cache
|
||||
blogApp.cache = {};
|
||||
|
||||
// set view engine
|
||||
hbsOptions = {
|
||||
partialsDir: [config.paths.helperTemplates],
|
||||
onCompile: function onCompile(exhbs, source) {
|
||||
return exhbs.handlebars.compile(source, {preventIndent: true});
|
||||
}
|
||||
};
|
||||
|
||||
fs.stat(themePartials, function stat(err, stats) {
|
||||
// Check that the theme has a partials directory before trying to use it
|
||||
if (!err && stats && stats.isDirectory()) {
|
||||
hbsOptions.partialsDir.push(themePartials);
|
||||
}
|
||||
});
|
||||
|
||||
blogApp.engine('hbs', hbs.express3(hbsOptions));
|
||||
|
||||
// Update user error template
|
||||
errors.updateActiveTheme(activeTheme);
|
||||
|
||||
// Set active theme variable on the express server
|
||||
blogApp.set('activeTheme', activeTheme);
|
||||
},
|
||||
|
||||
// ### updateActiveTheme
|
||||
// Updates the blogApp's activeTheme variable and subsequently
|
||||
// activates that theme's views with the hbs templating engine if it
|
||||
// is not yet activated.
|
||||
updateActiveTheme: function updateActiveTheme(req, res, next) {
|
||||
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function then(response) {
|
||||
var activeTheme = response.settings[0];
|
||||
|
||||
// Check if the theme changed
|
||||
if (activeTheme.value !== blogApp.get('activeTheme')) {
|
||||
// Change theme
|
||||
if (!config.paths.availableThemes.hasOwnProperty(activeTheme.value)) {
|
||||
if (!res.isAdmin) {
|
||||
// Throw an error if the theme is not available, but not on the admin UI
|
||||
return errors.throwError('The currently active theme ' + activeTheme.value + ' is missing.');
|
||||
} else {
|
||||
// At this point the activated theme is not present and the current
|
||||
// request is for the admin client. In order to allow the user access
|
||||
// to the admin client we set an hbs instance on the app so that middleware
|
||||
// processing can continue.
|
||||
blogApp.engine('hbs', hbs.express3());
|
||||
errors.logWarn('The currently active theme "' + activeTheme.value + '" is missing.');
|
||||
|
||||
return next();
|
||||
}
|
||||
} else {
|
||||
handlerMethods.activateTheme(activeTheme.value);
|
||||
}
|
||||
}
|
||||
next();
|
||||
}).catch(function handleError(err) {
|
||||
// Trying to start up without the active theme present, setup a simple hbs instance
|
||||
// and render an error page straight away.
|
||||
blogApp.engine('hbs', hbs.express3());
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return handlerMethods;
|
||||
};
|
||||
|
||||
module.exports = themeHandler;
|
|
@ -36,13 +36,13 @@ frontendRoutes = function frontendRoutes(middleware) {
|
|||
// password-protected frontend route
|
||||
privateRouter.route('/')
|
||||
.get(
|
||||
middleware.isPrivateSessionAuth,
|
||||
middleware.privateBlogging.isPrivateSessionAuth,
|
||||
frontend.private
|
||||
)
|
||||
.post(
|
||||
middleware.isPrivateSessionAuth,
|
||||
middleware.privateBlogging.isPrivateSessionAuth,
|
||||
middleware.spamPrevention.protected,
|
||||
middleware.authenticateProtection,
|
||||
middleware.privateBlogging.authenticateProtection,
|
||||
frontend.private
|
||||
);
|
||||
|
||||
|
|
145
core/test/unit/middleware/authentication_spec.js
Normal file
145
core/test/unit/middleware/authentication_spec.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*globals describe, it, beforeEach, afterEach */
|
||||
/*jshint expr:true*/
|
||||
var sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
passport = require('passport'),
|
||||
authenticate = require('../../../server/middleware/authenticate'),
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
user = {id: 1},
|
||||
info = {scope: '*'},
|
||||
token = 'test_token';
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
function registerSuccessfulBearerStrategy() {
|
||||
// register fake BearerStrategy which always authenticates
|
||||
passport.use(new BearerStrategy(
|
||||
function strategy(accessToken, done) {
|
||||
accessToken.should.eql(token);
|
||||
return done(null, user, info);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
function registerUnsuccessfulBearerStrategy() {
|
||||
// register fake BearerStrategy which always authenticates
|
||||
passport.use(new BearerStrategy(
|
||||
function strategy(accessToken, done) {
|
||||
accessToken.should.eql(token);
|
||||
return done(null, false);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
function registerSuccessfulBearerStrategy() {
|
||||
// register fake BearerStrategy which always authenticates
|
||||
passport.use(new BearerStrategy(
|
||||
function strategy(accessToken, done) {
|
||||
accessToken.should.eql(token);
|
||||
return done(null, user, info);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
describe('authenticate', function () {
|
||||
var res, req, next, sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
req = {};
|
||||
res = {};
|
||||
next = sandbox.spy();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should skip authentication if not hitting /ghost', function (done) {
|
||||
req.path = '/tag/foo/';
|
||||
req.method = 'GET';
|
||||
|
||||
registerSuccessfulBearerStrategy();
|
||||
authenticate(req, res, next);
|
||||
|
||||
next.called.should.be.true;
|
||||
next.calledWith().should.be.true;
|
||||
done();
|
||||
});
|
||||
|
||||
it('should skip authentication if hitting /ghost/api/v0.1/authenticaton/', function (done) {
|
||||
req.path = '/ghost/api/v0.1/authentication/';
|
||||
req.method = 'GET';
|
||||
|
||||
registerSuccessfulBearerStrategy();
|
||||
authenticate(req, res, next);
|
||||
|
||||
next.called.should.be.true;
|
||||
next.calledWith().should.be.true;
|
||||
done();
|
||||
});
|
||||
|
||||
it('should skip authentication if hitting GET /ghost/api/v0.1/authenticaton/setup/', function (done) {
|
||||
req.path = '/ghost/api/v0.1/authentication/setup/';
|
||||
req.method = 'GET';
|
||||
|
||||
registerSuccessfulBearerStrategy();
|
||||
authenticate(req, res, next);
|
||||
|
||||
next.called.should.be.true;
|
||||
next.calledWith().should.be.true;
|
||||
done();
|
||||
});
|
||||
|
||||
it('should authentication if hitting PUT /ghost/api/v0.1/authenticaton/setup/', function (done) {
|
||||
req.path = '/ghost/api/v0.1/authentication/setup/';
|
||||
req.method = 'PUT';
|
||||
req.headers = {};
|
||||
req.headers.authorization = 'Bearer ' + token;
|
||||
|
||||
registerSuccessfulBearerStrategy();
|
||||
authenticate(req, res, next);
|
||||
|
||||
next.called.should.be.true;
|
||||
next.calledWith(null, user, info).should.be.true;
|
||||
done();
|
||||
});
|
||||
|
||||
it('should authenticate if hitting /ghost/api/ endpoint', function (done) {
|
||||
req.path = '/ghost/api/v0.1/test/';
|
||||
req.method = 'PUT';
|
||||
req.headers = {};
|
||||
req.headers.authorization = 'Bearer ' + token;
|
||||
|
||||
registerSuccessfulBearerStrategy();
|
||||
authenticate(req, res, next);
|
||||
|
||||
next.called.should.be.true;
|
||||
next.calledWith(null, user, info).should.be.true;
|
||||
done();
|
||||
});
|
||||
|
||||
it('shouldn\'t authenticate if hitting /ghost/ auth endpoint with invalid credentials', function (done) {
|
||||
res.status = {};
|
||||
req.path = '/ghost/api/v0.1/test/';
|
||||
req.method = 'PUT';
|
||||
req.headers = {};
|
||||
req.headers.authorization = 'Bearer ' + token;
|
||||
|
||||
registerUnsuccessfulBearerStrategy();
|
||||
|
||||
// stub res.status for error handling
|
||||
sandbox.stub(res, 'status', function (statusCode) {
|
||||
statusCode.should.eql(401);
|
||||
return {
|
||||
json: function (err) {
|
||||
err.errors[0].errorType.should.eql('NoPermissionError');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
authenticate(req, res, next);
|
||||
next.called.should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
92
core/test/unit/middleware/check-ssl_spec.js
Normal file
92
core/test/unit/middleware/check-ssl_spec.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*globals describe, it, beforeEach, afterEach */
|
||||
/*jshint expr:true*/
|
||||
var sinon = require('sinon'),
|
||||
checkSSL = require('../../../server/middleware/check-ssl');
|
||||
|
||||
describe('checkSSL', function () {
|
||||
var sandbox, res, req, next;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
req = sinon.spy();
|
||||
res = sinon.spy();
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('skips if already on SSL', function () {
|
||||
res.isAdmin = true;
|
||||
req.isSecure = true;
|
||||
checkSSL(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSSLRequired', function () {
|
||||
var isSSLrequired = checkSSL.isSSLrequired;
|
||||
|
||||
it('SSL is required if config.url starts with https', function () {
|
||||
isSSLrequired(undefined, 'https://example.com', undefined).should.be.true;
|
||||
});
|
||||
|
||||
it('SSL is required if isAdmin and config.forceAdminSSL is set', function () {
|
||||
isSSLrequired(true, 'http://example.com', true).should.be.true;
|
||||
});
|
||||
|
||||
it('SSL is not required if config.url starts with "http:/" and forceAdminSSL is not set', function () {
|
||||
isSSLrequired(false, 'http://example.com', false).should.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('sslForbiddenOrRedirect', function () {
|
||||
var sslForbiddenOrRedirect = checkSSL.sslForbiddenOrRedirect;
|
||||
it('Return forbidden if config forces admin SSL for AdminSSL redirect is false.', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: false},
|
||||
configUrl: 'http://example.com'
|
||||
});
|
||||
response.isForbidden.should.be.true;
|
||||
});
|
||||
|
||||
it('If not forbidden, should produce SSL to redirect to when config.url ends with no slash', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.isForbidden.should.be.false;
|
||||
response.redirectUrl({}).should.equal('https://example.com/config/path/req/path');
|
||||
});
|
||||
|
||||
it('If config ends is slash, potential double-slash in resulting URL is removed', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path/',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.redirectUrl({}).should.equal('https://example.com/config/path/req/path');
|
||||
});
|
||||
|
||||
it('If config.urlSSL is provided it is preferred over config.url', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path/',
|
||||
configUrlSSL: 'https://example.com/ssl/config/path/',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.redirectUrl({}).should.equal('https://example.com/ssl/config/path/req/path');
|
||||
});
|
||||
|
||||
it('query string in request is preserved in redirect URL', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path/',
|
||||
configUrlSSL: 'https://example.com/ssl/config/path/',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.redirectUrl({a: 'b'}).should.equal('https://example.com/ssl/config/path/req/path?a=b');
|
||||
});
|
||||
});
|
252
core/test/unit/middleware/private-blogging_spec.js
Normal file
252
core/test/unit/middleware/private-blogging_spec.js
Normal file
|
@ -0,0 +1,252 @@
|
|||
/*globals describe, beforeEach, afterEach, it*/
|
||||
/*jshint expr:true*/
|
||||
var crypto = require('crypto'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
privateBlogging = require('../../../server/middleware/private-blogging'),
|
||||
api = require('../../../server/api'),
|
||||
errors = require('../../../server/errors'),
|
||||
fs = require('fs');
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
function hash(password, salt) {
|
||||
var hasher = crypto.createHash('sha256');
|
||||
|
||||
hasher.update(password + salt, 'utf8');
|
||||
|
||||
return hasher.digest('hex');
|
||||
}
|
||||
|
||||
describe('Private Blogging', function () {
|
||||
var sandbox,
|
||||
apiSettingsStub;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('passProtect', function () {
|
||||
var req, res, next;
|
||||
|
||||
beforeEach(function () {
|
||||
req = {}, res = {};
|
||||
apiSettingsStub = sandbox.stub(api.settings, 'read');
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
it('checkIsPrivate should call next if not private', function (done) {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'isPrivate',
|
||||
value: 'false'
|
||||
}]
|
||||
}));
|
||||
|
||||
privateBlogging.checkIsPrivate(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
res.isPrivateBlog.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('checkIsPrivate should load session if private', function (done) {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'isPrivate',
|
||||
value: 'true'
|
||||
}]
|
||||
}));
|
||||
|
||||
privateBlogging.checkIsPrivate(req, res, next).then(function () {
|
||||
res.isPrivateBlog.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
describe('not private', function () {
|
||||
beforeEach(function () {
|
||||
res.isPrivateBlog = false;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if not private', function () {
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should redirect if blog is not private', function () {
|
||||
res = {
|
||||
redirect: sinon.spy(),
|
||||
isPrivateBlog: false
|
||||
};
|
||||
privateBlogging.isPrivateSessionAuth(req, res, next);
|
||||
res.redirect.called.should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('private', function () {
|
||||
var errorSpy;
|
||||
|
||||
beforeEach(function () {
|
||||
res.isPrivateBlog = true;
|
||||
errorSpy = sandbox.spy(errors, 'error404');
|
||||
res = {
|
||||
status: function () {
|
||||
return this;
|
||||
},
|
||||
send: function () {},
|
||||
set: function () {},
|
||||
isPrivateBlog: true
|
||||
};
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if admin', function () {
|
||||
res.isAdmin = true;
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if is the "private" route', function () {
|
||||
req.url = '/private/';
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is sitemap', function () {
|
||||
req.url = '/sitemap.xml';
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is rss', function () {
|
||||
req.url = '/rss';
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is rss plus something', function () {
|
||||
req.url = '/rss/sometag';
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should render custom robots.txt', function () {
|
||||
req.url = '/robots.txt';
|
||||
res.writeHead = sinon.spy();
|
||||
res.end = sinon.spy();
|
||||
sandbox.stub(fs, 'readFile', function (file, cb) {
|
||||
cb(null, 'User-agent: * Disallow: /');
|
||||
});
|
||||
privateBlogging.filterPrivateRoutes(req, res, next);
|
||||
res.writeHead.called.should.be.true;
|
||||
res.end.called.should.be.true;
|
||||
});
|
||||
|
||||
it('authenticateProtection should call next if error', function () {
|
||||
res.error = 'Test Error';
|
||||
privateBlogging.authenticateProtection(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
describe('with hash verification', function () {
|
||||
beforeEach(function () {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'password')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'password',
|
||||
value: 'rightpassword'
|
||||
}]
|
||||
}));
|
||||
});
|
||||
|
||||
it('authenticatePrivateSession should return next if hash is verified', function (done) {
|
||||
var salt = Date.now().toString();
|
||||
|
||||
req.session = {
|
||||
token: hash('rightpassword', salt),
|
||||
salt: salt
|
||||
};
|
||||
|
||||
privateBlogging.authenticatePrivateSession(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('authenticatePrivateSession should redirect if hash is not verified', function (done) {
|
||||
req.url = '/welcome-to-ghost';
|
||||
req.session = {
|
||||
token: 'wrongpassword',
|
||||
salt: Date.now().toString()
|
||||
};
|
||||
res.redirect = sinon.spy();
|
||||
|
||||
privateBlogging.authenticatePrivateSession(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should redirect if hash is verified', function (done) {
|
||||
var salt = Date.now().toString();
|
||||
|
||||
req.session = {
|
||||
token: hash('rightpassword', salt),
|
||||
salt: salt
|
||||
};
|
||||
res.redirect = sandbox.spy();
|
||||
|
||||
privateBlogging.isPrivateSessionAuth(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should return next if hash is not verified', function (done) {
|
||||
req.session = {
|
||||
token: 'wrongpassword',
|
||||
salt: Date.now().toString()
|
||||
};
|
||||
|
||||
privateBlogging.isPrivateSessionAuth(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('authenticateProtection should return next if password is incorrect', function (done) {
|
||||
req.body = {password: 'wrongpassword'};
|
||||
|
||||
privateBlogging.authenticateProtection(req, res, next).then(function () {
|
||||
res.error.should.not.be.empty;
|
||||
next.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('authenticateProtection should redirect if password is correct', function (done) {
|
||||
req.body = {password: 'rightpassword'};
|
||||
req.session = {};
|
||||
res.redirect = sandbox.spy();
|
||||
|
||||
privateBlogging.authenticateProtection(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
74
core/test/unit/middleware/redirect-to-setup_spec.js
Normal file
74
core/test/unit/middleware/redirect-to-setup_spec.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*globals describe, it, beforeEach, afterEach */
|
||||
/*jshint expr:true*/
|
||||
var sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
api = require('../../../server/api'),
|
||||
redirectToSetup = require('../../../server/middleware/redirect-to-setup');
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
describe('redirectToSetup', function () {
|
||||
var res, req, next, sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
res = sinon.spy();
|
||||
req = sinon.spy();
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should redirect to setup if not on setup', function (done) {
|
||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
||||
return Promise.resolve({setup: [{status: false}]});
|
||||
});
|
||||
|
||||
req.path = '/';
|
||||
res.redirect = sinon.spy(function () {
|
||||
next.called.should.be.false;
|
||||
res.redirect.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
|
||||
redirectToSetup(req, res, next);
|
||||
});
|
||||
|
||||
it('should not redirect if setup is done', function (done) {
|
||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
||||
return Promise.resolve({setup: [{status: true}]});
|
||||
});
|
||||
|
||||
res = {redirect: sinon.spy()};
|
||||
req.path = '/';
|
||||
|
||||
next = sinon.spy(function () {
|
||||
next.called.should.be.true;
|
||||
res.redirect.called.should.be.false;
|
||||
done();
|
||||
});
|
||||
|
||||
redirectToSetup(req, res, next);
|
||||
});
|
||||
|
||||
it('should not redirect if already on setup', function (done) {
|
||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
||||
return Promise.resolve({setup: [{status: false}]});
|
||||
});
|
||||
|
||||
res = {redirect: sinon.spy()};
|
||||
req.path = '/ghost/setup/';
|
||||
|
||||
next = sinon.spy(function () {
|
||||
next.called.should.be.true;
|
||||
res.redirect.called.should.be.false;
|
||||
done();
|
||||
});
|
||||
|
||||
redirectToSetup(req, res, next);
|
||||
});
|
||||
});
|
105
core/test/unit/middleware/serve-shared-file_spec.js
Normal file
105
core/test/unit/middleware/serve-shared-file_spec.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*globals describe, it, beforeEach, afterEach */
|
||||
/*jshint expr:true*/
|
||||
var fs = require('fs'),
|
||||
sinon = require('sinon'),
|
||||
serveSharedFile = require('../../../server/middleware/serve-shared-file');
|
||||
|
||||
describe('serveSharedFile', function () {
|
||||
var res, req, next, sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
res = sinon.spy();
|
||||
req = sinon.spy();
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should return a middleware', function () {
|
||||
var result = serveSharedFile('robots.txt', 'text/plain', 3600);
|
||||
|
||||
result.should.be.a.Function;
|
||||
});
|
||||
|
||||
it('should skip if the request does NOT match the file', function () {
|
||||
var middleware = serveSharedFile('robots.txt', 'text/plain', 3600);
|
||||
req.url = '/favicon.ico';
|
||||
middleware(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should load the file and send it', function () {
|
||||
var middleware = serveSharedFile('robots.txt', 'text/plain', 3600),
|
||||
body = 'User-agent: * Disallow: /';
|
||||
req.url = '/robots.txt';
|
||||
|
||||
sandbox.stub(fs, 'readFile', function (file, cb) {
|
||||
cb(null, body);
|
||||
});
|
||||
|
||||
res = {
|
||||
writeHead: sinon.spy(),
|
||||
end: sinon.spy()
|
||||
};
|
||||
|
||||
middleware(req, res, next);
|
||||
next.called.should.be.false;
|
||||
res.writeHead.called.should.be.true;
|
||||
res.writeHead.args[0][0].should.equal(200);
|
||||
res.writeHead.calledWith(200, sinon.match.has('Content-Type')).should.be.true;
|
||||
res.writeHead.calledWith(200, sinon.match.has('Content-Length')).should.be.true;
|
||||
res.writeHead.calledWith(200, sinon.match.has('ETag')).should.be.true;
|
||||
res.writeHead.calledWith(200, sinon.match.has('Cache-Control', 'public, max-age=3600')).should.be.true;
|
||||
|
||||
res.end.calledWith(body).should.be.true;
|
||||
});
|
||||
|
||||
it('should send the correct headers', function () {
|
||||
var middleware = serveSharedFile('robots.txt', 'text/plain', 3600),
|
||||
body = 'User-agent: * Disallow: /';
|
||||
req.url = '/robots.txt';
|
||||
|
||||
sandbox.stub(fs, 'readFile', function (file, cb) {
|
||||
cb(null, body);
|
||||
});
|
||||
|
||||
res = {
|
||||
writeHead: sinon.spy(),
|
||||
end: sinon.spy()
|
||||
};
|
||||
|
||||
middleware(req, res, next);
|
||||
next.called.should.be.false;
|
||||
res.writeHead.called.should.be.true;
|
||||
res.writeHead.args[0][0].should.equal(200);
|
||||
res.writeHead.calledWith(200, sinon.match.has('Content-Type')).should.be.true;
|
||||
res.writeHead.calledWith(200, sinon.match.has('Content-Length')).should.be.true;
|
||||
res.writeHead.calledWith(200, sinon.match.has('ETag')).should.be.true;
|
||||
res.writeHead.calledWith(200, sinon.match.has('Cache-Control', 'public, max-age=3600')).should.be.true;
|
||||
});
|
||||
|
||||
it('should replace {{blog-url}} in text/plain', function () {
|
||||
var middleware = serveSharedFile('robots.txt', 'text/plain', 3600),
|
||||
body = 'User-agent: {{blog-url}}';
|
||||
req.url = '/robots.txt';
|
||||
|
||||
sandbox.stub(fs, 'readFile', function (file, cb) {
|
||||
cb(null, body);
|
||||
});
|
||||
|
||||
res = {
|
||||
writeHead: sinon.spy(),
|
||||
end: sinon.spy()
|
||||
};
|
||||
|
||||
middleware(req, res, next);
|
||||
next.called.should.be.false;
|
||||
res.writeHead.called.should.be.true;
|
||||
|
||||
res.end.calledWith('User-agent: http://default.com:2368').should.be.true;
|
||||
});
|
||||
});
|
71
core/test/unit/middleware/static-theme_spec.js
Normal file
71
core/test/unit/middleware/static-theme_spec.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*globals describe, it, beforeEach */
|
||||
/*jshint expr:true*/
|
||||
var sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
|
||||
api = require('../../../server/api'),
|
||||
express = require('express'),
|
||||
staticTheme = require('../../../server/middleware/static-theme');
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
describe('staticTheme', function () {
|
||||
var next;
|
||||
|
||||
beforeEach(function () {
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
it('should call next if hbs file type', function () {
|
||||
var req = {
|
||||
url: 'mytemplate.hbs'
|
||||
};
|
||||
|
||||
staticTheme(null)(req, null, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should call next if md file type', function () {
|
||||
var req = {
|
||||
url: 'README.md'
|
||||
};
|
||||
|
||||
staticTheme(null)(req, null, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should call next if json file type', function () {
|
||||
var req = {
|
||||
url: 'sample.json'
|
||||
};
|
||||
|
||||
staticTheme(null)(req, null, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should call express.static if valid file type', function (done) {
|
||||
var req = {
|
||||
url: 'myvalidfile.css'
|
||||
},
|
||||
settingsStub,
|
||||
sandbox = sinon.sandbox.create(),
|
||||
expressStatic = sinon.spy(express, 'static');
|
||||
|
||||
settingsStub = sandbox.stub(api.settings, 'read').withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'activeKey',
|
||||
value: 'casper'
|
||||
}]
|
||||
}));
|
||||
|
||||
staticTheme(null)(req, null, function (reqArg, res, next2) {
|
||||
/*jshint unused:false */
|
||||
sandbox.restore();
|
||||
next.called.should.be.false;
|
||||
expressStatic.called.should.be.true;
|
||||
expressStatic.args[0][1].maxAge.should.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
125
core/test/unit/middleware/theme-handler_spec.js
Normal file
125
core/test/unit/middleware/theme-handler_spec.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*globals describe, it, beforeEach, afterEach */
|
||||
/*jshint expr:true*/
|
||||
var _ = require('lodash'),
|
||||
sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
express = require('express'),
|
||||
|
||||
// Stuff we test
|
||||
themeHandler = require('../../../server/middleware/theme-handler'),
|
||||
errors = require('../../../server/errors'),
|
||||
|
||||
config = require('../../../server/config'),
|
||||
origConfig = _.cloneDeep(config),
|
||||
defaultConfig = require('../../../../config.example')[process.env.NODE_ENV];
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
describe('Theme Handler', function () {
|
||||
var req, res, next, blogApp, handler, sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
req = sinon.spy();
|
||||
res = sinon.spy();
|
||||
next = sinon.spy();
|
||||
blogApp = express();
|
||||
handler = themeHandler(blogApp);
|
||||
sandbox = sinon.sandbox.create();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
|
||||
// Reset config
|
||||
config.set(_.merge({}, origConfig, defaultConfig));
|
||||
});
|
||||
|
||||
describe('ghostLocals', function () {
|
||||
it('sets all locals', function () {
|
||||
req.path = '/awesome-post';
|
||||
|
||||
handler.ghostLocals(req, res, next);
|
||||
|
||||
res.locals.should.be.an.Object;
|
||||
res.locals.version.should.exist;
|
||||
res.locals.safeVersion.should.exist;
|
||||
res.locals.relativeUrl.should.equal(req.path);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateTheme', function () {
|
||||
it('should activate new theme', function () {
|
||||
var errorStub = sandbox.stub(errors, 'updateActiveTheme');
|
||||
handler.activateTheme('casper');
|
||||
|
||||
errorStub.calledWith('casper').should.be.true;
|
||||
blogApp.get('activeTheme').should.equal('casper');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configHbsForContext', function () {
|
||||
it('calls next', function () {
|
||||
req.secure = true;
|
||||
res.locals = {};
|
||||
handler.configHbsForContext(req, res, next);
|
||||
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('sets secure local variable', function () {
|
||||
req.secure = true;
|
||||
res.locals = {};
|
||||
|
||||
handler.configHbsForContext(req, res, next);
|
||||
|
||||
res.locals.secure.should.equal(req.secure);
|
||||
});
|
||||
|
||||
it('sets view path', function () {
|
||||
req.secure = true;
|
||||
res.locals = {};
|
||||
blogApp.set('activeTheme', 'casper');
|
||||
|
||||
handler.configHbsForContext(req, res, next);
|
||||
|
||||
blogApp.get('views').should.not.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
// describe('updateActiveTheme', function () {
|
||||
// it('updates the active theme if changed', function () {
|
||||
// var activateThemeSpy = sinon.spy(handler, 'activateTheme');
|
||||
// sandbox.stub(api.settings, 'read').withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({
|
||||
// settings: [{
|
||||
// key: 'activeKey',
|
||||
// value: 'casper'
|
||||
// }]
|
||||
// }));
|
||||
// blogApp.set('activeTheme', 'not-casper');
|
||||
// config.set({paths: {availableThemes: {casper: {}}}});
|
||||
//
|
||||
// handler.updateActiveTheme(req, res, next);
|
||||
//
|
||||
// activateThemeSpy.called.should.be.false;
|
||||
// next.called.should.be.false;
|
||||
// });
|
||||
//
|
||||
// it('throws error if theme is missing', function () {
|
||||
// var errorSpy = sinon.spy(errors, 'throwError');
|
||||
// sandbox.stub(api.settings, 'read').withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({
|
||||
// settings: [{
|
||||
// key: 'activeKey',
|
||||
// value: 'rasper'
|
||||
// }]
|
||||
// }));
|
||||
// blogApp.set('activeTheme', 'not-casper');
|
||||
// config.set({paths: {availableThemes: {casper: {}}}});
|
||||
//
|
||||
// handler.updateActiveTheme(req, res, next);
|
||||
//
|
||||
// errorSpy.called.should.be.true;
|
||||
// next.called.should.be.false;
|
||||
// });
|
||||
// });
|
||||
});
|
|
@ -1,413 +0,0 @@
|
|||
/*globals describe, beforeEach, afterEach, it*/
|
||||
/*jshint expr:true*/
|
||||
var assert = require('assert'),
|
||||
crypto = require('crypto'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
middleware = require('../../server/middleware').middleware,
|
||||
api = require('../../server/api'),
|
||||
errors = require('../../server/errors'),
|
||||
fs = require('fs');
|
||||
|
||||
function hash(password, salt) {
|
||||
var hasher = crypto.createHash('sha256');
|
||||
|
||||
hasher.update(password + salt, 'utf8');
|
||||
|
||||
return hasher.digest('hex');
|
||||
}
|
||||
|
||||
describe('Middleware', function () {
|
||||
var sandbox,
|
||||
apiSettingsStub;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('whenEnabled', function () {
|
||||
var cbFn, blogApp;
|
||||
|
||||
beforeEach(function () {
|
||||
cbFn = sinon.spy();
|
||||
blogApp = {
|
||||
enabled: function (setting) {
|
||||
if (setting === 'enabled') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
middleware.cacheBlogApp(blogApp);
|
||||
});
|
||||
|
||||
it('should call function if setting is enabled', function (done) {
|
||||
var req = 1, res = 2, next = 3;
|
||||
|
||||
middleware.whenEnabled('enabled', function (a, b, c) {
|
||||
assert.equal(a, 1);
|
||||
assert.equal(b, 2);
|
||||
assert.equal(c, 3);
|
||||
done();
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
it('should call next() if setting is disabled', function (done) {
|
||||
middleware.whenEnabled('rando', cbFn)(null, null, function (a) {
|
||||
should.not.exist(a);
|
||||
cbFn.calledOnce.should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('staticTheme', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(middleware, 'forwardToExpressStatic').yields();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
middleware.forwardToExpressStatic.restore();
|
||||
});
|
||||
|
||||
it('should call next if hbs file type', function (done) {
|
||||
var req = {
|
||||
url: 'mytemplate.hbs'
|
||||
};
|
||||
|
||||
middleware.staticTheme(null)(req, null, function (a) {
|
||||
should.not.exist(a);
|
||||
middleware.forwardToExpressStatic.calledOnce.should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if md file type', function (done) {
|
||||
var req = {
|
||||
url: 'README.md'
|
||||
};
|
||||
|
||||
middleware.staticTheme(null)(req, null, function (a) {
|
||||
should.not.exist(a);
|
||||
middleware.forwardToExpressStatic.calledOnce.should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if json file type', function (done) {
|
||||
var req = {
|
||||
url: 'sample.json'
|
||||
};
|
||||
|
||||
middleware.staticTheme(null)(req, null, function (a) {
|
||||
should.not.exist(a);
|
||||
middleware.forwardToExpressStatic.calledOnce.should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call express.static if valid file type', function (done) {
|
||||
var req = {
|
||||
url: 'myvalidfile.css'
|
||||
};
|
||||
|
||||
middleware.staticTheme(null)(req, null, function (reqArg, res, next) {
|
||||
/*jshint unused:false */
|
||||
middleware.forwardToExpressStatic.calledOnce.should.be.true;
|
||||
assert.deepEqual(middleware.forwardToExpressStatic.args[0][0], req);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSSLRequired', function () {
|
||||
var isSSLrequired = middleware.isSSLrequired;
|
||||
|
||||
it('SSL is required if config.url starts with https', function () {
|
||||
isSSLrequired(undefined, 'https://example.com', undefined).should.be.true;
|
||||
});
|
||||
|
||||
it('SSL is required if isAdmin and config.forceAdminSSL is set', function () {
|
||||
isSSLrequired(true, 'http://example.com', true).should.be.true;
|
||||
});
|
||||
|
||||
it('SSL is not required if config.url starts with "http:/" and forceAdminSSL is not set', function () {
|
||||
isSSLrequired(false, 'http://example.com', false).should.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('sslForbiddenOrRedirect', function () {
|
||||
var sslForbiddenOrRedirect = middleware.sslForbiddenOrRedirect;
|
||||
it('Return forbidden if config forces admin SSL for AdminSSL redirect is false.', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: false},
|
||||
configUrl: 'http://example.com'
|
||||
});
|
||||
response.isForbidden.should.be.true;
|
||||
});
|
||||
|
||||
it('If not forbidden, should produce SSL to redirect to when config.url ends with no slash', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.isForbidden.should.be.false;
|
||||
response.redirectUrl({}).should.equal('https://example.com/config/path/req/path');
|
||||
});
|
||||
|
||||
it('If config ends is slash, potential double-slash in resulting URL is removed', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path/',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.redirectUrl({}).should.equal('https://example.com/config/path/req/path');
|
||||
});
|
||||
|
||||
it('If config.urlSSL is provided it is preferred over config.url', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path/',
|
||||
configUrlSSL: 'https://example.com/ssl/config/path/',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.redirectUrl({}).should.equal('https://example.com/ssl/config/path/req/path');
|
||||
});
|
||||
|
||||
it('query string in request is preserved in redirect URL', function () {
|
||||
var response = sslForbiddenOrRedirect({
|
||||
forceAdminSSL: {redirect: true},
|
||||
configUrl: 'http://example.com/config/path/',
|
||||
configUrlSSL: 'https://example.com/ssl/config/path/',
|
||||
reqUrl: '/req/path'
|
||||
});
|
||||
response.redirectUrl({a: 'b'}).should.equal('https://example.com/ssl/config/path/req/path?a=b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('passProtect', function () {
|
||||
var req, res, next;
|
||||
|
||||
beforeEach(function () {
|
||||
req = {}, res = {};
|
||||
apiSettingsStub = sandbox.stub(api.settings, 'read');
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
it('checkIsPrivate should call next if not private', function (done) {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'isPrivate',
|
||||
value: 'false'
|
||||
}]
|
||||
}));
|
||||
|
||||
middleware.checkIsPrivate(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
res.isPrivateBlog.should.be.false;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('checkIsPrivate should load session if private', function (done) {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'isPrivate',
|
||||
value: 'true'
|
||||
}]
|
||||
}));
|
||||
|
||||
middleware.checkIsPrivate(req, res, next).then(function () {
|
||||
res.isPrivateBlog.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
describe('not private', function () {
|
||||
beforeEach(function () {
|
||||
res.isPrivateBlog = false;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if not private', function () {
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should redirect if blog is not private', function () {
|
||||
res = {
|
||||
redirect: sinon.spy(),
|
||||
isPrivateBlog: false
|
||||
};
|
||||
middleware.isPrivateSessionAuth(req, res, next);
|
||||
res.redirect.called.should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('private', function () {
|
||||
var errorSpy;
|
||||
|
||||
beforeEach(function () {
|
||||
res.isPrivateBlog = true;
|
||||
errorSpy = sandbox.spy(errors, 'error404');
|
||||
res = {
|
||||
status: function () {
|
||||
return this;
|
||||
},
|
||||
send: function () {},
|
||||
set: function () {},
|
||||
isPrivateBlog: true
|
||||
};
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if admin', function () {
|
||||
res.isAdmin = true;
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if is the "private" route', function () {
|
||||
req.url = '/private/';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is sitemap', function () {
|
||||
req.url = '/sitemap.xml';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is rss', function () {
|
||||
req.url = '/rss';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is rss plus something', function () {
|
||||
req.url = '/rss/sometag';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should render custom robots.txt', function () {
|
||||
req.url = '/robots.txt';
|
||||
res.writeHead = sinon.spy();
|
||||
res.end = sinon.spy();
|
||||
sandbox.stub(fs, 'readFile', function (file, cb) {
|
||||
cb(null, 'User-agent: * Disallow: /');
|
||||
});
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
res.writeHead.called.should.be.true;
|
||||
res.end.called.should.be.true;
|
||||
});
|
||||
|
||||
it('authenticateProtection should call next if error', function () {
|
||||
res.error = 'Test Error';
|
||||
middleware.authenticateProtection(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
describe('with hash verification', function () {
|
||||
beforeEach(function () {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'password')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'password',
|
||||
value: 'rightpassword'
|
||||
}]
|
||||
}));
|
||||
});
|
||||
|
||||
it('authenticatePrivateSession should return next if hash is verified', function (done) {
|
||||
var salt = Date.now().toString();
|
||||
|
||||
req.session = {
|
||||
token: hash('rightpassword', salt),
|
||||
salt: salt
|
||||
};
|
||||
|
||||
middleware.authenticatePrivateSession(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('authenticatePrivateSession should redirect if hash is not verified', function (done) {
|
||||
req.url = '/welcome-to-ghost';
|
||||
req.session = {
|
||||
token: 'wrongpassword',
|
||||
salt: Date.now().toString()
|
||||
};
|
||||
res.redirect = sinon.spy();
|
||||
|
||||
middleware.authenticatePrivateSession(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should redirect if hash is verified', function (done) {
|
||||
var salt = Date.now().toString();
|
||||
|
||||
req.session = {
|
||||
token: hash('rightpassword', salt),
|
||||
salt: salt
|
||||
};
|
||||
res.redirect = sandbox.spy();
|
||||
|
||||
middleware.isPrivateSessionAuth(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should return next if hash is not verified', function (done) {
|
||||
req.session = {
|
||||
token: 'wrongpassword',
|
||||
salt: Date.now().toString()
|
||||
};
|
||||
|
||||
middleware.isPrivateSessionAuth(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('authenticateProtection should return next if password is incorrect', function (done) {
|
||||
req.body = {password: 'wrongpassword'};
|
||||
|
||||
middleware.authenticateProtection(req, res, next).then(function () {
|
||||
res.error.should.not.be.empty;
|
||||
next.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('authenticateProtection should redirect if password is correct', function (done) {
|
||||
req.body = {password: 'rightpassword'};
|
||||
req.session = {};
|
||||
res.redirect = sandbox.spy();
|
||||
|
||||
middleware.authenticateProtection(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue