forked from hippoz/brainlet
324 lines
No EOL
13 KiB
JavaScript
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; |