Files
NodeBB/src/webserver.js

608 lines
20 KiB
JavaScript
Raw Normal View History

var path = require('path'),
fs = require('fs'),
2014-01-04 18:05:15 -05:00
nconf = require('nconf'),
express = require('express'),
2013-07-10 16:22:03 -04:00
express_namespace = require('express-namespace'),
WebServer = express(),
2014-01-04 18:05:15 -05:00
server,
winston = require('winston'),
2013-10-04 01:46:50 -04:00
validator = require('validator'),
async = require('async'),
utils = require('../public/src/utils'),
templates = require('./../public/src/templates'), // todo remove
translator = require('./../public/src/translator'),
2013-12-02 17:10:26 -05:00
db = require('./database'),
user = require('./user'),
notifications = require('./notifications'),
auth = require('./routes/authentication'),
meta = require('./meta'),
plugins = require('./plugins'),
logger = require('./logger'),
2014-02-27 14:55:41 -05:00
controllers = require('./controllers'),
middleware = require('./middleware'),
admin = require('./routes/admin'),
apiRoute = require('./routes/api'),
feedsRoute = require('./routes/feeds'),
metaRoute = require('./routes/meta');
2013-05-02 15:57:43 -04:00
2014-01-04 18:05:15 -05:00
if(nconf.get('ssl')) {
server = require('https').createServer({
key: fs.readFileSync(nconf.get('ssl').key),
2014-01-04 18:09:43 -05:00
cert: fs.readFileSync(nconf.get('ssl').cert)
2014-01-04 18:05:15 -05:00
}, WebServer);
} else {
server = require('http').createServer(WebServer);
}
// Signals
2014-02-22 02:27:14 -05:00
var shutdown = function(code) {
winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.');
db.close();
winston.info('[app] Database connection closed.');
winston.info('[app] Shutdown complete.');
2014-02-22 02:27:14 -05:00
process.exit();
},
restart = function() {
if (process.send) {
winston.info('[app] Restarting...');
process.send('nodebb:restart');
} else {
winston.error('[app] Could not restart server. Shutting down.');
shutdown();
}
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('SIGHUP', restart);
process.on('uncaughtException', function(err) {
winston.error('[app] Encountered Uncaught Exception: ' + err.message);
console.log(err.stack);
restart();
});
2013-09-23 12:50:27 -04:00
(function (app) {
2013-11-11 13:25:54 -05:00
"use strict";
// this can be moved to app.js
var clientScripts;
2013-11-25 16:28:07 -05:00
plugins.ready(function() {
// Minify client-side libraries
meta.js.get(function (err, scripts) {
clientScripts = scripts.map(function (script) {
script = {
script: script
};
return script;
});
});
});
2013-08-23 13:14:36 -04:00
logger.init(app);
async.series({
themesData: meta.themes.get,
currentThemeData: function(next) {
db.getObjectFields('config', ['theme:type', 'theme:id', 'theme:staticDir', 'theme:templates'], next);
}
}, function(err, data) {
middleware(app, data);
if (err) {
winston.error('Errors were encountered while attempting to initialise NodeBB.');
process.exit();
} else {
if (process.env.NODE_ENV === 'development') {
winston.info('Middlewares loaded.');
}
}
});
2014-02-27 14:55:41 -05:00
app.prepareAPI = function(req, res, next) {
res.locals.isAPI = true;
next();
};
app.authenticate = function(req, res, next) {
if(!req.user) {
if (res.locals.isAPI) {
return res.json(403, 'not-allowed');
} else {
return res.redirect('403');
}
} else {
next();
}
};
app.checkGlobalPrivacySettings = function(req, res, next) {
var callerUID = req.user ? parseInt(req.user.uid, 10) : 0;
if (!callerUID && !!parseInt(meta.config.privateUserInfo, 10)) {
if (res.locals.isAPI) {
return res.json(403, 'not-allowed');
} else {
return res.redirect('403');
}
}
next();
};
app.checkAccountPermissions = function(req, res, next) {
var callerUID = req.user ? parseInt(req.user.uid, 10) : 0;
// this function requires userslug to be passed in. todo: /user/uploadpicture should pass in userslug I think
user.getUidByUserslug(req.params.userslug, function (err, uid) {
if (err) {
return next(err);
}
// not sure if this check really should belong here. also make sure we're not doing this check again in the actual method
if (!uid) {
if (res.locals.isAPI) {
return res.json(404);
} else {
return res.redirect('404');
}
}
if (parseInt(uid, 10) === callerUID) {
return next();
}
user.isAdministrator(callerUID, function(err, isAdmin) {
if(err) {
return next(err);
}
if(isAdmin) {
next();
}
if (res.locals.isAPI) {
return res.json(403, 'not-allowed');
} else {
return res.redirect('403');
}
});
});
};
2014-02-27 14:55:41 -05:00
app.buildHeader = function(req, res, next) {
async.parallel([
function(next) {
// temp, don't forget to set metaTags and linkTags to res.locals.header
app.build_header({
req: req,
res: res
}, function(err, template) {
res.locals.header = template;
next(err);
});
},
function(next) {
// this is slower than the original implementation because the rendered template is not cached
// but I didn't bother to fix this because we will deprecate [filter:footer.build] in favour of the widgets system by 0.4x
plugins.fireHook('filter:footer.build', '', function(err, appendHTML) {
app.render('footer', {footerHTML: appendHTML}, function(err, template) {
translator.translate(template, function(parsedTemplate) {
res.locals.footer = template;
next(err);
});
});
2014-02-27 14:55:41 -05:00
});
}
], function(err) {
next();
});
};
/**
* `options` object requires: req, res
2013-12-31 20:28:31 -05:00
* accepts: metaTags, linkTags
*/
2013-09-23 12:50:27 -04:00
app.build_header = function (options, callback) {
var custom_header = {
'navigation': []
};
plugins.fireHook('filter:header.build', custom_header, function(err, custom_header) {
var defaultMetaTags = [{
name: 'viewport',
content: 'width=device-width, initial-scale=1.0, user-scalable=no'
}, {
name: 'content-type',
content: 'text/html; charset=UTF-8'
}, {
name: 'apple-mobile-web-app-capable',
content: 'yes'
}, {
property: 'og:site_name',
content: meta.config.title || 'NodeBB'
}, {
property: 'keywords',
2013-11-11 13:25:54 -05:00
content: meta.config.keywords || ''
}],
2014-02-19 21:47:26 -05:00
defaultLinkTags = [{
rel: 'apple-touch-icon',
2014-02-19 21:47:26 -05:00
href: '/apple-touch-icon'
}],
templateValues = {
bootswatchCSS: meta.config['theme:src'],
pluginCSS: plugins.cssFiles.map(function(file) { return { path: nconf.get('relative_path') + file.replace(/\\/g, '/') }; }),
2013-10-22 12:39:14 -04:00
title: meta.config.title || '',
description: meta.config.description || '',
2013-10-22 12:39:14 -04:00
'brand:logo': meta.config['brand:logo'] || '',
'brand:logo:display': meta.config['brand:logo']?'':'hide',
csrf: options.res.locals.csrf_token,
relative_path: nconf.get('relative_path'),
clientScripts: clientScripts,
navigation: custom_header.navigation,
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
allowRegistration: meta.config.allowRegistration === undefined || parseInt(meta.config.allowRegistration, 10) === 1,
searchEnabled: plugins.hasListeners('filter:search.query') ? true : false
2013-12-31 20:53:24 -05:00
},
escapeList = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
"'": '&apos;',
'"': '&quot;'
};
var uid = '0';
2013-12-31 20:28:31 -05:00
// Meta Tags
2014-02-27 14:55:41 -05:00
/*templateValues.metaTags = defaultMetaTags.concat(options.metaTags || []).map(function(tag) {
2014-02-27 01:51:33 -05:00
if(!tag || typeof tag.content !== 'string') {
winston.warn('Invalid meta tag. ', tag);
2014-02-27 01:43:24 -05:00
return tag;
}
2013-12-31 20:53:24 -05:00
tag.content = tag.content.replace(/[&<>'"]/g, function(tag) {
return escapeList[tag] || tag;
});
2013-12-31 20:28:31 -05:00
return tag;
2014-02-27 14:55:41 -05:00
});*/
// Link Tags
2014-02-27 14:55:41 -05:00
/*templateValues.linkTags = defaultLinkTags.concat(options.linkTags || []);
templateValues.linkTags.push({
rel: "icon",
type: "image/x-icon",
2014-02-10 22:24:36 -05:00
href: nconf.get('relative_path') + '/favicon.ico'
2014-02-27 14:55:41 -05:00
});*/
2013-12-31 20:28:31 -05:00
2013-11-11 13:25:54 -05:00
if(options.req.user && options.req.user.uid) {
uid = options.req.user.uid;
2013-11-11 13:25:54 -05:00
}
2014-01-31 13:17:28 -05:00
// Custom CSS
templateValues.useCustomCSS = false;
if (meta.config.useCustomCSS === '1') {
templateValues.useCustomCSS = true;
templateValues.customCSS = meta.config.customCSS;
}
async.parallel([
function(next) {
translator.get('pages:' + path.basename(options.req.url), function(translated) {
2014-02-27 14:55:41 -05:00
/*var metaTitle = templateValues.metaTags.filter(function(tag) {
return tag.name === 'title';
});
if (translated) {
templateValues.browserTitle = translated;
} else if (metaTitle.length > 0 && metaTitle[0].content) {
templateValues.browserTitle = metaTitle[0].content;
} else {
templateValues.browserTitle = meta.config.browserTitle || 'NodeBB';
2014-02-27 14:55:41 -05:00
}*/
next();
});
},
function(next) {
user.isAdministrator(uid, function(err, isAdmin) {
templateValues.isAdmin = isAdmin || false;
next();
});
}
], function() {
2014-02-27 14:55:41 -05:00
/*translator.translate(templates.header.parse(templateValues), function(template) {
callback(null, template);
2014-02-27 14:55:41 -05:00
});*/
app.render('header', templateValues, function(err, template) {
callback(null, template)
});
2013-11-11 13:25:54 -05:00
});
});
2013-06-20 16:04:58 -04:00
};
2013-04-22 16:51:32 +00:00
// Cache static files on production
if (global.env !== 'development') {
app.enable('cache');
app.enable('minification');
// Configure cache-buster timestamp
require('child_process').exec('git describe --tags', {
cwd: path.join(__dirname, '../')
}, function(err, stdOut) {
if (!err) {
meta.config['cache-buster'] = stdOut.trim();
// winston.info('[init] Cache buster value set to: ' + stdOut);
} else {
2014-02-13 12:26:43 -05:00
fs.stat(path.join(__dirname, '../package.json'), function(err, stats) {
meta.config['cache-buster'] = new Date(stats.mtime).getTime();
});
}
});
}
if (nconf.get('port') != 80 && nconf.get('port') != 443 && nconf.get('use_port') === false) {
winston.info('Enabling \'trust proxy\'');
app.enable('trust proxy');
}
if ((nconf.get('port') == 80 || nconf.get('port') == 443) && process.env.NODE_ENV !== 'development') {
winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md');
}
module.exports.server = server;
2013-09-23 12:50:27 -04:00
module.exports.init = function () {
// translate all static templates served by webserver here. ex. footer, logout
2014-01-23 17:08:33 -05:00
plugins.fireHook('action:app.load', app);
2013-11-22 11:42:42 -05:00
2014-02-27 14:55:41 -05:00
/*translator.translate(templates.logout.toString(), function(parsedTemplate) {
2013-11-11 13:25:54 -05:00
templates.logout = parsedTemplate;
2014-02-27 14:55:41 -05:00
});*/
server.on("error", function(e){
if (e.code === 'EADDRINUSE') {
winston.error('NodeBB address in use, exiting...');
process.exit(1);
} else {
throw e;
}
});
var port = nconf.get('PORT') || nconf.get('port');
winston.info('NodeBB attempting to listen on: ' + ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port);
server.listen(port, nconf.get('bind_address'), function(){
winston.info('NodeBB Ready');
});
2013-11-11 13:25:54 -05:00
};
2013-09-23 12:50:27 -04:00
app.create_route = function (url, tpl) { // to remove
var routerScript = '<script> \
ajaxify.initialLoad = true; \
templates.ready(function(){ajaxify.go("' + url + '", null, true);}); \
</script>';
return routerScript;
};
2013-08-23 13:14:36 -04:00
2013-09-23 12:50:27 -04:00
app.namespace(nconf.get('relative_path'), function () {
auth.registerApp(app);
metaRoute.createRoutes(app);
2013-11-12 12:41:16 -05:00
admin.createRoutes(app);
apiRoute.createRoutes(app);
feedsRoute.createRoutes(app);
2013-07-10 21:31:58 -04:00
// Basic Routes (entirely client-side parsed, goal is to move the rest of the crap in this file into this one section)
2013-09-23 12:50:27 -04:00
(function () {
var routes = [],
2014-02-27 16:52:46 -05:00
loginRequired = ['notifications'];
2013-08-23 13:14:36 -04:00
async.each(routes.concat(loginRequired), function(route, next) {
app.get('/' + route, function (req, res) {
if (loginRequired.indexOf(route) !== -1 && !req.user) {
return res.redirect('/403');
}
app.build_header({
req: req,
res: res
}, function (err, header) {
res.send((isNaN(parseInt(route, 10)) ? 200 : parseInt(route, 10)), header + app.create_route(route) + templates.footer);
2013-07-10 21:31:58 -04:00
});
});
});
2013-07-10 21:31:58 -04:00
}());
2013-08-23 13:14:36 -04:00
2014-02-27 17:04:41 -05:00
/* Main */
2014-02-27 14:55:41 -05:00
app.get('/', app.buildHeader, controllers.home);
app.get('/api/home', app.prepareAPI, controllers.home);
2014-02-27 16:52:46 -05:00
app.get('/login', app.buildHeader, controllers.login);
app.get('/api/login', app.prepareAPI, controllers.login);
app.get('/register', app.buildHeader, controllers.register);
app.get('/api/register', app.prepareAPI, controllers.register);
2014-02-27 17:04:41 -05:00
app.get('/confirm/:code', app.buildHeader, controllers.confirmEmail);
app.get('/api/confirm/:code', app.prepareAPI, controllers.confirmEmail);
2014-02-27 17:16:06 -05:00
app.get('/sitemap.xml', controllers.sitemap);
app.get('/robots.txt', controllers.robots);
app.get('/outgoing', app.buildHeader, controllers.outgoing);
app.get('/api/outgoing', app.prepareAPI, controllers.outgoing);
/* Static Pages */
app.get('/404', app.buildHeader, controllers.static['404']);
app.get('/api/404', app.prepareAPI, controllers.static['404']);
app.get('/403', app.buildHeader, controllers.static['403']);
app.get('/api/403', app.prepareAPI, controllers.static['403']);
app.get('/500', app.buildHeader, controllers.static['500']);
app.get('/api/500', app.prepareAPI, controllers.static['500']);
2014-02-27 17:04:41 -05:00
/* Topics */
2014-02-27 14:55:41 -05:00
app.get('/topic/:topic_id/:slug?', app.buildHeader, controllers.topics.get);
app.get('/api/topic/:topic_id/:slug?', app.prepareAPI, controllers.topics.get);
2014-02-27 17:04:41 -05:00
/* Categories */
2014-02-27 14:55:41 -05:00
app.get('/popular/:set?', app.buildHeader, controllers.categories.popular);
app.get('/api/popular/:set?', app.prepareAPI, controllers.categories.popular);
app.get('/recent/:term?', app.buildHeader, controllers.categories.recent);
app.get('/api/recent/:term?', app.prepareAPI, controllers.categories.recent);
2014-02-27 16:39:34 -05:00
app.get('/unread/', app.buildHeader, app.authenticate, controllers.categories.unread);
app.get('/api/unread/', app.prepareAPI, app.authenticate, controllers.categories.unread);
app.get('/unread/total', app.buildHeader, app.authenticate, controllers.categories.unreadTotal);
app.get('/api/unread/total', app.prepareAPI, app.authenticate, controllers.categories.unreadTotal);
2014-02-27 14:55:41 -05:00
app.get('/category/:category_id/:slug?', app.buildHeader, controllers.categories.get);
app.get('/api/category/:category_id/:slug?', app.prepareAPI, controllers.categories.get);
/* Accounts */
app.get('/user/:userslug', app.buildHeader, app.checkGlobalPrivacySettings, controllers.accounts.getAccount);
app.get('/api/user/:userslug', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.accounts.getAccount);
app.get('/user/:userslug/following', app.buildHeader, app.checkGlobalPrivacySettings, controllers.accounts.getFollowing);
app.get('/api/user/:userslug/following', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.accounts.getFollowing);
app.get('/user/:userslug/followers', app.buildHeader, app.checkGlobalPrivacySettings, controllers.accounts.getFollowers);
app.get('/api/user/:userslug/followers', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.accounts.getFollowers);
app.get('/user/:userslug/favourites', app.buildHeader, app.checkGlobalPrivacySettings, app.checkAccountPermissions, controllers.accounts.getFavourites);
app.get('/api/user/:userslug/favourites', app.prepareAPI, app.checkGlobalPrivacySettings, app.checkAccountPermissions, controllers.accounts.getFavourites);
app.get('/user/:userslug/posts', app.buildHeader, app.checkGlobalPrivacySettings, controllers.accounts.getPosts);
app.get('/api/user/:userslug/posts', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.accounts.getPosts);
app.get('/user/:userslug/edit', app.buildHeader, app.checkGlobalPrivacySettings, app.checkAccountPermissions, controllers.accounts.accountEdit);
app.get('/api/user/:userslug/edit', app.prepareAPI, app.checkGlobalPrivacySettings, app.checkAccountPermissions, controllers.accounts.accountEdit);
// todo: admin recently gained access to this page, pls check if it actually works
app.get('/user/:userslug/settings', app.buildHeader, app.checkGlobalPrivacySettings, app.checkAccountPermissions, controllers.accounts.accountSettings);
app.get('/api/user/:userslug/settings', app.prepareAPI, app.checkGlobalPrivacySettings, app.checkAccountPermissions, controllers.accounts.accountSettings);
app.get('/api/user/uid/:uid', app.checkGlobalPrivacySettings, controllers.accounts.getUserByUID);
2014-02-28 15:26:39 -05:00
// this should have been in the API namespace
// also, perhaps pass in :userslug so we can use checkAccountPermissions middleware, in future will allow admins to upload a picture for a user
app.post('/user/uploadpicture', app.prepareAPI, app.checkGlobalPrivacySettings, /*app.checkAccountPermissions,*/ controllers.accounts.uploadPicture);
2014-02-28 15:26:39 -05:00
2014-02-28 14:19:43 -05:00
/* Users */
app.get('/users', app.buildHeader, app.checkGlobalPrivacySettings, controllers.users.getOnlineUsers);
app.get('/api/users', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.users.getOnlineUsers);
// was this duped by accident or purpose?
app.get('/users/online', app.buildHeader, app.checkGlobalPrivacySettings, controllers.users.getOnlineUsers);
app.get('/api/users/online', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.users.getOnlineUsers);
app.get('/users/sort-posts', app.buildHeader, app.checkGlobalPrivacySettings, controllers.users.getUsersSortedByPosts);
app.get('/api/users/sort-posts', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.users.getUsersSortedByPosts);
app.get('/users/sort-reputation', app.buildHeader, app.checkGlobalPrivacySettings, controllers.users.getUsersSortedByReputation);
app.get('/api/users/sort-reputation', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.users.getUsersSortedByReputation);
app.get('/users/latest', app.buildHeader, app.checkGlobalPrivacySettings, controllers.users.getUsersSortedByJoinDate);
app.get('/api/users/latest', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.users.getUsersSortedByJoinDate);
app.get('/users/search', app.buildHeader, app.checkGlobalPrivacySettings, controllers.users.getUsersForSearch);
app.get('/api/users/search', app.prepareAPI, app.checkGlobalPrivacySettings, controllers.users.getUsersForSearch);
2014-01-30 19:52:32 -05:00
2013-12-21 00:07:00 -05:00
app.get('/search/:term?', function (req, res) {
2013-12-12 22:51:17 -05:00
if (!req.user && meta.config.allowGuestSearching !== '1') {
2013-09-23 14:40:31 -04:00
return res.redirect('/403');
2013-11-11 13:25:54 -05:00
}
2013-12-21 00:07:00 -05:00
if(!req.params.term) {
req.params.term = '';
}
2013-09-17 13:09:37 -04:00
app.build_header({
req: req,
res: res
2013-09-23 12:50:27 -04:00
}, function (err, header) {
res.send(header + app.create_route('search/' + req.params.term, null, 'search') + templates.footer);
});
});
2013-08-23 13:14:36 -04:00
2013-11-03 17:15:18 -05:00
// Other routes
require('./routes/plugins')(app);
2013-10-25 16:01:31 -04:00
// Debug routes
if (process.env.NODE_ENV === 'development') {
require('./routes/debug')(app);
}
var custom_routes = {
'routes': [],
'api': [],
'templates': []
};
app.get_custom_templates = function() {
return custom_routes.templates.map(function(tpl) {
return tpl.template.split('.tpl')[0];
});
2014-02-20 02:05:49 -05:00
};
plugins.ready(function() {
plugins.fireHook('filter:server.create_routes', custom_routes, function(err, custom_routes) {
var routes = custom_routes.routes;
for (var route in routes) {
if (routes.hasOwnProperty(route)) {
(function(route) {
app[routes[route].method || 'get'](routes[route].route, function(req, res) {
routes[route].options(req, res, function(options) {
app.build_header({
req: options.req || req,
res: options.res || res
}, function (err, header) {
2013-11-11 13:25:54 -05:00
res.send(header + options.content + templates.footer);
});
});
});
}(route));
}
}
var apiRoutes = custom_routes.api;
for (var route in apiRoutes) {
if (apiRoutes.hasOwnProperty(route)) {
(function(route) {
app[apiRoutes[route].method || 'get']('/api' + apiRoutes[route].route, function(req, res) {
apiRoutes[route].callback(req, res, function(data) {
res.json(data);
});
});
}(route));
}
}
var templateRoutes = custom_routes.templates;
for (var route in templateRoutes) {
if (templateRoutes.hasOwnProperty(route)) {
(function(route) {
app.get('/templates/' + templateRoutes[route].template, function(req, res) {
res.send(templateRoutes[route].content);
});
}(route));
}
}
});
});
});
2013-04-22 16:51:32 +00:00
}(WebServer));