Spaces:
Running
Running
| var iolib = require("socket.io"), | |
| { log, gauge, monitorFunction } = require("./log.js"), | |
| BoardData = require("./boardData.js").BoardData, | |
| config = require("./configuration"), | |
| jsonwebtoken = require("jsonwebtoken"); | |
| /** Map from name to *promises* of BoardData | |
| @type {{[boardName: string]: Promise<BoardData>}} | |
| */ | |
| var boards = {}; | |
| /** | |
| * Prevents a function from throwing errors. | |
| * If the inner function throws, the outer function just returns undefined | |
| * and logs the error. | |
| * @template A | |
| * @param {A} fn | |
| * @returns {A} | |
| */ | |
| function noFail(fn) { | |
| const monitored = monitorFunction(fn); | |
| return function noFailWrapped(arg) { | |
| try { | |
| return monitored(arg); | |
| } catch (e) { | |
| console.trace(e); | |
| } | |
| }; | |
| } | |
| function startIO(app) { | |
| io = iolib(app); | |
| if (config.AUTH_SECRET_KEY) { | |
| // Middleware to check for valid jwt | |
| io.use(function (socket, next) { | |
| if (socket.handshake.query && socket.handshake.query.token) { | |
| jsonwebtoken.verify( | |
| socket.handshake.query.token, | |
| config.AUTH_SECRET_KEY, | |
| function (err, decoded) { | |
| if (err) | |
| return next(new Error("Authentication error: Invalid JWT")); | |
| next(); | |
| }, | |
| ); | |
| } else { | |
| next(new Error("Authentication error: No jwt provided")); | |
| } | |
| }); | |
| } | |
| io.on("connection", noFail(handleSocketConnection)); | |
| return io; | |
| } | |
| /** Returns a promise to a BoardData with the given name | |
| * @returns {Promise<BoardData>} | |
| */ | |
| function getBoard(name) { | |
| if (boards.hasOwnProperty(name)) { | |
| return boards[name]; | |
| } else { | |
| var board = BoardData.load(name); | |
| boards[name] = board; | |
| gauge("boards in memory", Object.keys(boards).length); | |
| return board; | |
| } | |
| } | |
| /** | |
| * Executes on every new connection | |
| * @param {iolib.Socket} socket | |
| */ | |
| function handleSocketConnection(socket) { | |
| /** | |
| * Function to call when an user joins a board | |
| * @param {string} name | |
| */ | |
| async function joinBoard(name) { | |
| // Default to the public board | |
| if (!name) name = "anonymous"; | |
| // Join the board | |
| socket.join(name); | |
| var board = await getBoard(name); | |
| board.users.add(socket.id); | |
| log("board joined", { board: board.name, users: board.users.size }); | |
| gauge("connected." + name, board.users.size); | |
| return board; | |
| } | |
| socket.on( | |
| "error", | |
| noFail(function onSocketError(error) { | |
| log("ERROR", error); | |
| }), | |
| ); | |
| socket.on("getboard", async function onGetBoard(name) { | |
| var board = await joinBoard(name); | |
| //Send all the board's data as soon as it's loaded | |
| socket.emit("broadcast", { _children: board.getAll() }); | |
| }); | |
| socket.on("joinboard", noFail(joinBoard)); | |
| var lastEmitSecond = (Date.now() / config.MAX_EMIT_COUNT_PERIOD) | 0; | |
| var emitCount = 0; | |
| socket.on( | |
| "broadcast", | |
| noFail(function onBroadcast(message) { | |
| var currentSecond = (Date.now() / config.MAX_EMIT_COUNT_PERIOD) | 0; | |
| if (currentSecond === lastEmitSecond) { | |
| emitCount++; | |
| if (emitCount > config.MAX_EMIT_COUNT) { | |
| var request = socket.client.request; | |
| if (emitCount % 100 === 0) { | |
| log("BANNED", { | |
| user_agent: request.headers["user-agent"], | |
| original_ip: | |
| request.headers["x-forwarded-for"] || | |
| request.headers["forwarded"], | |
| emit_count: emitCount, | |
| }); | |
| } | |
| return; | |
| } | |
| } else { | |
| emitCount = 0; | |
| lastEmitSecond = currentSecond; | |
| } | |
| var boardName = message.board || "anonymous"; | |
| var data = message.data; | |
| if (!socket.rooms.has(boardName)) socket.join(boardName); | |
| if (!data) { | |
| console.warn("Received invalid message: %s.", JSON.stringify(message)); | |
| return; | |
| } | |
| if ( | |
| !(data.tool || data.type === "child") || | |
| config.BLOCKED_TOOLS.includes(data.tool) | |
| ) { | |
| log("BLOCKED MESSAGE", data); | |
| return; | |
| } | |
| // Save the message in the board | |
| handleMessage(boardName, data, socket); | |
| //Send data to all other users connected on the same board | |
| socket.broadcast.to(boardName).emit("broadcast", data); | |
| }), | |
| ); | |
| socket.on("disconnecting", function onDisconnecting(reason) { | |
| socket.rooms.forEach(async function disconnectFrom(room) { | |
| if (boards.hasOwnProperty(room)) { | |
| var board = await boards[room]; | |
| board.users.delete(socket.id); | |
| var userCount = board.users.size; | |
| log("disconnection", { | |
| board: board.name, | |
| users: board.users.size, | |
| reason, | |
| }); | |
| gauge("connected." + board.name, userCount); | |
| if (userCount === 0) unloadBoard(room); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * Unloads a board from memory. | |
| * @param {string} boardName | |
| **/ | |
| async function unloadBoard(boardName) { | |
| if (boards.hasOwnProperty(boardName)) { | |
| const board = await boards[boardName]; | |
| await board.save(); | |
| log("unload board", { board: board.name, users: board.users.size }); | |
| delete boards[boardName]; | |
| gauge("boards in memory", Object.keys(boards).length); | |
| } | |
| } | |
| function handleMessage(boardName, message, socket) { | |
| if (message.tool === "Cursor") { | |
| message.socket = socket.id; | |
| } else { | |
| saveHistory(boardName, message); | |
| } | |
| } | |
| async function saveHistory(boardName, message) { | |
| if (!(message.tool || message.type === "child") && !message._children) { | |
| console.error("Received a badly formatted message (no tool). ", message); | |
| } | |
| var board = await getBoard(boardName); | |
| board.processMessage(message); | |
| } | |
| function generateUID(prefix, suffix) { | |
| var uid = Date.now().toString(36); //Create the uids in chronological order | |
| uid += Math.round(Math.random() * 36).toString(36); //Add a random character at the end | |
| if (prefix) uid = prefix + uid; | |
| if (suffix) uid = uid + suffix; | |
| return uid; | |
| } | |
| if (exports) { | |
| exports.start = startIO; | |
| } | |