diff --git a/settings.json.template b/settings.json.template
index 7aaa5d7ed..f89fcd8ed 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 90c69e595..2d02a45a6 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 a6cf8f4d8..5556efa1e 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 fa7e70771..7b21206c9 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 e040f7aca..6774b653a 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 d0e287373..028d8ab1b 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 6b443edb7..9d2c52e44 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 12fcc55c5..cb6a64033 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 83441da08..ac56bc358 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 fe40b6282..89da6941f 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 2ce8dbb66..601200ab5 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 6ea2938b9..85801d33e 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
-
-
Search for plugins to install
-
-
+
+
+
+
Search for plugins to install
+
+
+
+
..
of
.
+
+
@@ -167,4 +84,4 @@
-