Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add admin api #237

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<boardname>?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.
Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions server/boardData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
3 changes: 3 additions & 0 deletions server/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"] || ""),
};
37 changes: 37 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand Down Expand Up @@ -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);
Expand Down
44 changes: 32 additions & 12 deletions server/sockets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<BoardData>}
*/
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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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", {
Expand All @@ -163,7 +182,7 @@ function handleSocketConnection(socket) {
reason,
});
gauge("connected." + board.name, userCount);
if (userCount === 0) unloadBoard(room);
if (userCount === 0) unloadBoard(board.name);
}
});
});
Expand Down Expand Up @@ -209,4 +228,5 @@ function generateUID(prefix, suffix) {

if (exports) {
exports.start = startIO;
exports.deleteBoard = deleteBoard
}