db/SecurityManager.js: converted checkAccess() to pure Promises

Also converted the handler functions that depend on checkAccess() into async
functions too.

NB: this commit needs specific attention to it because it touches a lot of
security related code!
This commit is contained in:
Ray Bellis 2019-01-28 13:13:24 +00:00
parent 7709fd46e5
commit e58da69cfb
3 changed files with 508 additions and 687 deletions

View file

@ -18,8 +18,6 @@
* limitations under the License. * limitations under the License.
*/ */
var ERR = require("async-stacktrace");
var async = require("async");
var authorManager = require("./AuthorManager"); var authorManager = require("./AuthorManager");
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
var padManager = require("./PadManager"); var padManager = require("./PadManager");
@ -35,168 +33,104 @@ const thenify = require("thenify").withCallback;
* @param sessionCookie the session the user has (set via api) * @param sessionCookie the session the user has (set via api)
* @param token the token of the author (randomly generated at client side, used for public pads) * @param token the token of the author (randomly generated at client side, used for public pads)
* @param password the password the user has given to access this pad, can be null * @param password the password the user has given to access this pad, can be null
* @param callback will be called with (err, {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}) * @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx})
*/ */
exports.checkAccess = thenify(function(padID, sessionCookie, token, password, callback) exports.checkAccess = async function(padID, sessionCookie, token, password)
{ {
var statusObject; // immutable object
let deny = Object.freeze({ accessStatus: "deny" });
if (!padID) { if (!padID) {
callback(null, {accessStatus: "deny"}); return deny;
return;
} }
// allow plugins to deny access // allow plugins to deny access
var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1; var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1;
if (deniedByHook) { if (deniedByHook) {
callback(null, {accessStatus: "deny"}); return deny;
return;
} }
// get author for this token
let tokenAuthor = await authorManager.getAuthor4Token(token);
// check if pad exists
let padExists = await padManager.doesPadExist(padID);
if (settings.requireSession) { if (settings.requireSession) {
// a valid session is required (api-only mode) // a valid session is required (api-only mode)
if (!sessionCookie) { if (!sessionCookie) {
// without sessionCookie, access is denied // without sessionCookie, access is denied
callback(null, {accessStatus: "deny"}); return deny;
return;
} }
} else { } else {
// a session is not required, so we'll check if it's a public pad // a session is not required, so we'll check if it's a public pad
if (padID.indexOf("$") === -1) { if (padID.indexOf("$") === -1) {
// it's not a group pad, means we can grant access // it's not a group pad, means we can grant access
// get author for this token
authorManager.getAuthor4Token(token, function(err, author) {
if (ERR(err, callback)) return;
// assume user has access // assume user has access
statusObject = { accessStatus: "grant", authorID: author }; let statusObject = { accessStatus: "grant", authorID: tokenAuthor };
if (settings.editOnly) { if (settings.editOnly) {
// user can't create pads // user can't create pads
// check if pad exists if (!padExists) {
padManager.doesPadExists(padID, function(err, exists) {
if (ERR(err, callback)) return;
if (!exists) {
// pad doesn't exist - user can't have access // pad doesn't exist - user can't have access
statusObject.accessStatus = "deny"; statusObject.accessStatus = "deny";
} }
// grant or deny access, with author of token
callback(null, statusObject);
});
return;
} }
// user may create new pads - no need to check anything // user may create new pads - no need to check anything
// grant access, with author of token // grant access, with author of token
callback(null, statusObject); return statusObject;
});
// don't continue
return;
} }
} }
var groupID = padID.split("$")[0]; let validSession = false;
var padExists = false; let sessionAuthor;
var validSession = false; let isPublic;
var sessionAuthor; let isPasswordProtected;
var tokenAuthor; let passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
var isPublic;
var isPasswordProtected;
var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong
async.series([
// get basic informations from the database
function(callback) {
async.parallel([
// does pad exist
function(callback) {
padManager.doesPadExists(padID, function(err, exists) {
if (ERR(err, callback)) return;
padExists = exists;
callback();
});
},
// get information about all sessions contained in this cookie // get information about all sessions contained in this cookie
function(callback) { if (sessionCookie) {
if (!sessionCookie) { let groupID = padID.split("$")[0];
callback(); let sessionIDs = sessionCookie.split(',');
return;
}
var sessionIDs = sessionCookie.split(','); // was previously iterated in parallel using async.forEach
for (let sessionID of sessionIDs) {
async.forEach(sessionIDs, function(sessionID, callback) { try {
sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { let sessionInfo = await sessionManager.getSessionInfo(sessionID);
// skip session if it doesn't exist
if (err && err.message == "sessionID does not exist") {
authLogger.debug("Auth failed: unknown session");
callback();
return;
}
if (ERR(err, callback)) return;
var now = Math.floor(Date.now()/1000);
// is it for this group? // is it for this group?
if (sessionInfo.groupID != groupID) { if (sessionInfo.groupID != groupID) {
authLogger.debug("Auth failed: wrong group"); authLogger.debug("Auth failed: wrong group");
callback(); continue;
return;
} }
// is validUntil still ok? // is validUntil still ok?
let now = Math.floor(Date.now() / 1000);
if (sessionInfo.validUntil <= now) { if (sessionInfo.validUntil <= now) {
authLogger.debug("Auth failed: validUntil"); authLogger.debug("Auth failed: validUntil");
callback(); continue;
return;
} }
// There is a valid session // fall-through - there is a valid session
validSession = true; validSession = true;
sessionAuthor = sessionInfo.authorID; sessionAuthor = sessionInfo.authorID;
break;
callback(); } catch (err) {
}); // skip session if it doesn't exist
}, callback); if (err.message == "sessionID does not exist") {
}, authLogger.debug("Auth failed: unknown session");
} else {
// get author for token throw err;
function(callback) { }
// get author for this token }
authorManager.getAuthor4Token(token, function(err, author) {
if (ERR(err, callback)) return;
tokenAuthor = author;
callback();
});
} }
], callback);
},
// get more informations of this pad, if avaiable
function(callback) {
// skip this if the pad doesn't exist
if (padExists == false) {
callback();
return;
} }
padManager.getPad(padID, function(err, pad) { if (padExists) {
if (ERR(err, callback)) return; let pad = await padManager.getPad(padID);
// is it a public pad? // is it a public pad?
isPublic = pad.getPublicStatus(); isPublic = pad.getPublicStatus();
@ -208,97 +142,113 @@ exports.checkAccess = thenify(function(padID, sessionCookie, token, password, ca
if (isPasswordProtected && password && pad.isCorrectPassword(password)) { if (isPasswordProtected && password && pad.isCorrectPassword(password)) {
passwordStatus = "correct"; passwordStatus = "correct";
} }
}
callback();
});
},
function(callback) {
if (validSession && padExists) {
// - a valid session for this group is avaible AND pad exists // - a valid session for this group is avaible AND pad exists
if (validSession && padExists) {
let authorID = sessionAuthor;
let grant = Object.freeze({ accessStatus: "grant", authorID });
if (!isPasswordProtected) { if (!isPasswordProtected) {
// - the pad is not password protected // - the pad is not password protected
// --> grant access // --> grant access
statusObject = { accessStatus: "grant", authorID: sessionAuthor }; return grant;
} else if (settings.sessionNoPassword) { }
if (settings.sessionNoPassword) {
// - the setting to bypass password validation is set // - the setting to bypass password validation is set
// --> grant access // --> grant access
statusObject = { accessStatus: "grant", authorID: sessionAuthor }; return grant;
} else if (isPasswordProtected && passwordStatus === "correct") { }
if (isPasswordProtected && passwordStatus === "correct") {
// - the pad is password protected and password is correct // - the pad is password protected and password is correct
// --> grant access // --> grant access
statusObject = { accessStatus: "grant", authorID: sessionAuthor }; return grant;
} else if (isPasswordProtected && passwordStatus === "wrong") { }
if (isPasswordProtected && passwordStatus === "wrong") {
// - the pad is password protected but wrong password given // - the pad is password protected but wrong password given
// --> deny access, ask for new password and tell them that the password is wrong // --> deny access, ask for new password and tell them that the password is wrong
statusObject = { accessStatus: "wrongPassword" }; return { accessStatus: "wrongPassword" };
} else if (isPasswordProtected && passwordStatus === "notGiven") { }
if (isPasswordProtected && passwordStatus === "notGiven") {
// - the pad is password protected but no password given // - the pad is password protected but no password given
// --> ask for password // --> ask for password
statusObject = { accessStatus: "needPassword" }; return { accessStatus: "needPassword" };
} else {
throw new Error("Ops, something wrong happend");
} }
} else if (validSession && !padExists) {
throw new Error("Oops, something wrong happend");
}
if (validSession && !padExists) {
// - a valid session for this group avaible but pad doesn't exist // - a valid session for this group avaible but pad doesn't exist
// --> grant access // --> grant access by default
statusObject = {accessStatus: "grant", authorID: sessionAuthor}; let accessStatus = "grant";
let authorID = sessionAuthor;
if (settings.editOnly) {
// --> deny access if user isn't allowed to create the pad // --> deny access if user isn't allowed to create the pad
if (settings.editOnly) {
authLogger.debug("Auth failed: valid session & pad does not exist"); authLogger.debug("Auth failed: valid session & pad does not exist");
statusObject.accessStatus = "deny"; accessStatus = "deny";
} }
} else if (!validSession && padExists) {
return { accessStatus, authorID };
}
if (!validSession && padExists) {
// there is no valid session avaiable AND pad exists // there is no valid session avaiable AND pad exists
// -- it's public and not password protected let authorID = tokenAuthor;
let grant = Object.freeze({ accessStatus: "grant", authorID });
if (isPublic && !isPasswordProtected) { if (isPublic && !isPasswordProtected) {
// -- it's public and not password protected
// --> grant access, with author of token // --> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor}; return grant;
} else if (isPublic && isPasswordProtected && passwordStatus === "correct") { }
if (isPublic && isPasswordProtected && passwordStatus === "correct") {
// - it's public and password protected and password is correct // - it's public and password protected and password is correct
// --> grant access, with author of token // --> grant access, with author of token
statusObject = {accessStatus: "grant", authorID: tokenAuthor}; return grant;
} else if (isPublic && isPasswordProtected && passwordStatus === "wrong") { }
if (isPublic && isPasswordProtected && passwordStatus === "wrong") {
// - it's public and the pad is password protected but wrong password given // - it's public and the pad is password protected but wrong password given
// --> deny access, ask for new password and tell them that the password is wrong // --> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"}; return { accessStatus: "wrongPassword" };
} else if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { }
if (isPublic && isPasswordProtected && passwordStatus === "notGiven") {
// - it's public and the pad is password protected but no password given // - it's public and the pad is password protected but no password given
// --> ask for password // --> ask for password
statusObject = {accessStatus: "needPassword"}; return { accessStatus: "needPassword" };
} else if (!isPublic) { }
if (!isPublic) {
// - it's not public // - it's not public
authLogger.debug("Auth failed: invalid session & pad is not public"); authLogger.debug("Auth failed: invalid session & pad is not public");
// --> deny access // --> deny access
statusObject = {accessStatus: "deny"}; return { accessStatus: "deny" };
} else {
throw new Error("Ops, something wrong happend");
} }
} else {
throw new Error("Oops, something wrong happend");
}
// there is no valid session avaiable AND pad doesn't exist // there is no valid session avaiable AND pad doesn't exist
authLogger.debug("Auth failed: invalid session & pad does not exist"); authLogger.debug("Auth failed: invalid session & pad does not exist");
// --> deny access return { accessStatus: "deny" };
statusObject = {accessStatus: "deny"};
} }
callback();
}
],
function(err) {
if (ERR(err, callback)) return;
callback(null, statusObject);
});
});

View file

@ -164,7 +164,7 @@ exports.handleDisconnect = function(client)
* @param client the client that send this message * @param client the client that send this message
* @param message the message from the client * @param message the message from the client
*/ */
exports.handleMessage = function(client, message) exports.handleMessage = async function(client, message)
{ {
if (message == null) { if (message == null) {
return; return;
@ -181,35 +181,33 @@ exports.handleMessage = function(client, message)
return; return;
} }
var handleMessageHook = function(callback) { async function handleMessageHook() {
// Allow plugins to bypass the readonly message blocker // Allow plugins to bypass the readonly message blocker
hooks.aCallAll("handleMessageSecurity", { client: client, message: message }, function( err, messages ) { let messages = await hooks.aCallAll("handleMessageSecurity", { client: client, message: message });
if(ERR(err, callback)) return;
_.each(messages, function(newMessage){ for (let message of messages) {
if ( newMessage === true ) { if (message === true) {
thisSession.readonly = false; thisSession.readonly = false;
} break;
}); }
}); }
let dropMessage = false;
var dropMessage = false;
// Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages
// handleMessage will be called, even if the client is not authorized // handleMessage will be called, even if the client is not authorized
hooks.aCallAll("handleMessage", { client: client, message: message }, function( err, messages ) { messages = await hooks.aCallAll("handleMessage", { client: client, message: message });
if(ERR(err, callback)) return; for (let message of messages) {
_.each(messages, function(newMessage){ if (message === null ) {
if ( newMessage === null ) {
dropMessage = true; dropMessage = true;
break;
} }
});
// If no plugins explicitly told us to drop the message, its ok to proceed
if(!dropMessage){ callback() };
});
} }
var finalHandler = function() { return dropMessage;
}
function finalHandler() {
// Check what type of message we get and delegate to the other methods // Check what type of message we get and delegate to the other methods
if (message.type == "CLIENT_READY") { if (message.type == "CLIENT_READY") {
handleClientReady(client, message); handleClientReady(client, message);
@ -256,11 +254,11 @@ exports.handleMessage = function(client, message)
return; return;
} }
async.series([ let dropMessage = await handleMessageHook();
handleMessageHook, if (!dropMessage) {
// check permissions // check permissions
function(callback) {
// client tried to auth for the first time (first msg from the client) // client tried to auth for the first time (first msg from the client)
if (message.type == "CLIENT_READY") { if (message.type == "CLIENT_READY") {
createSessionInfo(client, message); createSessionInfo(client, message);
@ -278,32 +276,27 @@ exports.handleMessage = function(client, message)
return; return;
} }
var auth = sessioninfos[client.id].auth; let auth = sessioninfos[client.id].auth;
var checkAccessCallback = function(err, statusObject) {
if (ERR(err, callback)) return;
if (statusObject.accessStatus == "grant") {
// access was granted
callback();
} else {
// no access, send the client a message that tells him why
client.json.send({accessStatus: statusObject.accessStatus})
}
};
// check if pad is requested via readOnly // check if pad is requested via readOnly
if (auth.padID.indexOf("r.") === 0) { let padId = auth.padID;
// Pad is readOnly, first get the real Pad ID // Pad is readOnly, first get the real Pad ID
readOnlyManager.getPadId(auth.padID, function(err, value) { if (padId.indexOf("r.") === 0) {
ERR(err); padId = await readOnlyManager.getPadId(padID);
securityManager.checkAccess(value, auth.sessionID, auth.token, auth.password, checkAccessCallback); }
});
} else { let { accessStatus } = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password);
securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, auth.password, checkAccessCallback);
// no access, send the client a message that tells him why
if (accessStatus !== "grant") {
client.json.send({ accessStatus });
return;
}
// access was granted
finalHandler();
} }
},
finalHandler
]);
} }
@ -977,7 +970,7 @@ function createSessionInfo(client, message)
* @param client the client that send this message * @param client the client that send this message
* @param message the message from the client * @param message the message from the client
*/ */
function handleClientReady(client, message) async function handleClientReady(client, message)
{ {
// check if all ok // check if all ok
if (!message.token) { if (!message.token) {
@ -1000,131 +993,74 @@ function handleClientReady(client, message)
return; return;
} }
var author;
var authorName;
var authorColorId;
var pad;
var historicalAuthorData = {}; var historicalAuthorData = {};
var currentTime;
var padIds;
hooks.callAll("clientReady", message); hooks.callAll("clientReady", message);
async.series([
// Get ro/rw id:s // Get ro/rw id:s
function(callback) { let padIds = await readOnlyManager.getIds(message.padId);
readOnlyManager.getIds(message.padId, function(err, value) {
if (ERR(err, callback)) return;
padIds = value;
callback();
});
},
// check permissions // check permissions
function(callback) {
// Note: message.sessionID is an entierly different kind of // Note: message.sessionID is an entierly different kind of
// session from the sessions we use here! Beware! // session from the sessions we use here! Beware!
// FIXME: Call our "sessions" "connections". // FIXME: Call our "sessions" "connections".
// FIXME: Use a hook instead // FIXME: Use a hook instead
// FIXME: Allow to override readwrite access with readonly // FIXME: Allow to override readwrite access with readonly
securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) { let statusObject = await securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password);
if (ERR(err, callback)) return; let accessStatus = statusObject.accessStatus;
if (statusObject.accessStatus == "grant") {
// access was granted
author = statusObject.authorID;
callback();
} else {
// no access, send the client a message that tells him why // no access, send the client a message that tells him why
client.json.send({accessStatus: statusObject.accessStatus}) if (accessStatus !== "grant") {
client.json.send({ accessStatus });
return;
} }
});
}, let author = statusObject.authorID;
// get all authordata of this new user, and load the pad-object from the database // get all authordata of this new user, and load the pad-object from the database
function(callback) let value = await authorManager.getAuthor(author);
{ let authorColorId = value.colorId;
async.parallel([ let authorName = value.name;
// get colorId and name
function(callback) {
authorManager.getAuthor(author, function(err, value) {
if (ERR(err, callback)) return;
authorColorId = value.colorId;
authorName = value.name;
callback();
});
},
// get pad // get pad
function(callback) { let pad = await padManager.getPad(padIds.padId);
padManager.getPad(padIds.padId, function(err, value) {
if (ERR(err, callback)) return;
pad = value; // these db requests all need the pad object (timestamp of latest revision, author data)
callback(); let authors = pad.getAllAuthors();
});
}
], callback);
},
// these db requests all need the pad object (timestamp of latest revission, author data) // get timestamp of latest revision needed for timeslider
function(callback) { let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber());
var authors = pad.getAllAuthors();
async.parallel([
// get timestamp of latest revission needed for timeslider
function(callback) {
pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) {
if (ERR(err, callback)) return;
currentTime = date;
callback();
});
},
// get all author data out of the database // get all author data out of the database
function(callback) { for (let authorId of authors) {
async.forEach(authors, function(authorId, callback) { try {
authorManager.getAuthor(authorId, function(err, author) { let author = await authorManager.getAuthor(authorId);
if (!author && !err) {
messageLogger.error("There is no author for authorId:", authorId);
return callback();
}
if (ERR(err, callback)) return;
historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients)
callback(); } catch (err) {
}); messageLogger.error("There is no author for authorId:", authorId);
}, callback); }
} }
], callback);
},
// glue the clientVars together, send them and tell the other clients that a new one is there // glue the clientVars together, send them and tell the other clients that a new one is there
function(callback) {
// Check that the client is still here. It might have disconnected between callbacks. // Check that the client is still here. It might have disconnected between callbacks.
if (sessioninfos[client.id] === undefined) { if (sessioninfos[client.id] === undefined) {
return callback(); return;
} }
// Check if this author is already on the pad, if yes, kick the other sessions! // Check if this author is already on the pad, if yes, kick the other sessions!
var roomClients = _getRoomClients(pad.id); let roomClients = _getRoomClients(pad.id);
async.forEach(roomClients, function(client, callback) {
var sinfo = sessioninfos[client.id];
for (let client of roomClients) {
let sinfo = sessioninfos[client.id];
if (sinfo && sinfo.author == author) { if (sinfo && sinfo.author == author) {
// fix user's counter, works on page refresh or if user closes browser window and then rejoins // fix user's counter, works on page refresh or if user closes browser window and then rejoins
sessioninfos[client.id] = {}; sessioninfos[client.id] = {};
client.leave(padIds.padId); client.leave(padIds.padId);
client.json.send({disconnect:"userdup"}); client.json.send({disconnect:"userdup"});
} }
}); }
// Save in sessioninfos that this session belonges to this pad // Save in sessioninfos that this session belonges to this pad
sessioninfos[client.id].padId = padIds.padId; sessioninfos[client.id].padId = padIds.padId;
@ -1132,7 +1068,7 @@ function handleClientReady(client, message)
sessioninfos[client.id].readonly = padIds.readonly; sessioninfos[client.id].readonly = padIds.readonly;
// Log creation/(re-)entering of a pad // Log creation/(re-)entering of a pad
var ip = remoteAddress[client.id]; let ip = remoteAddress[client.id];
// Anonymize the IP address if IP logging is disabled // Anonymize the IP address if IP logging is disabled
if (settings.disableIPlogging) { if (settings.disableIPlogging) {
@ -1145,7 +1081,7 @@ function handleClientReady(client, message)
accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad');
} }
if (message.reconnect == true) { if (message.reconnect) {
// If this is a reconnect, we don't have to send the client the ClientVars again // If this is a reconnect, we don't have to send the client the ClientVars again
// Join the pad and start receiving updates // Join the pad and start receiving updates
client.join(padIds.padId); client.join(padIds.padId);
@ -1161,9 +1097,6 @@ function handleClientReady(client, message)
var startNum = message.client_rev + 1; var startNum = message.client_rev + 1;
var endNum = pad.getHeadRevisionNumber() + 1; var endNum = pad.getHeadRevisionNumber() + 1;
async.series([
// push all the revision numbers needed into revisionsNeeded array
function(callback) {
var headNum = pad.getHeadRevisionNumber(); var headNum = pad.getHeadRevisionNumber();
if (endNum > headNum + 1) { if (endNum > headNum + 1) {
@ -1174,58 +1107,23 @@ function handleClientReady(client, message)
startNum = 0; startNum = 0;
} }
for (var r = startNum; r < endNum; r++) { for (let r = startNum; r < endNum; r++) {
revisionsNeeded.push(r); revisionsNeeded.push(r);
changesets[r] = {}; changesets[r] = {};
} }
callback(); // get changesets, author and timestamp needed for pending revisions
}, for (let revNum of revisionsNeeded) {
changesets[revNum]['changeset'] = await pad.getRevisionChangeset(revNum);
// get changesets needed for pending revisions changesets[revNum]['author'] = await pad.getRevisionAuthor(revNum);
function(callback) { changesets[revNum]['timestamp'] = await pad.getRevisionDate(revNum);
async.eachSeries(revisionsNeeded, function(revNum, callback) {
pad.getRevisionChangeset(revNum, function(err, value) {
if (ERR(err)) return;
changesets[revNum]['changeset'] = value;
callback();
});
}, callback);
},
// get author for each changeset
function(callback) {
async.eachSeries(revisionsNeeded, function(revNum, callback) {
pad.getRevisionAuthor(revNum, function(err, value) {
if (ERR(err)) return;
changesets[revNum]['author'] = value;
callback();
});
}, callback);
},
// get timestamp for each changeset
function(callback) {
async.eachSeries(revisionsNeeded, function(revNum, callback) {
pad.getRevisionDate(revNum, function(err, value) {
if (ERR(err)) return;
changesets[revNum]['timestamp'] = value;
callback();
});
}, callback);
} }
],
// return error and pending changesets // return pending changesets
function(err) { for (let r of revisionsNeeded) {
if (ERR(err, callback)) return;
async.eachSeries(revisionsNeeded, function(r, callback) { let forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool);
var forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool); let wireMsg = {"type":"COLLABROOM",
var wireMsg = {"type":"COLLABROOM",
"data":{type:"CLIENT_RECONNECT", "data":{type:"CLIENT_RECONNECT",
headRev:pad.getHeadRevisionNumber(), headRev:pad.getHeadRevisionNumber(),
newRev:r, newRev:r,
@ -1235,8 +1133,7 @@ function handleClientReady(client, message)
currentTime: changesets[r]['timestamp'] currentTime: changesets[r]['timestamp']
}}; }};
client.json.send(wireMsg); client.json.send(wireMsg);
callback(); }
});
if (startNum == endNum) { if (startNum == endNum) {
var Msg = {"type":"COLLABROOM", var Msg = {"type":"COLLABROOM",
@ -1246,9 +1143,10 @@ function handleClientReady(client, message)
}}; }};
client.json.send(Msg); client.json.send(Msg);
} }
});
} else { } else {
// This is a normal first connect // This is a normal first connect
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
try { try {
var atext = Changeset.cloneAText(pad.atext); var atext = Changeset.cloneAText(pad.atext);
@ -1258,7 +1156,8 @@ function handleClientReady(client, message)
} catch(e) { } catch(e) {
console.error(e.stack || e) console.error(e.stack || e)
client.json.send({ disconnect:"corruptPad" }); // pull the brakes client.json.send({ disconnect:"corruptPad" }); // pull the brakes
return callback();
return;
} }
// Warning: never ever send padIds.padId to the client. If the // Warning: never ever send padIds.padId to the client. If the
@ -1327,15 +1226,12 @@ function handleClientReady(client, message)
} }
// call the clientVars-hook so plugins can modify them before they get sent to the client // call the clientVars-hook so plugins can modify them before they get sent to the client
hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }, function( err, messages ) { let messages = await hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad });
if (ERR(err, callback)) return;
_.each(messages, function(newVars) {
// combine our old object with the new attributes from the hook // combine our old object with the new attributes from the hook
for(var attr in newVars) { for (let msg of messages) {
clientVars[attr] = newVars[attr]; Object.assign(clientVars, msg);
} }
});
// Join the pad and start receiving updates // Join the pad and start receiving updates
client.join(padIds.padId); client.join(padIds.padId);
@ -1345,13 +1241,11 @@ function handleClientReady(client, message)
// Save the current revision in sessioninfos, should be the same as in clientVars // Save the current revision in sessioninfos, should be the same as in clientVars
sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
});
}
sessioninfos[client.id].author = author; sessioninfos[client.id].author = author;
// prepare the notification for the other users on the pad, that this user joined // prepare the notification for the other users on the pad, that this user joined
var messageToTheOtherUsers = { let messageToTheOtherUsers = {
"type": "COLLABROOM", "type": "COLLABROOM",
"data": { "data": {
type: "USER_NEWINFO", type: "USER_NEWINFO",
@ -1373,39 +1267,29 @@ function handleClientReady(client, message)
client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers);
// Get sessions for this pad // Get sessions for this pad
var roomClients = _getRoomClients(pad.id); roomClients = _getRoomClients(pad.id);
for (let roomClient of roomClients) {
async.forEach(roomClients, function(roomClient, callback) {
var author;
// Jump over, if this session is the connection session // Jump over, if this session is the connection session
if (roomClient.id == client.id) { if (roomClient.id == client.id) {
return callback(); continue;
} }
// Since sessioninfos might change while being enumerated, check if the // Since sessioninfos might change while being enumerated, check if the
// sessionID is still assigned to a valid session // sessionID is still assigned to a valid session
if (sessioninfos[roomClient.id] !== undefined) { if (sessioninfos[roomClient.id] === undefined) {
author = sessioninfos[roomClient.id].author; continue;
} else {
// If the client id is not valid, callback();
return callback();
} }
async.waterfall([ let author = sessioninfos[roomClient.id].author;
// get the authorname & colorId // get the authorname & colorId
function(callback) {
// reuse previously created cache of author's data
if (historicalAuthorData[author]) {
callback(null, historicalAuthorData[author]);
} else {
authorManager.getAuthor(author, callback);
}
},
function(authorInfo, callback) { // reuse previously created cache of author's data
let authorInfo = historicalAuthorData[author] || await authorManager.getAuthor(author);
// Send the new User a Notification about this other user // Send the new User a Notification about this other user
var msg = { let msg = {
"type": "COLLABROOM", "type": "COLLABROOM",
"data": { "data": {
type: "USER_NEWINFO", type: "USER_NEWINFO",
@ -1421,13 +1305,7 @@ function handleClientReady(client, message)
client.json.send(msg); client.json.send(msg);
} }
], callback);
}, callback);
} }
],
function(err) {
ERR(err);
});
} }
/** /**
@ -1496,7 +1374,6 @@ function handleChangesetRequest(client, message)
]); ]);
} }
/** /**
* Tries to rebuild the getChangestInfo function of the original Etherpad * Tries to rebuild the getChangestInfo function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144

View file

@ -19,7 +19,6 @@
* limitations under the License. * limitations under the License.
*/ */
var ERR = require("async-stacktrace");
var log4js = require('log4js'); var log4js = require('log4js');
var messageLogger = log4js.getLogger("message"); var messageLogger = log4js.getLogger("message");
var securityManager = require("../db/SecurityManager"); var securityManager = require("../db/SecurityManager");
@ -80,7 +79,7 @@ exports.setSocketIO = function(_socket) {
components[i].handleConnect(client); components[i].handleConnect(client);
} }
client.on('message', function(message) { client.on('message', async function(message) {
if (message.protocolVersion && message.protocolVersion != 2) { if (message.protocolVersion && message.protocolVersion != 2) {
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
return; return;
@ -92,27 +91,22 @@ exports.setSocketIO = function(_socket) {
} else { } else {
// try to authorize the client // try to authorize the client
if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
var checkAccessCallback = function(err, statusObject) { // check for read-only pads
ERR(err); let padId = message.padId;
if (padId.indexOf("r.") === 0) {
padId = await readOnlyManager.getPadId(message.padId);
}
if (statusObject.accessStatus === "grant") { let { accessStatus } = await securityManager.checkAccess(padId, message.sessionID, message.token, message.password);
if (accessStatus === "grant") {
// access was granted, mark the client as authorized and handle the message // access was granted, mark the client as authorized and handle the message
clientAuthorized = true; clientAuthorized = true;
handleMessage(client, message); handleMessage(client, message);
} else { } else {
// no access, send the client a message that tells him why // no access, send the client a message that tells him why
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
client.json.send({accessStatus: statusObject.accessStatus}); client.json.send({ accessStatus });
}
};
if (message.padId.indexOf("r.") === 0) {
readOnlyManager.getPadId(message.padId, function(err, value) {
ERR(err);
securityManager.checkAccess(value, message.sessionID, message.token, message.password, checkAccessCallback);
});
} else {
// this message has everything to try an authorization
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, checkAccessCallback);
} }
} else { } else {
// drop message // drop message