From e58da69cfbb9af955e0e6424ec9496a34182fe7c Mon Sep 17 00:00:00 2001 From: Ray Bellis Date: Mon, 28 Jan 2019 13:13:24 +0000 Subject: [PATCH] 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! --- src/node/db/SecurityManager.js | 410 +++++++------- src/node/handler/PadMessageHandler.js | 749 +++++++++++--------------- src/node/handler/SocketIORouter.js | 36 +- 3 files changed, 508 insertions(+), 687 deletions(-) diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index e9aefcd61..dcdbe6a62 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -18,8 +18,6 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); -var async = require("async"); var authorManager = require("./AuthorManager"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var padManager = require("./PadManager"); @@ -35,270 +33,222 @@ const thenify = require("thenify").withCallback; * @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 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) { - callback(null, {accessStatus: "deny"}); - return; + return deny; } // allow plugins to deny access var deniedByHook = hooks.callAll("onAccessCheck", {'padID': padID, 'password': password, 'token': token, 'sessionCookie': sessionCookie}).indexOf(false) > -1; if (deniedByHook) { - callback(null, {accessStatus: "deny"}); - return; + return deny; } + // get author for this token + let tokenAuthor = await authorManager.getAuthor4Token(token); + + // check if pad exists + let padExists = await padManager.doesPadExist(padID); + if (settings.requireSession) { // a valid session is required (api-only mode) if (!sessionCookie) { // without sessionCookie, access is denied - callback(null, {accessStatus: "deny"}); - - return; + return deny; } } else { // a session is not required, so we'll check if it's a public pad if (padID.indexOf("$") === -1) { // 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 + let statusObject = { accessStatus: "grant", authorID: tokenAuthor }; - // assume user has access - statusObject = { accessStatus: "grant", authorID: author }; + if (settings.editOnly) { + // user can't create pads - if (settings.editOnly) { - // user can't create pads - - // check if pad exists - padManager.doesPadExists(padID, function(err, exists) { - if (ERR(err, callback)) return; - - if (!exists) { - // pad doesn't exist - user can't have access - statusObject.accessStatus = "deny"; - } - - // grant or deny access, with author of token - callback(null, statusObject); - }); - - return; + if (!padExists) { + // pad doesn't exist - user can't have access + statusObject.accessStatus = "deny"; } + } - // user may create new pads - no need to check anything - // grant access, with author of token - callback(null, statusObject); - }); - - // don't continue - return; + // user may create new pads - no need to check anything + // grant access, with author of token + return statusObject; } } - var groupID = padID.split("$")[0]; - var padExists = false; - var validSession = false; - var sessionAuthor; - var tokenAuthor; - var isPublic; - var isPasswordProtected; - var passwordStatus = password == null ? "notGiven" : "wrong"; // notGiven, correct, wrong + let validSession = false; + let sessionAuthor; + let isPublic; + let isPasswordProtected; + let 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; + // get information about all sessions contained in this cookie + if (sessionCookie) { + let groupID = padID.split("$")[0]; + let sessionIDs = sessionCookie.split(','); - padExists = exists; - callback(); - }); - }, + // was previously iterated in parallel using async.forEach + for (let sessionID of sessionIDs) { + try { + let sessionInfo = await sessionManager.getSessionInfo(sessionID); - // get information about all sessions contained in this cookie - function(callback) { - if (!sessionCookie) { - callback(); - return; - } - - var sessionIDs = sessionCookie.split(','); - - async.forEach(sessionIDs, function(sessionID, callback) { - sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) { - // 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? - if (sessionInfo.groupID != groupID) { - authLogger.debug("Auth failed: wrong group"); - callback(); - - return; - } - - // is validUntil still ok? - if (sessionInfo.validUntil <= now) { - authLogger.debug("Auth failed: validUntil"); - callback(); - - return; - } - - // There is a valid session - validSession = true; - sessionAuthor = sessionInfo.authorID; - - callback(); - }); - }, callback); - }, - - // get author for token - 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 (ERR(err, callback)) return; - - // is it a public pad? - isPublic = pad.getPublicStatus(); - - // is it password protected? - isPasswordProtected = pad.isPasswordProtected(); - - // is password correct? - if (isPasswordProtected && password && pad.isCorrectPassword(password)) { - passwordStatus = "correct"; + // is it for this group? + if (sessionInfo.groupID != groupID) { + authLogger.debug("Auth failed: wrong group"); + continue; } - callback(); - }); - }, + // is validUntil still ok? + let now = Math.floor(Date.now() / 1000); + if (sessionInfo.validUntil <= now) { + authLogger.debug("Auth failed: validUntil"); + continue; + } - function(callback) { - if (validSession && padExists) { - // - a valid session for this group is avaible AND pad exists - if (!isPasswordProtected) { - // - the pad is not password protected - - // --> grant access - statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (settings.sessionNoPassword) { - // - the setting to bypass password validation is set - - // --> grant access - statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (isPasswordProtected && passwordStatus === "correct") { - // - the pad is password protected and password is correct - - // --> grant access - statusObject = { accessStatus: "grant", authorID: sessionAuthor }; - } else if (isPasswordProtected && passwordStatus === "wrong") { - // - the pad is password protected but wrong password given - - // --> deny access, ask for new password and tell them that the password is wrong - statusObject = { accessStatus: "wrongPassword" }; - } else if (isPasswordProtected && passwordStatus === "notGiven") { - // - the pad is password protected but no password given - - // --> ask for password - statusObject = { accessStatus: "needPassword" }; + // fall-through - there is a valid session + validSession = true; + sessionAuthor = sessionInfo.authorID; + break; + } catch (err) { + // skip session if it doesn't exist + if (err.message == "sessionID does not exist") { + authLogger.debug("Auth failed: unknown session"); } else { - throw new Error("Ops, something wrong happend"); + throw err; } - } else if (validSession && !padExists) { - // - a valid session for this group avaible but pad doesn't exist - - // --> grant access - statusObject = {accessStatus: "grant", authorID: sessionAuthor}; - - if (settings.editOnly) { - // --> deny access if user isn't allowed to create the pad - authLogger.debug("Auth failed: valid session & pad does not exist"); - statusObject.accessStatus = "deny"; - } - } else if (!validSession && padExists) { - // there is no valid session avaiable AND pad exists - - // -- it's public and not password protected - if (isPublic && !isPasswordProtected) { - // --> grant access, with author of token - statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } else if (isPublic && isPasswordProtected && passwordStatus === "correct") { - // - it's public and password protected and password is correct - - // --> grant access, with author of token - statusObject = {accessStatus: "grant", authorID: tokenAuthor}; - } else if (isPublic && isPasswordProtected && passwordStatus === "wrong") { - // - 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 - statusObject = {accessStatus: "wrongPassword"}; - } else if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { - // - it's public and the pad is password protected but no password given - - // --> ask for password - statusObject = {accessStatus: "needPassword"}; - } else if (!isPublic) { - // - it's not public - - authLogger.debug("Auth failed: invalid session & pad is not public"); - // --> deny access - statusObject = {accessStatus: "deny"}; - } else { - throw new Error("Ops, something wrong happend"); - } - } else { - // there is no valid session avaiable AND pad doesn't exist - authLogger.debug("Auth failed: invalid session & pad does not exist"); - // --> deny access - statusObject = {accessStatus: "deny"}; } - - callback(); } - ], - function(err) { - if (ERR(err, callback)) return; + } - callback(null, statusObject); - }); -}); + if (padExists) { + let pad = await padManager.getPad(padID); + + // is it a public pad? + isPublic = pad.getPublicStatus(); + + // is it password protected? + isPasswordProtected = pad.isPasswordProtected(); + + // is password correct? + if (isPasswordProtected && password && pad.isCorrectPassword(password)) { + passwordStatus = "correct"; + } + } + + // - 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) { + // - the pad is not password protected + + // --> grant access + return grant; + } + + if (settings.sessionNoPassword) { + // - the setting to bypass password validation is set + + // --> grant access + return grant; + } + + if (isPasswordProtected && passwordStatus === "correct") { + // - the pad is password protected and password is correct + + // --> grant access + return grant; + } + + if (isPasswordProtected && passwordStatus === "wrong") { + // - the pad is password protected but wrong password given + + // --> deny access, ask for new password and tell them that the password is wrong + return { accessStatus: "wrongPassword" }; + } + + if (isPasswordProtected && passwordStatus === "notGiven") { + // - the pad is password protected but no password given + + // --> ask for password + return { accessStatus: "needPassword" }; + } + + throw new Error("Oops, something wrong happend"); + } + + if (validSession && !padExists) { + // - a valid session for this group avaible but pad doesn't exist + + // --> grant access by default + let accessStatus = "grant"; + let authorID = sessionAuthor; + + // --> deny access if user isn't allowed to create the pad + if (settings.editOnly) { + authLogger.debug("Auth failed: valid session & pad does not exist"); + accessStatus = "deny"; + } + + return { accessStatus, authorID }; + } + + if (!validSession && padExists) { + // there is no valid session avaiable AND pad exists + + let authorID = tokenAuthor; + let grant = Object.freeze({ accessStatus: "grant", authorID }); + + if (isPublic && !isPasswordProtected) { + // -- it's public and not password protected + + // --> grant access, with author of token + return grant; + } + + if (isPublic && isPasswordProtected && passwordStatus === "correct") { + // - it's public and password protected and password is correct + + // --> grant access, with author of token + return grant; + } + + if (isPublic && isPasswordProtected && passwordStatus === "wrong") { + // - 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 + return { accessStatus: "wrongPassword" }; + } + + if (isPublic && isPasswordProtected && passwordStatus === "notGiven") { + // - it's public and the pad is password protected but no password given + + // --> ask for password + return { accessStatus: "needPassword" }; + } + + if (!isPublic) { + // - it's not public + + authLogger.debug("Auth failed: invalid session & pad is not public"); + // --> deny access + return { accessStatus: "deny" }; + } + + throw new Error("Oops, something wrong happend"); + } + + // there is no valid session avaiable AND pad doesn't exist + authLogger.debug("Auth failed: invalid session & pad does not exist"); + return { accessStatus: "deny" }; +} diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 77d7dc6c5..3004ae49c 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -164,7 +164,7 @@ exports.handleDisconnect = function(client) * @param client the client that send this message * @param message the message from the client */ -exports.handleMessage = function(client, message) +exports.handleMessage = async function(client, message) { if (message == null) { return; @@ -181,35 +181,33 @@ exports.handleMessage = function(client, message) return; } - var handleMessageHook = function(callback) { + async function handleMessageHook() { // Allow plugins to bypass the readonly message blocker - hooks.aCallAll("handleMessageSecurity", { client: client, message: message }, function( err, messages ) { - if(ERR(err, callback)) return; - _.each(messages, function(newMessage){ - if ( newMessage === true ) { - thisSession.readonly = false; - } - }); - }); + let messages = await hooks.aCallAll("handleMessageSecurity", { client: client, message: message }); + + for (let message of messages) { + if (message === true) { + 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 // handleMessage will be called, even if the client is not authorized - hooks.aCallAll("handleMessage", { client: client, message: message }, function( err, messages ) { - if(ERR(err, callback)) return; - _.each(messages, function(newMessage){ - if ( newMessage === null ) { - dropMessage = true; - } - }); - - // If no plugins explicitly told us to drop the message, its ok to proceed - if(!dropMessage){ callback() }; - }); + messages = await hooks.aCallAll("handleMessage", { client: client, message: message }); + for (let message of messages) { + if (message === null ) { + dropMessage = true; + break; + } + } + return dropMessage; } - var finalHandler = function() { + function finalHandler() { // Check what type of message we get and delegate to the other methods if (message.type == "CLIENT_READY") { handleClientReady(client, message); @@ -256,54 +254,49 @@ exports.handleMessage = function(client, message) return; } - async.series([ - handleMessageHook, + let dropMessage = await handleMessageHook(); + if (!dropMessage) { // check permissions - function(callback) { - // client tried to auth for the first time (first msg from the client) - if (message.type == "CLIENT_READY") { + + // client tried to auth for the first time (first msg from the client) + if (message.type == "CLIENT_READY") { createSessionInfo(client, message); - } + } - // Note: message.sessionID is an entirely different kind of - // session from the sessions we use here! Beware! - // FIXME: Call our "sessions" "connections". - // FIXME: Use a hook instead - // FIXME: Allow to override readwrite access with readonly + // Note: message.sessionID is an entirely different kind of + // session from the sessions we use here! Beware! + // FIXME: Call our "sessions" "connections". + // FIXME: Use a hook instead + // FIXME: Allow to override readwrite access with readonly - // Simulate using the load testing tool - if (!sessioninfos[client.id].auth) { - console.error("Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.") - return; - } + // Simulate using the load testing tool + if (!sessioninfos[client.id].auth) { + console.error("Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.") + return; + } - var auth = sessioninfos[client.id].auth; - var checkAccessCallback = function(err, statusObject) { - if (ERR(err, callback)) return; + let auth = sessioninfos[client.id].auth; - 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 + let padId = auth.padID; - // check if pad is requested via readOnly - if (auth.padID.indexOf("r.") === 0) { - // Pad is readOnly, first get the real Pad ID - readOnlyManager.getPadId(auth.padID, function(err, value) { - ERR(err); - securityManager.checkAccess(value, auth.sessionID, auth.token, auth.password, checkAccessCallback); - }); - } else { - securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, auth.password, checkAccessCallback); - } - }, - finalHandler - ]); + // Pad is readOnly, first get the real Pad ID + if (padId.indexOf("r.") === 0) { + padId = await readOnlyManager.getPadId(padID); + } + + let { accessStatus } = await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password); + + // no access, send the client a message that tells him why + if (accessStatus !== "grant") { + client.json.send({ accessStatus }); + return; + } + + // access was granted + finalHandler(); + } } @@ -977,7 +970,7 @@ function createSessionInfo(client, message) * @param client the client that send this message * @param message the message from the client */ -function handleClientReady(client, message) +async function handleClientReady(client, message) { // check if all ok if (!message.token) { @@ -1000,434 +993,319 @@ function handleClientReady(client, message) return; } - var author; - var authorName; - var authorColorId; - var pad; var historicalAuthorData = {}; - var currentTime; - var padIds; hooks.callAll("clientReady", message); - async.series([ - // Get ro/rw id:s - function(callback) { - readOnlyManager.getIds(message.padId, function(err, value) { - if (ERR(err, callback)) return; + // Get ro/rw id:s + let padIds = await readOnlyManager.getIds(message.padId); - padIds = value; - callback(); - }); - }, + // check permissions - // check permissions - function(callback) { - // Note: message.sessionID is an entierly different kind of - // session from the sessions we use here! Beware! - // FIXME: Call our "sessions" "connections". - // FIXME: Use a hook instead - // FIXME: Allow to override readwrite access with readonly - securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) { - if (ERR(err, callback)) return; + // Note: message.sessionID is an entierly different kind of + // session from the sessions we use here! Beware! + // FIXME: Call our "sessions" "connections". + // FIXME: Use a hook instead + // FIXME: Allow to override readwrite access with readonly + let statusObject = await securityManager.checkAccess(padIds.padId, message.sessionID, message.token, message.password); + 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 - client.json.send({accessStatus: statusObject.accessStatus}) - } - }); - }, + // no access, send the client a message that tells him why + if (accessStatus !== "grant") { + client.json.send({ accessStatus }); + return; + } - // get all authordata of this new user, and load the pad-object from the database - function(callback) - { - async.parallel([ - // get colorId and name - function(callback) { - authorManager.getAuthor(author, function(err, value) { - if (ERR(err, callback)) return; + let author = statusObject.authorID; - authorColorId = value.colorId; - authorName = value.name; - callback(); - }); - }, + // get all authordata of this new user, and load the pad-object from the database + let value = await authorManager.getAuthor(author); + let authorColorId = value.colorId; + let authorName = value.name; - // get pad - function(callback) { - padManager.getPad(padIds.padId, function(err, value) { - if (ERR(err, callback)) return; + // get pad + let pad = await padManager.getPad(padIds.padId); - pad = value; - callback(); - }); - } - ], callback); - }, + // these db requests all need the pad object (timestamp of latest revision, author data) + let authors = pad.getAllAuthors(); - // these db requests all need the pad object (timestamp of latest revission, author data) - function(callback) { - var authors = pad.getAllAuthors(); + // get timestamp of latest revision needed for timeslider + let currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); - async.parallel([ - // get timestamp of latest revission needed for timeslider - function(callback) { - pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) { - if (ERR(err, callback)) return; + // get all author data out of the database + for (let authorId of authors) { + try { + let author = await authorManager.getAuthor(authorId); + historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) + } catch (err) { + messageLogger.error("There is no author for authorId:", authorId); + } + } - currentTime = date; - callback(); - }); - }, + // glue the clientVars together, send them and tell the other clients that a new one is there - // get all author data out of the database - function(callback) { - async.forEach(authors, function(authorId, callback) { - authorManager.getAuthor(authorId, function(err, author) { - if (!author && !err) { - messageLogger.error("There is no author for authorId:", authorId); + // Check that the client is still here. It might have disconnected between callbacks. + if (sessioninfos[client.id] === undefined) { + return; + } - return callback(); - } + // Check if this author is already on the pad, if yes, kick the other sessions! + let roomClients = _getRoomClients(pad.id); - if (ERR(err, callback)) return; + for (let client of roomClients) { + let sinfo = sessioninfos[client.id]; + if (sinfo && sinfo.author == author) { + // fix user's counter, works on page refresh or if user closes browser window and then rejoins + sessioninfos[client.id] = {}; + client.leave(padIds.padId); + client.json.send({disconnect:"userdup"}); + } + } - historicalAuthorData[authorId] = { name: author.name, colorId: author.colorId }; // Filter author attribs (e.g. don't send author's pads to all clients) - callback(); - }); - }, callback); - } - ], callback); + // Save in sessioninfos that this session belonges to this pad + sessioninfos[client.id].padId = padIds.padId; + sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; + sessioninfos[client.id].readonly = padIds.readonly; - }, + // Log creation/(re-)entering of a pad + let ip = remoteAddress[client.id]; - // 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. - if(sessioninfos[client.id] === undefined) { - return callback(); - } + // Anonymize the IP address if IP logging is disabled + if (settings.disableIPlogging) { + ip = 'ANONYMOUS'; + } - // Check if this author is already on the pad, if yes, kick the other sessions! - var roomClients = _getRoomClients(pad.id); + if (pad.head > 0) { + accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); + } else if (pad.head == 0) { + accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); + } - async.forEach(roomClients, function(client, callback) { - var sinfo = sessioninfos[client.id]; + if (message.reconnect) { + // If this is a reconnect, we don't have to send the client the ClientVars again + // Join the pad and start receiving updates + client.join(padIds.padId); - if (sinfo && sinfo.author == author) { - // fix user's counter, works on page refresh or if user closes browser window and then rejoins - sessioninfos[client.id] = {}; - client.leave(padIds.padId); - client.json.send({ disconnect:"userdup" }); - } - }); + // Save the revision in sessioninfos, we take the revision from the info the client send to us + sessioninfos[client.id].rev = message.client_rev; - // Save in sessioninfos that this session belonges to this pad - sessioninfos[client.id].padId = padIds.padId; - sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; - sessioninfos[client.id].readonly = padIds.readonly; + // During the client reconnect, client might miss some revisions from other clients. By using client revision, + // this below code sends all the revisions missed during the client reconnect + var revisionsNeeded = []; + var changesets = {}; - // Log creation/(re-)entering of a pad - var ip = remoteAddress[client.id]; + var startNum = message.client_rev + 1; + var endNum = pad.getHeadRevisionNumber() + 1; - // Anonymize the IP address if IP logging is disabled - if (settings.disableIPlogging) { - ip = 'ANONYMOUS'; - } + var headNum = pad.getHeadRevisionNumber(); - if (pad.head > 0) { - accessLogger.info('[ENTER] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" entered the pad'); - } else if (pad.head == 0) { - accessLogger.info('[CREATE] Pad "' + padIds.padId + '": Client ' + client.id + ' with IP "' + ip + '" created the pad'); - } + if (endNum > headNum + 1) { + endNum = headNum + 1; + } - if (message.reconnect == true) { - // If this is a reconnect, we don't have to send the client the ClientVars again - // Join the pad and start receiving updates - client.join(padIds.padId); + if (startNum < 0) { + startNum = 0; + } - // Save the revision in sessioninfos, we take the revision from the info the client send to us - sessioninfos[client.id].rev = message.client_rev; + for (let r = startNum; r < endNum; r++) { + revisionsNeeded.push(r); + changesets[r] = {}; + } - // During the client reconnect, client might miss some revisions from other clients. By using client revision, - // this below code sends all the revisions missed during the client reconnect - var revisionsNeeded = []; - var changesets = {}; + // get changesets, author and timestamp needed for pending revisions + for (let revNum of revisionsNeeded) { + changesets[revNum]['changeset'] = await pad.getRevisionChangeset(revNum); + changesets[revNum]['author'] = await pad.getRevisionAuthor(revNum); + changesets[revNum]['timestamp'] = await pad.getRevisionDate(revNum); + } - var startNum = message.client_rev + 1; - var endNum = pad.getHeadRevisionNumber() + 1; + // return pending changesets + for (let r of revisionsNeeded) { - async.series([ - // push all the revision numbers needed into revisionsNeeded array - function(callback) { - var headNum = pad.getHeadRevisionNumber(); - - if (endNum > headNum+1) { - endNum = headNum+1; - } - - if (startNum < 0) { - startNum = 0; - } - - for (var r = startNum; r < endNum; r++) { - revisionsNeeded.push(r); - changesets[r] = {}; - } - - callback(); - }, - - // get changesets needed for pending revisions - function(callback) { - 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 - function(err) { - if (ERR(err, callback)) return; - - async.eachSeries(revisionsNeeded, function(r, callback) { - var forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool); - var wireMsg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - headRev:pad.getHeadRevisionNumber(), - newRev:r, - changeset:forWire.translated, - apool: forWire.pool, - author: changesets[r]['author'], - currentTime: changesets[r]['timestamp'] + let forWire = Changeset.prepareForWire(changesets[r]['changeset'], pad.pool); + let wireMsg = {"type":"COLLABROOM", + "data":{type:"CLIENT_RECONNECT", + headRev:pad.getHeadRevisionNumber(), + newRev:r, + changeset:forWire.translated, + apool: forWire.pool, + author: changesets[r]['author'], + currentTime: changesets[r]['timestamp'] }}; - client.json.send(wireMsg); - callback(); - }); + client.json.send(wireMsg); + } - if (startNum == endNum) { - var Msg = {"type":"COLLABROOM", - "data":{type:"CLIENT_RECONNECT", - noChanges: true, - newRev: pad.getHeadRevisionNumber() - }}; - client.json.send(Msg); - } - }); - } else { - // This is a normal first connect - // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted - try { - var atext = Changeset.cloneAText(pad.atext); - var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); - var apool = attribsForWire.pool.toJsonable(); - atext.attribs = attribsForWire.translated; - } catch(e) { - console.error(e.stack || e) - client.json.send({ disconnect:"corruptPad" });// pull the brakes - return callback(); - } + if (startNum == endNum) { + var Msg = {"type":"COLLABROOM", + "data":{type:"CLIENT_RECONNECT", + noChanges: true, + newRev: pad.getHeadRevisionNumber() + }}; + client.json.send(Msg); + } - // Warning: never ever send padIds.padId to the client. If the - // client is read only you would open a security hole 1 swedish - // mile wide... - var clientVars = { - "skinName": settings.skinName, - "accountPrivs": { - "maxRevisions": 100 - }, - "automaticReconnectionTimeout": settings.automaticReconnectionTimeout, - "initialRevisionList": [], - "initialOptions": { - "guestPolicy": "deny" - }, - "savedRevisions": pad.getSavedRevisions(), - "collab_client_vars": { - "initialAttributedText": atext, - "clientIp": "127.0.0.1", - "padId": message.padId, - "historicalAuthorData": historicalAuthorData, - "apool": apool, - "rev": pad.getHeadRevisionNumber(), - "time": currentTime, - }, - "colorPalette": authorManager.getColorPalette(), + } else { + // This is a normal first connect + + // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted + try { + var atext = Changeset.cloneAText(pad.atext); + var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + var apool = attribsForWire.pool.toJsonable(); + atext.attribs = attribsForWire.translated; + } catch(e) { + console.error(e.stack || e) + client.json.send({ disconnect:"corruptPad" }); // pull the brakes + + return; + } + + // Warning: never ever send padIds.padId to the client. If the + // client is read only you would open a security hole 1 swedish + // mile wide... + var clientVars = { + "skinName": settings.skinName, + "accountPrivs": { + "maxRevisions": 100 + }, + "automaticReconnectionTimeout": settings.automaticReconnectionTimeout, + "initialRevisionList": [], + "initialOptions": { + "guestPolicy": "deny" + }, + "savedRevisions": pad.getSavedRevisions(), + "collab_client_vars": { + "initialAttributedText": atext, "clientIp": "127.0.0.1", - "userIsGuest": true, - "userColor": authorColorId, "padId": message.padId, - "padOptions": settings.padOptions, - "padShortcutEnabled": settings.padShortcutEnabled, - "initialTitle": "Pad: " + message.padId, - "opts": {}, - // tell the client the number of the latest chat-message, which will be - // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) - "chatHead": pad.chatHead, - "numConnectedUsers": roomClients.length, - "readOnlyId": padIds.readOnlyPadId, - "readonly": padIds.readonly, - "serverTimestamp": Date.now(), - "userId": author, - "abiwordAvailable": settings.abiwordAvailable(), - "sofficeAvailable": settings.sofficeAvailable(), - "exportAvailable": settings.exportAvailable(), - "plugins": { - "plugins": plugins.plugins, - "parts": plugins.parts, - }, - "indentationOnNewLine": settings.indentationOnNewLine, - "scrollWhenFocusLineIsOutOfViewport": { - "percentage" : { - "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, - "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, - }, - "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration, - "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, - "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, - }, - "initialChangesets": [] // FIXME: REMOVE THIS SHIT + "historicalAuthorData": historicalAuthorData, + "apool": apool, + "rev": pad.getHeadRevisionNumber(), + "time": currentTime, + }, + "colorPalette": authorManager.getColorPalette(), + "clientIp": "127.0.0.1", + "userIsGuest": true, + "userColor": authorColorId, + "padId": message.padId, + "padOptions": settings.padOptions, + "padShortcutEnabled": settings.padShortcutEnabled, + "initialTitle": "Pad: " + message.padId, + "opts": {}, + // tell the client the number of the latest chat-message, which will be + // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) + "chatHead": pad.chatHead, + "numConnectedUsers": roomClients.length, + "readOnlyId": padIds.readOnlyPadId, + "readonly": padIds.readonly, + "serverTimestamp": Date.now(), + "userId": author, + "abiwordAvailable": settings.abiwordAvailable(), + "sofficeAvailable": settings.sofficeAvailable(), + "exportAvailable": settings.exportAvailable(), + "plugins": { + "plugins": plugins.plugins, + "parts": plugins.parts, + }, + "indentationOnNewLine": settings.indentationOnNewLine, + "scrollWhenFocusLineIsOutOfViewport": { + "percentage" : { + "editionAboveViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, + "editionBelowViewport": settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, + }, + "duration": settings.scrollWhenFocusLineIsOutOfViewport.duration, + "scrollWhenCaretIsInTheLastLineOfViewport": settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, + "percentageToScrollWhenUserPressesArrowUp": settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, + }, + "initialChangesets": [] // FIXME: REMOVE THIS SHIT + } + + // Add a username to the clientVars if one avaiable + if (authorName != null) { + clientVars.userName = authorName; + } + + // call the clientVars-hook so plugins can modify them before they get sent to the client + let messages = await hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }); + + // combine our old object with the new attributes from the hook + for (let msg of messages) { + Object.assign(clientVars, msg); + } + + // Join the pad and start receiving updates + client.join(padIds.padId); + + // Send the clientVars to the Client + client.json.send({type: "CLIENT_VARS", data: clientVars}); + + // Save the current revision in sessioninfos, should be the same as in clientVars + sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); + + sessioninfos[client.id].author = author; + + // prepare the notification for the other users on the pad, that this user joined + let messageToTheOtherUsers = { + "type": "COLLABROOM", + "data": { + type: "USER_NEWINFO", + userInfo: { + "ip": "127.0.0.1", + "colorId": authorColorId, + "userAgent": "Anonymous", + "userId": author } + } + }; - // Add a username to the clientVars if one avaiable - if (authorName != null) { - clientVars.userName = authorName; - } + // Add the authorname of this new User, if avaiable + if (authorName != null) { + messageToTheOtherUsers.data.userInfo.name = authorName; + } - // 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 ) { - if (ERR(err, callback)) return; + // notify all existing users about new user + client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - _.each(messages, function(newVars) { - // combine our old object with the new attributes from the hook - for(var attr in newVars) { - clientVars[attr] = newVars[attr]; - } - }); + // Get sessions for this pad + roomClients = _getRoomClients(pad.id); + for (let roomClient of roomClients) { - // Join the pad and start receiving updates - client.join(padIds.padId); - - // Send the clientVars to the Client - client.json.send({ type: "CLIENT_VARS", data: clientVars }); - - // Save the current revision in sessioninfos, should be the same as in clientVars - sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); - }); + // Jump over, if this session is the connection session + if (roomClient.id == client.id) { + continue; } - sessioninfos[client.id].author = author; + // Since sessioninfos might change while being enumerated, check if the + // sessionID is still assigned to a valid session + if (sessioninfos[roomClient.id] === undefined) { + continue; + } - // prepare the notification for the other users on the pad, that this user joined - var messageToTheOtherUsers = { + let author = sessioninfos[roomClient.id].author; + + // get the authorname & colorId + + // 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 + let msg = { "type": "COLLABROOM", "data": { type: "USER_NEWINFO", userInfo: { "ip": "127.0.0.1", - "colorId": authorColorId, + "colorId": authorInfo.colorId, + "name": authorInfo.name, "userAgent": "Anonymous", "userId": author } } }; - // Add the authorname of this new User, if avaiable - if (authorName != null) { - messageToTheOtherUsers.data.userInfo.name = authorName; - } - - // notify all existing users about new user - client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers); - - // Get sessions for this pad - var roomClients = _getRoomClients(pad.id); - - async.forEach(roomClients, function(roomClient, callback) { - var author; - - // Jump over, if this session is the connection session - if (roomClient.id == client.id) { - return callback(); - } - - // Since sessioninfos might change while being enumerated, check if the - // sessionID is still assigned to a valid session - if (sessioninfos[roomClient.id] !== undefined) { - author = sessioninfos[roomClient.id].author; - } else { - // If the client id is not valid, callback(); - return callback(); - } - - async.waterfall([ - // 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) { - // Send the new User a Notification about this other user - var msg = { - "type": "COLLABROOM", - "data": { - type: "USER_NEWINFO", - userInfo: { - "ip": "127.0.0.1", - "colorId": authorInfo.colorId, - "name": authorInfo.name, - "userAgent": "Anonymous", - "userId": author - } - } - }; - - client.json.send(msg); - } - ], callback); - }, callback); + client.json.send(msg); } - ], - function(err) { - ERR(err); - }); + } } /** @@ -1496,7 +1374,6 @@ function handleChangesetRequest(client, message) ]); } - /** * 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 diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 08f9f47e8..27c1e9a22 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -19,7 +19,6 @@ * limitations under the License. */ -var ERR = require("async-stacktrace"); var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var securityManager = require("../db/SecurityManager"); @@ -80,7 +79,7 @@ exports.setSocketIO = function(_socket) { components[i].handleConnect(client); } - client.on('message', function(message) { + client.on('message', async function(message) { if (message.protocolVersion && message.protocolVersion != 2) { messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message)); return; @@ -92,27 +91,22 @@ exports.setSocketIO = function(_socket) { } else { // try to authorize the client if (message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) { - var checkAccessCallback = function(err, statusObject) { - ERR(err); + // check for read-only pads + let padId = message.padId; + if (padId.indexOf("r.") === 0) { + padId = await readOnlyManager.getPadId(message.padId); + } - if (statusObject.accessStatus === "grant") { - // access was granted, mark the client as authorized and handle the message - clientAuthorized = true; - handleMessage(client, message); - } else { - // no access, send the client a message that tells him why - messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); - client.json.send({accessStatus: statusObject.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); - }); + 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 + clientAuthorized = true; + handleMessage(client, message); } else { - // this message has everything to try an authorization - securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, checkAccessCallback); + // no access, send the client a message that tells him why + messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message)); + client.json.send({ accessStatus }); } } else { // drop message