diff options
Diffstat (limited to 'src/node/hooks/express')
-rw-r--r-- | src/node/hooks/express/adminplugins.js | 51 | ||||
-rw-r--r-- | src/node/hooks/express/apicalls.js | 60 | ||||
-rw-r--r-- | src/node/hooks/express/errorhandling.js | 52 | ||||
-rw-r--r-- | src/node/hooks/express/importexport.js | 41 | ||||
-rw-r--r-- | src/node/hooks/express/padreadonly.js | 65 | ||||
-rw-r--r-- | src/node/hooks/express/padurlsanitize.js | 29 | ||||
-rw-r--r-- | src/node/hooks/express/socketio.js | 49 | ||||
-rw-r--r-- | src/node/hooks/express/specialpages.js | 46 | ||||
-rw-r--r-- | src/node/hooks/express/static.js | 57 | ||||
-rw-r--r-- | src/node/hooks/express/webaccess.js | 51 |
10 files changed, 501 insertions, 0 deletions
diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js new file mode 100644 index 00000000..fa7e7077 --- /dev/null +++ b/src/node/hooks/express/adminplugins.js @@ -0,0 +1,51 @@ +var path = require('path'); +var eejs = require('ep_etherpad-lite/node/eejs'); +var installer = require('ep_etherpad-lite/static/js/pluginfw/installer'); +var plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.get('/admin/plugins', function(req, res) { + var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); + var render_args = { + plugins: plugins.plugins, + search_results: {}, + errors: [], + }; + + res.send(eejs.require( + "ep_etherpad-lite/templates/admin/plugins.html", + render_args), {}); + }); +} + +exports.socketio = function (hook_name, args, cb) { + var io = args.io.of("/pluginfw/installer"); + io.on('connection', function (socket) { + socket.on("load", function (query) { + socket.emit("installed-results", {results: plugins.plugins}); + }); + + socket.on("search", function (query) { + socket.emit("progress", {progress:0, message:'Fetching results...'}); + installer.search(query, function (progress) { + if (progress.results) + socket.emit("search-result", progress); + socket.emit("progress", progress); + }); + }); + + socket.on("install", function (plugin_name) { + socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."}); + installer.install(plugin_name, function (progress) { + socket.emit("progress", progress); + }); + }); + + socket.on("uninstall", function (plugin_name) { + socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."}); + installer.uninstall(plugin_name, function (progress) { + socket.emit("progress", progress); + }); + }); + }); +} diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js new file mode 100644 index 00000000..48d50722 --- /dev/null +++ b/src/node/hooks/express/apicalls.js @@ -0,0 +1,60 @@ +var log4js = require('log4js'); +var apiLogger = log4js.getLogger("API"); +var formidable = require('formidable'); +var apiHandler = require('../../handler/APIHandler'); + +//This is for making an api call, collecting all post information and passing it to the apiHandler +var apiCaller = function(req, res, fields) { + res.header("Content-Type", "application/json; charset=utf-8"); + + apiLogger.info("REQUEST, " + req.params.func + ", " + JSON.stringify(fields)); + + //wrap the send function so we can log the response + //note: res._send seems to be already in use, so better use a "unique" name + res._____send = res.send; + res.send = function (response) { + response = JSON.stringify(response); + apiLogger.info("RESPONSE, " + req.params.func + ", " + response); + + //is this a jsonp call, if yes, add the function call + if(req.query.jsonp) + response = req.query.jsonp + "(" + response + ")"; + + res._____send(response); + } + + //call the api handler + apiHandler.handle(req.params.func, fields, req, res); +} + +exports.apiCaller = apiCaller; + +exports.expressCreateServer = function (hook_name, args, cb) { + //This is a api GET call, collect all post informations and pass it to the apiHandler + args.app.get('/api/1/:func', function (req, res) { + apiCaller(req, res, req.query) + }); + + //This is a api POST call, collect all post informations and pass it to the apiHandler + args.app.post('/api/1/:func', function(req, res) { + new formidable.IncomingForm().parse(req, function (err, fields, files) { + apiCaller(req, res, fields) + }); + }); + + //The Etherpad client side sends information about how a disconnect happen + args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) { + new formidable.IncomingForm().parse(req, function(err, fields, files) { + console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo); + res.end("OK"); + }); + }); + + //The Etherpad client side sends information about client side javscript errors + args.app.post('/jserror', function(req, res) { + new formidable.IncomingForm().parse(req, function(err, fields, files) { + console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo); + res.end("OK"); + }); + }); +} diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js new file mode 100644 index 00000000..cb8c5898 --- /dev/null +++ b/src/node/hooks/express/errorhandling.js @@ -0,0 +1,52 @@ +var os = require("os"); +var db = require('../../db/DB'); + + +exports.onShutdown = false; +exports.gracefulShutdown = function(err) { + if(err && err.stack) { + console.error(err.stack); + } else if(err) { + console.error(err); + } + + //ensure there is only one graceful shutdown running + if(exports.onShutdown) return; + exports.onShutdown = true; + + console.log("graceful shutdown..."); + + //stop the http server + exports.app.close(); + + //do the db shutdown + db.db.doShutdown(function() { + console.log("db sucessfully closed."); + + process.exit(0); + }); + + setTimeout(function(){ + process.exit(1); + }, 3000); +} + + +exports.expressCreateServer = function (hook_name, args, cb) { + exports.app = args.app; + + args.app.error(function(err, req, res, next){ + res.send(500); + console.error(err.stack ? err.stack : err.toString()); + exports.gracefulShutdown(); + }); + + //connect graceful shutdown with sigint and uncaughtexception + if(os.type().indexOf("Windows") == -1) { + //sigint is so far not working on windows + //https://github.com/joyent/node/issues/1553 + process.on('SIGINT', exports.gracefulShutdown); + } + + process.on('uncaughtException', exports.gracefulShutdown); +} diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js new file mode 100644 index 00000000..9e78f34d --- /dev/null +++ b/src/node/hooks/express/importexport.js @@ -0,0 +1,41 @@ +var hasPadAccess = require("../../padaccess"); +var settings = require('../../utils/Settings'); +var exportHandler = require('../../handler/ExportHandler'); +var importHandler = require('../../handler/ImportHandler'); + +exports.expressCreateServer = function (hook_name, args, cb) { + args.app.get('/p/:pad/:rev?/export/:type', function(req, res, next) { + var types = ["pdf", "doc", "txt", "html", "odt", "dokuwiki"]; + //send a 404 if we don't support this filetype + if (types.indexOf(req.params.type) == -1) { + next(); + return; + } + + //if abiword is disabled, and this is a format we only support with abiword, output a message + if (settings.abiword == null && + ["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) { + res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature"); + return; + } + + res.header("Access-Control-Allow-Origin", "*"); + + hasPadAccess(req, res, function() { + exportHandler.doExport(req, res, req.params.pad, req.params.type); + }); + }); + + //handle import requests + args.app.post('/p/:pad/import', function(req, res, next) { + //if abiword is disabled, skip handling this request + if(settings.abiword == null) { + next(); + return; + } + + hasPadAccess(req, res, function() { + importHandler.doImport(req, res, req.params.pad); + }); + }); +} diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js new file mode 100644 index 00000000..60ece0ad --- /dev/null +++ b/src/node/hooks/express/padreadonly.js @@ -0,0 +1,65 @@ +var async = require('async'); +var ERR = require("async-stacktrace"); +var readOnlyManager = require("../../db/ReadOnlyManager"); +var hasPadAccess = require("../../padaccess"); +var exporthtml = require("../../utils/ExportHtml"); + +exports.expressCreateServer = function (hook_name, args, cb) { + //serve read only pad + args.app.get('/ro/:id', function(req, res) + { + var html; + var padId; + var pad; + + async.series([ + //translate the read only pad to a padId + function(callback) + { + readOnlyManager.getPadId(req.params.id, function(err, _padId) + { + if(ERR(err, callback)) return; + + padId = _padId; + + //we need that to tell hasPadAcess about the pad + req.params.pad = padId; + + callback(); + }); + }, + //render the html document + function(callback) + { + //return if the there is no padId + if(padId == null) + { + callback("notfound"); + return; + } + + hasPadAccess(req, res, function() + { + //render the html document + exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html) + { + if(ERR(err, callback)) return; + html = _html; + callback(); + }); + }); + } + ], function(err) + { + //throw any unexpected error + if(err && err != "notfound") + ERR(err); + + if(err == "notfound") + res.send('404 - Not Found', 404); + else + res.send(html); + }); + }); + +}
\ No newline at end of file diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js new file mode 100644 index 00000000..4f5dd7a5 --- /dev/null +++ b/src/node/hooks/express/padurlsanitize.js @@ -0,0 +1,29 @@ +var padManager = require('../../db/PadManager'); + +exports.expressCreateServer = function (hook_name, args, cb) { + //redirects browser to the pad's sanitized url if needed. otherwise, renders the html + args.app.param('pad', function (req, res, next, padId) { + //ensure the padname is valid and the url doesn't end with a / + if(!padManager.isValidPadId(padId) || /\/$/.test(req.url)) + { + res.send('Such a padname is forbidden', 404); + } + else + { + padManager.sanitizePadId(padId, function(sanitizedPadId) { + //the pad id was sanitized, so we redirect to the sanitized version + if(sanitizedPadId != padId) + { + var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId); + res.header('Location', real_path); + res.send('You should be redirected to <a href="' + real_path + '">' + real_path + '</a>', 302); + } + //the pad id was fine, so just render it + else + { + next(); + } + }); + } + }); +} diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js new file mode 100644 index 00000000..e040f7ac --- /dev/null +++ b/src/node/hooks/express/socketio.js @@ -0,0 +1,49 @@ +var log4js = require('log4js'); +var socketio = require('socket.io'); +var settings = require('../../utils/Settings'); +var socketIORouter = require("../../handler/SocketIORouter"); +var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); + +var padMessageHandler = require("../../handler/PadMessageHandler"); +var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler"); + + +exports.expressCreateServer = function (hook_name, args, cb) { + //init socket.io and redirect all requests to the MessageHandler + var io = socketio.listen(args.app); + + //this is only a workaround to ensure it works with all browers behind a proxy + //we should remove this when the new socket.io version is more stable + io.set('transports', ['xhr-polling']); + + var socketIOLogger = log4js.getLogger("socket.io"); + io.set('logger', { + debug: function (str) + { + socketIOLogger.debug.apply(socketIOLogger, arguments); + }, + info: function (str) + { + socketIOLogger.info.apply(socketIOLogger, arguments); + }, + warn: function (str) + { + socketIOLogger.warn.apply(socketIOLogger, arguments); + }, + error: function (str) + { + socketIOLogger.error.apply(socketIOLogger, arguments); + }, + }); + + //minify socket.io javascript + if(settings.minify) + io.enable('browser client minification'); + + //Initalize the Socket.IO Router + socketIORouter.setSocketIO(io); + socketIORouter.addComponent("pad", padMessageHandler); + socketIORouter.addComponent("timeslider", timesliderMessageHandler); + + hooks.callAll("socketio", {"app": args.app, "io": io}); +} diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js new file mode 100644 index 00000000..474f475e --- /dev/null +++ b/src/node/hooks/express/specialpages.js @@ -0,0 +1,46 @@ +var path = require('path'); +var eejs = require('ep_etherpad-lite/node/eejs'); + +exports.expressCreateServer = function (hook_name, args, cb) { + + //serve index.html under / + args.app.get('/', function(req, res) + { + res.send(eejs.require("ep_etherpad-lite/templates/index.html")); + }); + + //serve robots.txt + args.app.get('/robots.txt', function(req, res) + { + var filePath = path.normalize(__dirname + "/../../../static/robots.txt"); + res.sendfile(filePath); + }); + + //serve favicon.ico + args.app.get('/favicon.ico', function(req, res) + { + var filePath = path.normalize(__dirname + "/../../../static/custom/favicon.ico"); + res.sendfile(filePath, function(err) + { + //there is no custom favicon, send the default favicon + if(err) + { + filePath = path.normalize(__dirname + "/../../../static/favicon.ico"); + res.sendfile(filePath); + } + }); + }); + + //serve pad.html under /p + args.app.get('/p/:pad', function(req, res, next) + { + res.send(eejs.require("ep_etherpad-lite/templates/pad.html")); + }); + + //serve timeslider.html under /p/$padname/timeslider + args.app.get('/p/:pad/timeslider', function(req, res, next) + { + res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html")); + }); + +}
\ No newline at end of file diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.js new file mode 100644 index 00000000..f284e478 --- /dev/null +++ b/src/node/hooks/express/static.js @@ -0,0 +1,57 @@ +var path = require('path'); +var minify = require('../../utils/Minify'); +var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +var CachingMiddleware = require('../../utils/caching_middleware'); +var settings = require("../../utils/Settings"); +var Yajsml = require('yajsml'); +var fs = require("fs"); +var ERR = require("async-stacktrace"); +var _ = require("underscore"); + +exports.expressCreateServer = function (hook_name, args, cb) { + // Cache both minified and static. + var assetCache = new CachingMiddleware; + args.app.all('/(javascripts|static)/*', assetCache.handle); + + // Minify will serve static files compressed (minify enabled). It also has + // file-specific hacks for ace/require-kernel/etc. + args.app.all('/static/:filename(*)', minify.minify); + + // Setup middleware that will package JavaScript files served by minify for + // CommonJS loader on the client-side. + var jsServer = new (Yajsml.Server)({ + rootPath: 'javascripts/src/' + , rootURI: 'http://localhost:' + settings.port + '/static/js/' + , libraryPath: 'javascripts/lib/' + , libraryURI: 'http://localhost:' + settings.port + '/static/plugins/' + }); + + var StaticAssociator = Yajsml.associators.StaticAssociator; + var associations = + Yajsml.associators.associationsForSimpleMapping(minify.tar); + var associator = new StaticAssociator(associations); + jsServer.setAssociator(associator); + args.app.use(jsServer); + + // serve plugin definitions + // not very static, but served here so that client can do require("pluginfw/static/js/plugin-definitions.js"); + args.app.get('/pluginfw/plugin-definitions.json', function (req, res, next) { + + var clientParts = _(plugins.parts) + .filter(function(part){ return _(part).has('client_hooks') }); + + var clientPlugins = {}; + + _(clientParts).chain() + .map(function(part){ return part.plugin }) + .uniq() + .each(function(name){ + clientPlugins[name] = _(plugins.plugins[name]).clone(); + delete clientPlugins[name]['package']; + }); + + res.header("Content-Type","application/json; charset=utf-8"); + res.write(JSON.stringify({"plugins": clientPlugins, "parts": clientParts})); + res.end(); + }); +} diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js new file mode 100644 index 00000000..d0e28737 --- /dev/null +++ b/src/node/hooks/express/webaccess.js @@ -0,0 +1,51 @@ +var express = require('express'); +var log4js = require('log4js'); +var httpLogger = log4js.getLogger("http"); +var settings = require('../../utils/Settings'); + + +//checks for basic http auth +exports.basicAuth = function (req, res, next) { + + // When handling HTTP-Auth, an undefined password will lead to no authorization at all + var pass = settings.httpAuth || ''; + + if (req.path.indexOf('/admin') == 0) { + var pass = settings.adminHttpAuth; + + } + + // Just pass if password is an empty string + if (pass === '') { + return next(); + } + + + // If a password has been set and auth headers are present... + if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + // ...check login and password + if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) { + return next(); + } + } + + // Otherwise return Auth required Headers, delayed for 1 second, if auth failed. + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + if (req.headers.authorization) { + setTimeout(function () { + res.send('Authentication required', 401); + }, 1000); + } else { + res.send('Authentication required', 401); + } +} + +exports.expressConfigure = function (hook_name, args, cb) { + args.app.use(exports.basicAuth); + + // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. + // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. + if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR")) + args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'})); + args.app.use(express.cookieParser()); +} |