mirror of
https://github.com/lovasoa/whitebophir
synced 2024-11-10 06:24:17 +00:00
75d2bb1fe8
the server should not need it
217 lines
5.8 KiB
JavaScript
217 lines
5.8 KiB
JavaScript
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;
|
|
}
|