summaryrefslogtreecommitdiff
path: root/src/node/hooks/express
diff options
context:
space:
mode:
Diffstat (limited to 'src/node/hooks/express')
-rw-r--r--src/node/hooks/express/adminplugins.js51
-rw-r--r--src/node/hooks/express/apicalls.js60
-rw-r--r--src/node/hooks/express/errorhandling.js52
-rw-r--r--src/node/hooks/express/importexport.js41
-rw-r--r--src/node/hooks/express/padreadonly.js65
-rw-r--r--src/node/hooks/express/padurlsanitize.js29
-rw-r--r--src/node/hooks/express/socketio.js49
-rw-r--r--src/node/hooks/express/specialpages.js46
-rw-r--r--src/node/hooks/express/static.js57
-rw-r--r--src/node/hooks/express/webaccess.js51
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());
+}