This repository has been archived on 2022-05-17. You can view files and clone it, but cannot push or open issues or pull requests.
brainlet/api/v1/gateway/index.js

324 lines
No EOL
13 KiB
JavaScript

const User = require("../../../models/User");
const secret = require("../../../secret");
const config = require("../../../config");
const Category = require("../../../models/Category");
const RateLimiter = require("./ratelimiter");
const jwt = require("jsonwebtoken");
const siolib = require("socket.io");
const uuid = require("uuid");
class GatewayServer {
constructor(httpServer) {
this._io = siolib(httpServer);
this._gateway = this._io.of("/gateway");
this.rateLimiter = new RateLimiter({
points: 5,
time: 1000,
minPoints: 0
});
this.eventSetup();
this._commandPrefix = "/";
}
}
GatewayServer.prototype._sendSystemMessage = function(socket, message, category) {
const messageObject = {
author: {
username: "__SYSTEM",
_id: "5fc69864f15a7c5e504c9a1f"
},
category: {
title: category.title,
_id: category._id
},
content: message,
_id: uuid.v4()
};
socket.emit("message", messageObject);
};
GatewayServer.prototype.notifyClientsOfUpdate = function(reason) {
this._gateway.emit("refreshClient", { reason: reason || "REFRESH" });
};
GatewayServer.prototype._processCommand = async function(socket, message) {
const content = message.content;
const fullCommandString = content.slice(this._commandPrefix.length, content.length);
const fullCommand = fullCommandString.split(" ");
const command = fullCommand[0] || "INVALID_COMMAND";
const args = fullCommand.length - 1;
switch (command) {
case "INVALID_COMMAND": {
this._sendSystemMessage(socket, "Invalid command.", message.category);
break;
}
case "admin/fr": {
if (args === 1) {
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
this._gateway.emit("refreshClient", { reason: fullCommand[1] || "REFRESH" });
} else {
this._sendSystemMessage(socket, "how about no", message.category);
}
} else {
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
}
break;
}
case "admin/fru": {
if (args === 1) {
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
const user = await this._findSocketInRoom(message.category._id, fullCommand[1]);
if (!user) {
this._sendSystemMessage(socket, "User not found.", message.category);
break;
}
this._gateway.in(user.user.sid).emit("refreshClient", { reason: "REFRESH" });
} else {
this._sendSystemMessage(socket, "how about no", message.category);
}
} else {
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
}
break;
}
default: {
this._sendSystemMessage(socket, "That command does not exist.", message.category);
break;
}
}
};
GatewayServer.prototype.authDisconnect = function(socket, callback) {
console.log("[E] [gateway] [handshake] User disconnected due to failed authentication");
socket.isConnected = false;
socket.disconnect();
socket.disconnect(true);
callback(new Error("ERR_GATEWAY_AUTH_FAIL"));
};
GatewayServer.prototype.eventSetup = function() {
this._gateway.use((socket, callback) => {
console.log("[*] [gateway] [handshake] User authentication attempt");
socket.isConnected = false;
setTimeout(() => {
if (socket.isConnected) return;
console.log("[E] [gateway] [handshake] User still not connected after timeout, removing...");
socket.disconnect();
socket.disconnect(true);
}, config.gatewayStillNotConnectedTimeoutMS);
// TODO: Maybe passing the token in the query is not the best idea?
const token = socket.handshake.query.token;
if (!token) return this.authDisconnect(socket, callback);
if (!(typeof token === "string")) return this.authDisconnect(socket, callback);
const allSockets = this._gateway.sockets;
for (let [_, e] of allSockets) {
if (e.user && e.user.token === token) {
console.log(`[E] [gateway] [handshake] User ${e.user.username} tried to connect more than once, rejecting connection...`);
return this.authDisconnect(socket, callback);
}
}
jwt.verify(token, secret.jwtPrivateKey, {}, async (err, data) => {
if (err) return this.authDisconnect(socket, callback);
if (!data) return this.authDisconnect(socket, callback);
if (!data.username) return this.authDisconnect(socket, callback);
const user = await User.findByUsername(data.username);
if (!user) return this.authDisconnect(socket, callback);
let permissionLevel = config.roleMap[user.role];
if (!permissionLevel) {
permissionLevel = 0;
}
if (permissionLevel < config.roleMap.USER) return this.authDisconnect(socket, callback);
socket.user = {
username: data.username,
_id: user._id.toString(),
token, // NOTE(hippoz): Maybe not secure
permissionLevel,
color: user.color
};
console.log(`[*] [gateway] [handshake] User ${data.username} has successfully authenticated`);
return callback();
});
});
this._gateway.on("connection", (socket) => {
console.log(`[*] [gateway] [handshake] User ${socket.user.username} connected, sending hello and waiting for yoo...`);
socket.emit("hello", {
gatewayStillNotConnectedTimeoutMS: config.gatewayStillNotConnectedTimeoutMS,
resolvedUser: {
username: socket.user.username,
_id: socket.user._id
}
});
socket.once("yoo", () => {
console.log(`[*] [gateway] [handshake] Got yoo from ${socket.user.username}, connection is finally completed!`);
socket.isConnected = true;
socket.on("message", async ({ category, content, nickAuthor, destUser }) => {
if (!category || !content || !socket.joinedCategories || !socket.isConnected || !socket.user || !(typeof content === "string") || !(typeof category._id === "string")) return;
content = content.trim();
if (!content || content === "" || content === " " || content.length >= 2000) return;
if (!this.rateLimiter.consoom(socket.user.token)) { // TODO: maybe user ip instead of token?
console.log(`[E] [gateway] Rate limiting ${socket.user.username}`);
return;
}
// TODO: When/if category permissions are added, check if the user has permissions for that category
const categoryTitle = socket.joinedCategories[category._id];
if (!categoryTitle || !(typeof categoryTitle === "string")) return;
let messageObject = {
author: {
username: socket.user.username,
_id: socket.user._id,
color: socket.user.color
},
category: {
title: categoryTitle,
_id: category._id
},
content: content,
_id: uuid.v4()
};
if (nickAuthor && nickAuthor.username && (typeof nickAuthor.username) === "string" && nickAuthor.username.length <= 32 && nickAuthor.username.length >= 3) {
if (socket.user.permissionLevel === config.roleMap.BOT) {
messageObject = {
nickAuthor: {
username: nickAuthor.username
},
...messageObject
};
}
}
if (messageObject.content.startsWith(this._commandPrefix)) {
this._processCommand(socket, messageObject);
return;
}
if (destUser && destUser._id && (typeof destUser._id) === "string") {
const user = await this._findSocketInRoom(messageObject.category._id, destUser._id);
if (!user) return;
this._gateway.in(user.user.sid).emit("message", messageObject);
return;
}
this._gateway.in(category._id).emit("message", messageObject);
});
socket.on("subscribe", async (categories) => {
if ( !socket.isConnected || !socket.user || !categories || !Array.isArray(categories) || categories === []) return;
try {
for (const v of categories) {
if (!v && !(typeof v === "string")) continue;
// TODO: When/if category permissions are added, check if the user has permissions for that category
const category = await Category.findById(v);
if (category && category.title && category._id) {
if (!socket.joinedCategories) socket.joinedCategories = {};
if (socket.joinedCategories[v]) continue;
socket.joinedCategories[v] = category.title;
await socket.join(v);
console.log(`[*] [gateway] User ${socket.user.username} subscribed to room ${v} (${category.title}), sending updated user list to all members of that room...`);
const upd = await this._generateClientListUpdateObject(v, category.title);
this._gateway.in(v).emit("clientListUpdate", upd);
}
}
} catch (e) {
return;
}
});
socket.on("disconnecting", async () => {
console.log(`[*] [gateway] User ${socket.user.username} is disconnecting, broadcasting updated user list to all of the rooms they have been in...`);
const rooms = socket.rooms;
rooms.forEach(async (room) => {
// Socket io automatically adds a user to a room with their own id
if (room === socket.id) return;
const categoryTitle = socket.joinedCategories[room] || "UNKNOWN";
await socket.leave(room);
const upd = await this._generateClientListUpdateObject(room, categoryTitle);
socket.in(room).emit("clientListUpdate", upd);
});
});
});
});
};
GatewayServer.prototype._getSocketsInRoom = async function(room) {
// NOTE: I have no idea why i have to do this dumb thing, why can't socket io just let you simply get the sockets from a room? idk
// There kinda was a way in the previous version, but they want to change the api for the worse each version, i'm guessing
const clients = await this._gateway.in(room).allSockets();
const updatedClientList = [];
clients.forEach((sid) => {
const client = this._gateway.sockets.get(sid); // lol they also used dumb ass maps for the socket list, can you fucking not?
if (!client || !client.isConnected || !client.user) return;
updatedClientList.push({
user: {
username: client.user.username,
_id: client.user._id,
color: client.user.color,
sid: client.id
}
});
});
return updatedClientList;
};
GatewayServer.prototype._findSocketInRoom = async function(room, userid) {
// NOTE: I have no idea why i have to do this dumb thing, why can't socket io just let you simply get the sockets from a room? idk
// There kinda was a way in the previous version, but they want to change the api for the worse each version, i'm guessing
const clients = await this._gateway.in(room).allSockets();
const updatedClientList = [];
clients.forEach((sid) => {
const client = this._gateway.sockets.get(sid); // lol they also used dumb ass maps for the socket list, can you fucking not?
if (!client || !client.isConnected || !client.user) return;
if (userid !== client.user._id) return;
updatedClientList.push({
user: {
username: client.user.username,
_id: client.user._id,
color: client.user.color,
sid: client.id
}
});
});
return updatedClientList[0] || undefined;
};
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle="UNKNOWN") {
const clientList = await this._getSocketsInRoom(room);
return {
category: {
title: categoryTitle,
_id: room
},
clientList
};
};
module.exports = GatewayServer;