diff --git a/README.md b/README.md index 0560c10d..480c140f 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ WBO supports authentication with a JWT. This should be passed in as a query with The `AUTH_SECRET_KEY` variable in [`configuration.js`](./server/configuration.js) should be filled with the secret key for the JWT. +## Admin API + +WBO provides an administrative API for deleting boards. It can be used as following: `https://myboard.com/admin/delete/?secret={key}` where `key` is the secret set in environment variable `ADMIN_SECRET_KEY`. If it is not provided the API will not be available. + ## Configuration When you start a WBO server, it loads its configuration from several environment variables. @@ -93,6 +97,7 @@ Some important environment variables are : - `WBO_HISTORY_DIR` : configures the directory where the boards are saved. Defaults to `./server-data/`. - `WBO_MAX_EMIT_COUNT` : the maximum number of messages that a client can send per unit of time. Increase this value if you want smoother drawings, at the expense of being susceptible to denial of service attacks if your server does not have enough processing power. By default, the units of this quantity are messages per 4 seconds, and the default value is `192`. - `AUTH_SECRET_KEY` : If you would like to authenticate your boards using jwt, this declares the secret key. + - `ADMIN_SECRET_KEY` : If you would like to use the administrative API provide the secret key for authentication. ## Troubleshooting diff --git a/server/boardData.js b/server/boardData.js index 3bb4c973..9be12b6c 100644 --- a/server/boardData.js +++ b/server/boardData.js @@ -287,6 +287,13 @@ class BoardData { } } + /** Deletes the board from storage + */ + deleteBoard() { + this.board = {} + this.save() + } + /** Load the data in the board from a file. * @param {string} name - name of the board */ diff --git a/server/configuration.js b/server/configuration.js index 04036b98..411e44ec 100644 --- a/server/configuration.js +++ b/server/configuration.js @@ -57,4 +57,7 @@ module.exports = { /** Secret key for jwt */ AUTH_SECRET_KEY: (process.env["AUTH_SECRET_KEY"] || ""), + + /** Admin Secret key for admin API */ + ADMIN_SECRET_KEY: (process.env["ADMIN_SECRET_KEY"] || ""), }; diff --git a/server/server.js b/server/server.js index 8e9f49aa..71898139 100644 --- a/server/server.js +++ b/server/server.js @@ -113,6 +113,27 @@ function checkUserPermission(url) { } } +/** + * Throws an error if the user does not have admin permission + * @param {URL} url + * @throws {Error} + */ + function checkAdminPermission(url) { + if(config.ADMIN_SECRET_KEY != "") { + var secret = url.searchParams.get("secret"); + if(secret) { + console.log(secret, config.ADMIN_SECRET_KEY) + if(secret !== config.ADMIN_SECRET_KEY){ + throw new Error("Secret is wrong"); + } + } else { // Error out as no token provided + throw new Error("No secret provided"); + } + } else { + throw new Error("Admin API is not activated") + } + } + /** * @type {import('http').RequestListener} */ @@ -231,6 +252,22 @@ function handleRequest(request, response) { }); break; + case "admin": + // Check if allowed access + checkAdminPermission(parsedUrl) + if (parts.length >= 2 && parts[1] !== "") { + if (parts[1] === "delete" && parts[2] !== ""){ + validateBoardName(parts[2]); + sockets.deleteBoard(parts[2]) + response.end("Ok"); + }else{ + throw new Error("Not enough arguments") + } + }else { + throw new Error("No action argument provided") + } + break; + case "": // Index page logRequest(request); indexTemplate.serve(request, response); diff --git a/server/sockets.js b/server/sockets.js index 58d9c656..6c44a353 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -28,6 +28,14 @@ function noFail(fn) { }; } +/** + * Standardises the Board name + * - Making it CaseInsenitive + */ +function standardiseBoardName(name) { + return name.toLowerCase() +} + function startIO(app) { io = iolib(app); if (config.AUTH_SECRET_KEY) { @@ -47,15 +55,25 @@ function startIO(app) { return io; } +async function deleteBoard(boardName) { + const board = await getBoard(boardName) + // Delete board if it exists, otherwise its already non-existent + if(board){ + await board.deleteBoard() + log("deleted board", { board: board.name }); + } + } + /** Returns a promise to a BoardData with the given name * @returns {Promise} */ function getBoard(name) { - if (boards.hasOwnProperty(name)) { - return boards[name]; + const standardisedName = standardiseBoardName(name); + if (boards.hasOwnProperty(standardisedName)) { + return boards[standardisedName]; } else { - var board = BoardData.load(name); - boards[name] = board; + var board = BoardData.load(standardisedName); + boards[standardisedName] = board; gauge("boards in memory", Object.keys(boards).length); return board; } @@ -72,15 +90,15 @@ function handleSocketConnection(socket) { */ async function joinBoard(name) { // Default to the public board - if (!name) name = "anonymous"; + const boardname = standardiseBoardName(name || "anonymous"); // Join the board - socket.join(name); + socket.join(boardname); - var board = await getBoard(name); + var board = await getBoard(boardname); board.users.add(socket.id); log("board joined", { board: board.name, users: board.users.size }); - gauge("connected." + name, board.users.size); + gauge("connected." + board.name, board.users.size); return board; } @@ -125,7 +143,7 @@ function handleSocketConnection(socket) { lastEmitSecond = currentSecond; } - var boardName = message.board || "anonymous"; + var boardName = standardiseBoardName(message.board || "anonymous"); var data = message.data; if (!socket.rooms.has(boardName)) socket.join(boardName); @@ -153,8 +171,9 @@ function handleSocketConnection(socket) { socket.on("disconnecting", function onDisconnecting(reason) { socket.rooms.forEach(async function disconnectFrom(room) { - if (boards.hasOwnProperty(room)) { - var board = await boards[room]; + const standardisedName = standardiseBoardName(room) + if (boards.hasOwnProperty(standardisedName)) { + var board = await boards[standardisedName]; board.users.delete(socket.id); var userCount = board.users.size; log("disconnection", { @@ -163,7 +182,7 @@ function handleSocketConnection(socket) { reason, }); gauge("connected." + board.name, userCount); - if (userCount === 0) unloadBoard(room); + if (userCount === 0) unloadBoard(board.name); } }); }); @@ -209,4 +228,5 @@ function generateUID(prefix, suffix) { if (exports) { exports.start = startIO; + exports.deleteBoard = deleteBoard }