overleaf/services/real-time/app/coffee/Router.js

264 lines
No EOL
9.4 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let Router;
const metrics = require("metrics-sharelatex");
const logger = require("logger-sharelatex");
const settings = require("settings-sharelatex");
const WebsocketController = require("./WebsocketController");
const HttpController = require("./HttpController");
const HttpApiController = require("./HttpApiController");
const bodyParser = require("body-parser");
const base64id = require("base64id");
const basicAuth = require('basic-auth-connect');
const httpAuth = basicAuth(function(user, pass){
const isValid = (user === settings.internal.realTime.user) && (pass === settings.internal.realTime.pass);
if (!isValid) {
logger.err({user, pass}, "invalid login details");
}
return isValid;
});
module.exports = (Router = {
_handleError(callback, error, client, method, attrs) {
if (callback == null) { callback = function(error) {}; }
if (attrs == null) { attrs = {}; }
for (let key of ["project_id", "doc_id", "user_id"]) {
attrs[key] = client.ol_context[key];
}
attrs.client_id = client.id;
attrs.err = error;
if (error.name === "CodedError") {
logger.warn(attrs, error.message, {code: error.code});
return callback({message: error.message, code: error.code});
}
if (error.message === 'unexpected arguments') {
// the payload might be very large, put it on level info
logger.log(attrs, 'unexpected arguments');
metrics.inc('unexpected-arguments', 1, { status: method });
return callback({ message: error.message });
}
if (["not authorized", "doc updater could not load requested ops", "no project_id found on client"].includes(error.message)) {
logger.warn(attrs, error.message);
return callback({message: error.message});
} else {
logger.error(attrs, `server side error in ${method}`);
// Don't return raw error to prevent leaking server side info
return callback({message: "Something went wrong in real-time service"});
}
},
_handleInvalidArguments(client, method, args) {
const error = new Error("unexpected arguments");
let callback = args[args.length - 1];
if (typeof callback !== 'function') {
callback = (function() {});
}
const attrs = {arguments: args};
return Router._handleError(callback, error, client, method, attrs);
},
configure(app, io, session) {
app.set("io", io);
app.get("/clients", HttpController.getConnectedClients);
app.get("/clients/:client_id", HttpController.getConnectedClient);
app.post("/project/:project_id/message/:message", httpAuth, bodyParser.json({limit: "5mb"}), HttpApiController.sendMessage);
app.post("/drain", httpAuth, HttpApiController.startDrain);
app.post("/client/:client_id/disconnect", httpAuth, HttpApiController.disconnectClient);
return session.on('connection', function(error, client, session) {
// init client context, we may access it in Router._handleError before
// setting any values
let user;
client.ol_context = {};
if (client != null) {
client.on("error", function(err) {
logger.err({ clientErr: err }, "socket.io client error");
if (client.connected) {
client.emit("reconnectGracefully");
return client.disconnect();
}
});
}
if (settings.shutDownInProgress) {
client.emit("connectionRejected", {message: "retry"});
client.disconnect();
return;
}
if ((client != null) && __guard__(error != null ? error.message : undefined, x => x.match(/could not look up session by key/))) {
logger.warn({err: error, client: (client != null), session: (session != null)}, "invalid session");
// tell the client to reauthenticate if it has an invalid session key
client.emit("connectionRejected", {message: "invalid session"});
client.disconnect();
return;
}
if (error != null) {
logger.err({err: error, client: (client != null), session: (session != null)}, "error when client connected");
if (client != null) {
client.emit("connectionRejected", {message: "error"});
}
if (client != null) {
client.disconnect();
}
return;
}
// send positive confirmation that the client has a valid connection
client.publicId = 'P.' + base64id.generateId();
client.emit("connectionAccepted", null, client.publicId);
metrics.inc('socket-io.connection');
metrics.gauge('socket-io.clients', __guard__(io.sockets.clients(), x1 => x1.length));
logger.log({session, client_id: client.id}, "client connected");
if (__guard__(session != null ? session.passport : undefined, x2 => x2.user) != null) {
({
user
} = session.passport);
} else if ((session != null ? session.user : undefined) != null) {
({
user
} = session);
} else {
user = {_id: "anonymous-user"};
}
client.on("joinProject", function(data, callback) {
if (data == null) { data = {}; }
if (typeof callback !== 'function') {
return Router._handleInvalidArguments(client, 'joinProject', arguments);
}
if (data.anonymousAccessToken) {
user.anonymousAccessToken = data.anonymousAccessToken;
}
return WebsocketController.joinProject(client, user, data.project_id, function(err, ...args) {
if (err != null) {
return Router._handleError(callback, err, client, "joinProject", {project_id: data.project_id, user_id: (user != null ? user.id : undefined)});
} else {
return callback(null, ...Array.from(args));
}
});
});
client.on("disconnect", function() {
metrics.inc('socket-io.disconnect');
metrics.gauge('socket-io.clients', __guard__(io.sockets.clients(), x3 => x3.length) - 1);
return WebsocketController.leaveProject(io, client, function(err) {
if (err != null) {
return Router._handleError((function() {}), err, client, "leaveProject");
}
});
});
// Variadic. The possible arguments:
// doc_id, callback
// doc_id, fromVersion, callback
// doc_id, options, callback
// doc_id, fromVersion, options, callback
client.on("joinDoc", function(doc_id, fromVersion, options, callback) {
if ((typeof fromVersion === "function") && !options) {
callback = fromVersion;
fromVersion = -1;
options = {};
} else if ((typeof fromVersion === "number") && (typeof options === "function")) {
callback = options;
options = {};
} else if ((typeof fromVersion === "object") && (typeof options === "function")) {
callback = options;
options = fromVersion;
fromVersion = -1;
} else if ((typeof fromVersion === "number") && (typeof options === "object") && (typeof callback === 'function')) {
// Called with 4 args, things are as expected
} else {
return Router._handleInvalidArguments(client, 'joinDoc', arguments);
}
return WebsocketController.joinDoc(client, doc_id, fromVersion, options, function(err, ...args) {
if (err != null) {
return Router._handleError(callback, err, client, "joinDoc", {doc_id, fromVersion});
} else {
return callback(null, ...Array.from(args));
}
});
});
client.on("leaveDoc", function(doc_id, callback) {
if (typeof callback !== 'function') {
return Router._handleInvalidArguments(client, 'leaveDoc', arguments);
}
return WebsocketController.leaveDoc(client, doc_id, function(err, ...args) {
if (err != null) {
return Router._handleError(callback, err, client, "leaveDoc");
} else {
return callback(null, ...Array.from(args));
}
});
});
client.on("clientTracking.getConnectedUsers", function(callback) {
if (callback == null) { callback = function(error, users) {}; }
if (typeof callback !== 'function') {
return Router._handleInvalidArguments(client, 'clientTracking.getConnectedUsers', arguments);
}
return WebsocketController.getConnectedUsers(client, function(err, users) {
if (err != null) {
return Router._handleError(callback, err, client, "clientTracking.getConnectedUsers");
} else {
return callback(null, users);
}
});
});
client.on("clientTracking.updatePosition", function(cursorData, callback) {
if (callback == null) { callback = function(error) {}; }
if (typeof callback !== 'function') {
return Router._handleInvalidArguments(client, 'clientTracking.updatePosition', arguments);
}
return WebsocketController.updateClientPosition(client, cursorData, function(err) {
if (err != null) {
return Router._handleError(callback, err, client, "clientTracking.updatePosition");
} else {
return callback();
}
});
});
return client.on("applyOtUpdate", function(doc_id, update, callback) {
if (callback == null) { callback = function(error) {}; }
if (typeof callback !== 'function') {
return Router._handleInvalidArguments(client, 'applyOtUpdate', arguments);
}
return WebsocketController.applyOtUpdate(client, doc_id, update, function(err) {
if (err != null) {
return Router._handleError(callback, err, client, "applyOtUpdate", {doc_id, update});
} else {
return callback();
}
});
});
});
}
});
function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}