2021-03-04 21:28:02 +02:00
const User = require ( "../../../models/User" ) ;
const secret = require ( "../../../secret" ) ;
const config = require ( "../../../config" ) ;
const Category = require ( "../../../models/Category" ) ;
const RateLimiter = require ( "./ratelimiter" ) ;
2020-11-16 21:16:25 +02:00
2021-03-04 21:28:02 +02:00
const jwt = require ( "jsonwebtoken" ) ;
const siolib = require ( "socket.io" ) ;
const uuid = require ( "uuid" ) ;
2020-11-16 21:16:25 +02:00
class GatewayServer {
constructor ( httpServer ) {
this . _io = siolib ( httpServer ) ;
2021-03-04 21:28:02 +02:00
this . _gateway = this . _io . of ( "/gateway" ) ;
2020-11-26 11:42:27 +02:00
this . rateLimiter = new RateLimiter ( {
points : 5 ,
time : 1000 ,
minPoints : 0
} ) ;
2020-11-16 21:16:25 +02:00
this . eventSetup ( ) ;
2020-12-01 22:28:35 +02:00
2021-03-04 21:28:02 +02:00
this . _commandPrefix = "/" ;
2020-11-16 21:16:25 +02:00
}
}
2020-12-01 22:28:35 +02:00
GatewayServer . prototype . _sendSystemMessage = function ( socket , message , category ) {
const messageObject = {
author : {
2021-03-04 21:28:02 +02:00
username : "__SYSTEM" ,
_id : "5fc69864f15a7c5e504c9a1f"
2020-12-01 22:28:35 +02:00
} ,
category : {
title : category . title ,
_id : category . _id
} ,
content : message ,
_id : uuid . v4 ( )
} ;
2021-03-04 21:28:02 +02:00
socket . emit ( "message" , messageObject ) ;
2020-12-01 22:28:35 +02:00
} ;
GatewayServer . prototype . notifyClientsOfUpdate = function ( reason ) {
2021-03-04 21:28:02 +02:00
this . _gateway . emit ( "refreshClient" , { reason : reason || "REFRESH" } ) ;
2020-12-01 22:28:35 +02:00
} ;
2020-12-08 00:27:51 +02:00
GatewayServer . prototype . _processCommand = async function ( socket , message ) {
2020-12-01 22:28:35 +02:00
const content = message . content ;
const fullCommandString = content . slice ( this . _commandPrefix . length , content . length ) ;
2021-03-04 21:28:02 +02:00
const fullCommand = fullCommandString . split ( " " ) ;
const command = fullCommand [ 0 ] || "INVALID_COMMAND" ;
2020-12-01 22:28:35 +02:00
const args = fullCommand . length - 1 ;
switch ( command ) {
2021-03-04 21:28:02 +02:00
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" } ) ;
2020-12-01 22:28:35 +02:00
} else {
2021-03-04 21:28:02 +02:00
this . _sendSystemMessage ( socket , "how about no" , message . category ) ;
2020-12-01 22:28:35 +02:00
}
2021-03-04 21:28:02 +02:00
} else {
this . _sendSystemMessage ( socket , "Invalid number of arguments." , message . category ) ;
2020-12-01 22:28:35 +02:00
}
2021-03-04 21:28:02 +02:00
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 ;
2020-12-08 00:25:27 +02:00
}
2021-03-04 21:28:02 +02:00
this . _gateway . in ( user . user . sid ) . emit ( "refreshClient" , { reason : "REFRESH" } ) ;
2020-12-08 00:25:27 +02:00
} else {
2021-03-04 21:28:02 +02:00
this . _sendSystemMessage ( socket , "how about no" , message . category ) ;
2020-12-08 00:25:27 +02:00
}
2021-03-04 21:28:02 +02:00
} else {
this . _sendSystemMessage ( socket , "Invalid number of arguments." , message . category ) ;
2020-12-01 22:28:35 +02:00
}
2021-03-04 21:28:02 +02:00
break ;
}
default : {
this . _sendSystemMessage ( socket , "That command does not exist." , message . category ) ;
break ;
}
2020-12-01 22:28:35 +02:00
}
} ;
2020-11-16 21:16:25 +02:00
GatewayServer . prototype . authDisconnect = function ( socket , callback ) {
2021-03-04 21:28:02 +02:00
console . log ( "[E] [gateway] [handshake] User disconnected due to failed authentication" ) ;
2020-11-16 21:16:25 +02:00
socket . isConnected = false ;
2020-11-16 21:33:03 +02:00
socket . disconnect ( ) ;
2020-11-16 21:16:25 +02:00
socket . disconnect ( true ) ;
2021-03-04 21:28:02 +02:00
callback ( new Error ( "ERR_GATEWAY_AUTH_FAIL" ) ) ;
2020-11-16 21:16:25 +02:00
} ;
GatewayServer . prototype . eventSetup = function ( ) {
this . _gateway . use ( ( socket , callback ) => {
2021-03-04 21:28:02 +02:00
console . log ( "[*] [gateway] [handshake] User authentication attempt" ) ;
2020-11-16 21:16:25 +02:00
socket . isConnected = false ;
setTimeout ( ( ) => {
2020-11-16 21:35:38 +02:00
if ( socket . isConnected ) return ;
2021-03-04 21:28:02 +02:00
console . log ( "[E] [gateway] [handshake] User still not connected after timeout, removing..." ) ;
2020-11-16 21:33:03 +02:00
socket . disconnect ( ) ;
2020-11-16 21:16:25 +02:00
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 ) ;
2021-03-04 21:28:02 +02:00
if ( ! ( typeof token === "string" ) ) return this . authDisconnect ( socket , callback ) ;
2020-11-16 21:16:25 +02:00
2020-11-21 14:44:54 +02:00
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 ) ;
}
}
2020-11-16 21:16:25 +02:00
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 ) ;
2020-11-17 18:13:07 +02:00
socket . user = {
username : data . username ,
2020-12-05 21:06:28 +02:00
_id : user . _id . toString ( ) ,
2020-12-03 01:46:29 +02:00
token , // NOTE(hippoz): Maybe not secure
permissionLevel ,
color : user . color
2020-11-17 18:13:07 +02:00
} ;
2020-11-16 22:08:23 +02:00
console . log ( ` [*] [gateway] [handshake] User ${ data . username } has successfully authenticated ` ) ;
2020-11-16 21:16:25 +02:00
return callback ( ) ;
} ) ;
} ) ;
2021-03-04 21:28:02 +02:00
this . _gateway . on ( "connection" , ( socket ) => {
2020-11-17 18:13:07 +02:00
console . log ( ` [*] [gateway] [handshake] User ${ socket . user . username } connected, sending hello and waiting for yoo... ` ) ;
2021-03-04 21:28:02 +02:00
socket . emit ( "hello" , {
2020-11-17 18:13:07 +02:00
gatewayStillNotConnectedTimeoutMS : config . gatewayStillNotConnectedTimeoutMS ,
resolvedUser : {
username : socket . user . username ,
_id : socket . user . _id
}
} ) ;
2021-03-04 21:28:02 +02:00
socket . once ( "yoo" , ( ) => {
2020-11-17 18:13:07 +02:00
console . log ( ` [*] [gateway] [handshake] Got yoo from ${ socket . user . username } , connection is finally completed! ` ) ;
2020-11-16 21:16:25 +02:00
socket . isConnected = true ;
2020-11-16 22:08:23 +02:00
2021-03-04 21:28:02 +02:00
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 ;
2020-11-17 18:13:07 +02:00
content = content . trim ( ) ;
2021-03-04 21:28:02 +02:00
if ( ! content || content === "" || content === " " || content . length >= 2000 ) return ;
2020-11-26 11:42:27 +02:00
if ( ! this . rateLimiter . consoom ( socket . user . token ) ) { // TODO: maybe user ip instead of token?
console . log ( ` [E] [gateway] Rate limiting ${ socket . user . username } ` ) ;
return ;
}
2020-11-17 15:27:23 +02:00
// TODO: When/if category permissions are added, check if the user has permissions for that category
const categoryTitle = socket . joinedCategories [ category . _id ] ;
2021-03-04 21:28:02 +02:00
if ( ! categoryTitle || ! ( typeof categoryTitle === "string" ) ) return ;
2020-11-17 15:27:23 +02:00
2020-12-05 19:10:11 +02:00
let messageObject = {
2020-11-17 15:27:23 +02:00
author : {
2020-11-17 18:13:07 +02:00
username : socket . user . username ,
2020-12-03 01:46:29 +02:00
_id : socket . user . _id ,
color : socket . user . color
2020-11-17 15:27:23 +02:00
} ,
category : {
title : categoryTitle ,
_id : category . _id
} ,
content : content ,
_id : uuid . v4 ( )
2020-11-17 11:28:11 +02:00
} ;
2020-11-20 20:15:20 +02:00
2021-03-04 21:28:02 +02:00
if ( nickAuthor && nickAuthor . username && ( typeof nickAuthor . username ) === "string" && nickAuthor . username . length <= 32 && nickAuthor . username . length >= 3 ) {
2020-12-05 19:10:11 +02:00
if ( socket . user . permissionLevel === config . roleMap . BOT ) {
messageObject = {
nickAuthor : {
username : nickAuthor . username
} ,
... messageObject
2021-03-04 21:28:02 +02:00
} ;
2020-12-05 19:10:11 +02:00
}
}
2020-12-01 22:28:35 +02:00
if ( messageObject . content . startsWith ( this . _commandPrefix ) ) {
this . _processCommand ( socket , messageObject ) ;
return ;
}
2021-03-04 21:28:02 +02:00
if ( destUser && destUser . _id && ( typeof destUser . _id ) === "string" ) {
2020-12-05 21:06:28 +02:00
const user = await this . _findSocketInRoom ( messageObject . category . _id , destUser . _id ) ;
if ( ! user ) return ;
2021-03-04 21:28:02 +02:00
this . _gateway . in ( user . user . sid ) . emit ( "message" , messageObject ) ;
2020-12-05 21:06:28 +02:00
return ;
}
2021-03-04 21:28:02 +02:00
this . _gateway . in ( category . _id ) . emit ( "message" , messageObject ) ;
2020-11-16 22:08:23 +02:00
} ) ;
2021-03-04 21:28:02 +02:00
socket . on ( "subscribe" , async ( categories ) => {
2020-11-20 19:25:56 +02:00
if ( ! socket . isConnected || ! socket . user || ! categories || ! Array . isArray ( categories ) || categories === [ ] ) return ;
2021-01-03 17:04:23 +02:00
try {
for ( const v of categories ) {
2021-03-04 21:28:02 +02:00
if ( ! v && ! ( typeof v === "string" ) ) continue ;
2021-01-03 17:04:23 +02:00
// 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 ) ;
2021-03-04 21:28:02 +02:00
this . _gateway . in ( v ) . emit ( "clientListUpdate" , upd ) ;
2021-01-03 17:04:23 +02:00
}
2020-11-17 15:27:23 +02:00
}
2021-01-03 17:04:23 +02:00
} catch ( e ) {
return ;
}
2020-11-16 22:08:23 +02:00
} ) ;
2020-11-21 12:08:32 +02:00
2021-03-04 21:28:02 +02:00
socket . on ( "disconnecting" , async ( ) => {
2020-11-21 12:08:32 +02:00
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 ;
2021-03-04 21:28:02 +02:00
const categoryTitle = socket . joinedCategories [ room ] || "UNKNOWN" ;
2020-11-21 12:08:32 +02:00
await socket . leave ( room ) ;
2020-11-21 17:40:35 +02:00
const upd = await this . _generateClientListUpdateObject ( room , categoryTitle ) ;
2021-03-04 21:28:02 +02:00
socket . in ( room ) . emit ( "clientListUpdate" , upd ) ;
2020-11-21 12:08:32 +02:00
} ) ;
} ) ;
2020-11-16 21:16:25 +02:00
} ) ;
} ) ;
} ;
2020-11-21 12:08:32 +02:00
GatewayServer . prototype . _getSocketsInRoom = async function ( room ) {
2020-11-21 17:40:35 +02:00
// 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 ( ) ;
2020-11-21 12:08:32 +02:00
const updatedClientList = [ ] ;
2020-11-21 17:40:35 +02:00
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 ;
2020-11-21 12:08:32 +02:00
updatedClientList . push ( {
user : {
username : client . user . username ,
2020-12-03 01:46:29 +02:00
_id : client . user . _id ,
2020-12-05 21:06:28 +02:00
color : client . user . color ,
sid : client . id
2020-11-21 12:08:32 +02:00
}
} ) ;
} ) ;
return updatedClientList ;
} ;
2020-12-05 21:06:28 +02:00
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 ;
} ;
2021-03-04 21:28:02 +02:00
GatewayServer . prototype . _generateClientListUpdateObject = async function ( room , categoryTitle = "UNKNOWN" ) {
2020-11-21 12:08:32 +02:00
const clientList = await this . _getSocketsInRoom ( room ) ;
return {
category : {
title : categoryTitle ,
_id : room
} ,
clientList
} ;
} ;
2020-11-16 21:16:25 +02:00
module . exports = GatewayServer ;