diff --git a/settings.json.template b/settings.json.template index 7aaa5d7e..f89fcd8e 100644 --- a/settings.json.template +++ b/settings.json.template @@ -40,22 +40,35 @@ "minify" : true, /* How long may clients use served javascript code (in seconds)? Without versioning this - is may cause problems during deployment. Set to 0 to disable caching */ - "maxAge" : 21600, // 6 hours + may cause problems during deployment. Set to 0 to disable caching */ + "maxAge" : 21600, // 60 * 60 * 6 = 6 hours /* This is the path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to enable the import/export of pads*/ "abiword" : null, - - /* This setting is used if you need http basic auth */ - // "httpAuth" : "user:pass", + + /* This setting is used if you require authentication of all users. + Note: /admin always requires authentication. */ + "requireAuthentication": false, - /* This setting is used for http basic auth for admin pages. If not set, the admin page won't be accessible from web*/ - // "adminHttpAuth" : "user:pass", + /* Require authorization by a module, or a user with is_admin set, see below. */ + "requireAuthorization": false, + + /* Users for basic authentication. is_admin = true gives access to /admin. + If you do not uncomment this, /admin will not be available! */ + /* + "users": { + "admin": { + "password": "changeme1", + "is_admin": true + }, + "user": { + "password": "changeme1", + "is_admin": false + } + }, + */ /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ - "loglevel": "INFO", - - /* cache 6 hours = 1000*60*60*6 */ - "maxAge": 21600000 + "loglevel": "INFO" } diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js index 90c69e59..2d02a45a 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.js @@ -23,6 +23,7 @@ var ejs = require("ejs"); var fs = require("fs"); var path = require("path"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); +var resolve = require("resolve"); exports.info = { buf_stack: [], @@ -91,13 +92,28 @@ exports.inherit = function (name, args) { exports.info.file_stack[exports.info.file_stack.length-1].inherit.push({name:name, args:args}); } -exports.require = function (name, args) { +exports.require = function (name, args, mod) { if (args == undefined) args = {}; - - if ((name.indexOf("./") == 0 || name.indexOf("../") == 0) && exports.info.file_stack.length) { - name = path.join(path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path), name); + + var basedir = __dirname; + var paths = []; + + if (exports.info.file_stack.length) { + basedir = path.dirname(exports.info.file_stack[exports.info.file_stack.length-1].path); } - var ejspath = require.resolve(name) + if (mod) { + basedir = path.dirname(mod.filename); + paths = mod.paths; + } + + var ejspath = resolve.sync( + name, + { + paths : paths, + basedir : basedir, + extensions : [ '.html', '.ejs' ], + } + ) args.e = exports; args.require = require; diff --git a/src/node/handler/TimesliderMessageHandler.js b/src/node/handler/TimesliderMessageHandler.js index a6cf8f4d..5556efa1 100644 --- a/src/node/handler/TimesliderMessageHandler.js +++ b/src/node/handler/TimesliderMessageHandler.js @@ -155,8 +155,6 @@ function createTimesliderClientVars (padId, callback) var clientVars = { viewId: padId, colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"], - sliderEnabled : true, - supportsSlider: true, savedRevisions: [], padIdForUrl: padId, fullWidth: false, diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index fa7e7077..7b21206c 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -21,13 +21,15 @@ exports.expressCreateServer = function (hook_name, args, cb) { exports.socketio = function (hook_name, args, cb) { var io = args.io.of("/pluginfw/installer"); io.on('connection', function (socket) { + if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return; + 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) { + installer.search(query, true, function (progress) { if (progress.results) socket.emit("search-result", progress); socket.emit("progress", progress); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index e040f7ac..6774b653 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -7,11 +7,27 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var padMessageHandler = require("../../handler/PadMessageHandler"); var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler"); - +var connect = require('connect'); + exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler var io = socketio.listen(args.app); + /* Require an express session cookie to be present, and load the + * session. See http://www.danielbaulig.de/socket-ioexpress for more + * info */ + io.set('authorization', function (data, accept) { + if (!data.headers.cookie) return accept('No session cookie transmitted.', false); + data.cookie = connect.utils.parseCookie(data.headers.cookie); + data.sessionID = data.cookie.express_sid; + args.app.sessionStore.get(data.sessionID, function (err, session) { + if (err || !session) return accept('Bad session / session has expired', false); + data.session = new connect.middleware.session.Session(data, session); + accept(null, true); + }); + }); + + //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']); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index d0e28737..028d8ab1 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -2,50 +2,108 @@ var express = require('express'); var log4js = require('log4js'); var httpLogger = log4js.getLogger("http"); var settings = require('../../utils/Settings'); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); //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(); + var hookResultMangle = function (cb) { + return function (err, data) { + return cb(!err && data.length && data[0]); } } - // 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); + var authorize = function (cb) { + // Do not require auth for static paths...this could be a bit brittle + if (req.path.match(/^\/(static|javascripts|pluginfw)/)) return cb(true); + + if (req.path.indexOf('/admin') != 0) { + if (!settings.requireAuthentication) return cb(true); + if (!settings.requireAuthorization && req.session && req.session.user) return cb(true); + } + + if (req.session && req.session.user && req.session.user.is_admin) return cb(true); + + hooks.aCallFirst("authorize", {req: req, res:res, next:next, resource: req.path}, hookResultMangle(cb)); } + + var authenticate = function (cb) { + // If auth headers are present use them to authenticate... + if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + var userpass = new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString().split(":") + var username = userpass[0]; + var password = userpass[1]; + + if (settings.users[username] != undefined && settings.users[username].password == password) { + settings.users[username].username = username; + req.session.user = settings.users[username]; + return cb(true); + } + return hooks.aCallFirst("authenticate", {req: req, res:res, next:next, username: username, password: password}, hookResultMangle(cb)); + } + hooks.aCallFirst("authenticate", {req: req, res:res, next:next}, hookResultMangle(cb)); + } + + + /* Authentication OR authorization failed. */ + var failure = function () { + return hooks.aCallFirst("authFailure", {req: req, res:res, next:next}, hookResultMangle(function (ok) { + if (ok) return; + /* No plugin handler for invalid auth. Return Auth required + * Headers, delayed for 1 second, if authentication failed + * before. */ + 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); + } + })); + } + + + /* This is the actual authentication/authorization hoop. It is done in four steps: + + 1) Try to just access the thing + 2) If not allowed using whatever creds are in the current session already, try to authenticate + 3) If authentication using already supplied credentials succeeds, try to access the thing again + 4) If all els fails, give the user a 401 to request new credentials + + Note that the process could stop already in step 3 with a redirect to login page. + + */ + + authorize(function (ok) { + if (ok) return next(); + authenticate(function (ok) { + if (!ok) return failure(); + authorize(function (ok) { + if (ok) return next(); + failure(); + }); + }); + }); } 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()); + + /* Do not let express create the session, so that we can retain a + * reference to it for socket.io to use. Also, set the key (cookie + * name) to a javascript identifier compatible string. Makes code + * handling it cleaner :) */ + + args.app.sessionStore = new express.session.MemoryStore(); + args.app.use(express.session({store: args.app.sessionStore, + key: 'express_sid', + secret: apikey = randomString(32)})); + + args.app.use(exports.basicAuth); } diff --git a/src/node/server.js b/src/node/server.js index 6b443edb..9d2c52e4 100644 --- a/src/node/server.js +++ b/src/node/server.js @@ -30,6 +30,7 @@ var path = require('path'); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm/lib/npm.js"); +var _ = require("underscore"); //try to get the git version var version = ""; @@ -88,11 +89,11 @@ async.waterfall([ //let the server listen app.listen(settings.port, settings.ip); console.log("Server is listening at " + settings.ip + ":" + settings.port); - if(settings.adminHttpAuth){ + if(!_.isEmpty(settings.users)){ console.log("Plugin admin page listening at " + settings.ip + ":" + settings.port + "/admin/plugins"); } else{ - console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit adminHttpAuth in settings.json"); + console.log("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); } callback(null); } diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 12fcc55c..cb6a6403 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -80,15 +80,12 @@ exports.abiword = null; */ exports.loglevel = "INFO"; -/** - * Http basic auth, with "user:password" format - */ -exports.httpAuth = null; - -/** - * Http basic auth, with "user:password" format - */ -exports.adminHttpAuth = null; +/* This setting is used if you need authentication and/or + * authorization. Note: /admin always requires authentication, and + * either authorization by a module, or a user with is_admin set */ +exports.requireAuthentication = false; +exports.requireAuthorization = false; +exports.users = {}; //checks if abiword is avaiable exports.abiwordAvailable = function() diff --git a/src/package.json b/src/package.json index 83441da0..ac56bc35 100644 --- a/src/package.json +++ b/src/package.json @@ -13,10 +13,12 @@ "yajsml" : "1.1.2", "request" : "2.9.100", "require-kernel" : "1.0.5", + "resolve" : "0.2.1", "socket.io" : "0.8.7", "ueberDB" : "0.1.7", "async" : "0.1.18", "express" : "2.5.8", + "connect" : "1.8.7", "clean-css" : "0.3.2", "uglify-js" : "1.2.5", "formidable" : "1.0.9", diff --git a/src/static/css/admin.css b/src/static/css/admin.css index fe40b628..89da6941 100644 --- a/src/static/css/admin.css +++ b/src/static/css/admin.css @@ -1,6 +1,5 @@ body { margin: 0; - height: 100%; color: #333; font: 14px helvetica, sans-serif; background: #ddd; @@ -42,34 +41,33 @@ form { width: 300px; margin: 0 auto; } -button, input { +input { font-weight: bold; font-size: 15px; } input[type="button"] { - height: 30px; + padding: 4px 6px; margin: 0; - display: block; } -input[value="Uninstall"], input[value="Install"] { +input[type="button"].do-install, input[type="button"].do-uninstall { float: right; width: 100px; } +input[type="button"]#do-search { + display: block; +} input[type="text"] { border-radius: 3px; box-sizing: border-box; - -moz-box-sizing: border-box; + -moz-box-sizing: border-box; padding: 10px; *padding: 0; /* IE7 hack */ width: 100%; outline: none; border: 1px solid #ddd; - margin: 0 0 5px 1px; + margin: 0 0 5px 0; max-width: 500px; } -button{ - display:block; -} table { border: 1px solid #ddd; border-radius: 3px; @@ -95,13 +93,13 @@ td, th { height: 500px; margin-left: -350px; margin-top: -250px; - border: 3px solid #999999; - background: #eeeeee; + border: 3px solid #999; + background: #eee; } .dialog .title { margin: 0; padding: 2px; - border-bottom: 3px solid #999999; + border-bottom: 3px solid #999; font-size: 24px; line-height: 24px; height: 24px; @@ -109,10 +107,11 @@ td, th { } .dialog .title .close { float: right; + padding: 1px 10px; } .dialog .history { - background: #222222; - color: #eeeeee; + background: #222; + color: #eee; position: absolute; top: 41px; bottom: 10px; diff --git a/src/static/css/pad.css b/src/static/css/pad.css index 2ce8dbb6..601200ab 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -1402,7 +1402,7 @@ input[type=checkbox] { float: left; width: 50%; } -#settingsmenu, +#settings, #importexport, #embed { position: absolute; @@ -1546,7 +1546,7 @@ input[type=checkbox] { box-sizing: border-box; width: 100%; } - #settingsmenu, + #settings, #importexport, #embed { left: 0; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 6ea2938b..85801d33 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -167,7 +167,13 @@ require.setGlobalKeyPath("require");\n\ buffer.push(Ace2Editor.EMBEDED[KERNEL_SOURCE]); buffer.push(KERNEL_BOOT); buffer.push('<\/script>'); - } + } else { + file = KERNEL_SOURCE; + buffer.push(' - - + + + + +
-

Etherpad Lite

- -
- <% if (errors.length) { %>
<% errors.forEach(function (item) { %> @@ -110,6 +19,8 @@ <% } %> +

Etherpad Lite

+

Installed plugins

@@ -131,32 +42,38 @@
-
-

Search for plugins to install

-
- - -
- - - - - - - - - - - - - - - - - -
NameDescription
- -
+ +
+
+

Search for plugins to install

+
+ + +
+ + + + + + + + + + + + + + + + + +
NameDescription
+ +
+ + .. of . + +

@@ -167,4 +84,4 @@

- \ No newline at end of file + diff --git a/src/templates/pad.html b/src/templates/pad.html index b9c77ea4..dea5a5bc 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -11,9 +11,9 @@ <% e.begin_block("styles"); %> - - - + + + <% e.end_block(); %> @@ -21,70 +21,68 @@ <% e.begin_block("body"); %>
+ <% e.begin_block("editbarMenuLeft"); %> +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • +
  • + +
  • +
  • + +
  • +
  • +
  • + +
  • + <% e.end_block(); %> +
    @@ -96,17 +94,17 @@ -
    +
    -
    +
    -
    +
    @@ -116,7 +114,7 @@
    Loading...
    -