const User = require('../../../models/User'); const secret = require('../../../secret'); const config = require('../../../config'); const Category = require('../../../models/Category'); 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.eventSetup(); } } 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, token // Maybe not secure }; 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', ({ category, content }) => { 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; // 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; const messageObject = { author: { username: socket.user.username, _id: socket.user._id }, category: { title: categoryTitle, _id: category._id }, content: content, _id: uuid.v4() }; this._gateway.in(category._id).emit('message', messageObject); }); socket.on('subscribe', async (categories) => { if ( !socket.isConnected || !socket.user || !categories || !Array.isArray(categories) || categories === []) return; 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...`); this._gateway.in(v).emit('clientListUpdate', await this._generateClientListUpdateObject(v, category.title)); } } }); 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); socket.in(room).emit('clientListUpdate', await this._generateClientListUpdateObject(room, categoryTitle)); }); }); }); }); }; GatewayServer.prototype._getSocketsInRoom = async function(room) { const clients = this._gateway.in(room).sockets; const updatedClientList = []; clients.forEach((client) => { if (!client.isConnected || !client.user) return; updatedClientList.push({ user: { username: client.user.username, _id: client.user._id } }); }); return updatedClientList; }; GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle='UNKNOWN') { const clientList = await this._getSocketsInRoom(room); return { category: { title: categoryTitle, _id: room }, clientList }; }; module.exports = GatewayServer;