2020-11-16 21:16:25 +02:00
const User = require ( '../../../models/User' ) ;
const secret = require ( '../../../secret' ) ;
const config = require ( '../../../config' ) ;
2020-11-17 15:27:23 +02:00
const Category = require ( '../../../models/Category' ) ;
2020-11-26 11:42:27 +02:00
const RateLimiter = require ( './ratelimiter' ) ;
2020-11-16 21:16:25 +02:00
const jwt = require ( 'jsonwebtoken' ) ;
2020-11-17 15:27:23 +02:00
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 ) ;
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
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 : {
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 = 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 ;
}
default : {
this . _sendSystemMessage ( socket , 'That command does not exist.' , message . category ) ;
break ;
}
}
} ;
2020-11-16 21:16:25 +02:00
GatewayServer . prototype . authDisconnect = function ( socket , callback ) {
2020-11-16 22:08:23 +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 ) ;
callback ( new Error ( 'ERR_GATEWAY_AUTH_FAIL' ) ) ;
} ;
GatewayServer . prototype . eventSetup = function ( ) {
this . _gateway . use ( ( socket , callback ) => {
2020-11-16 22:08:23 +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 ;
2020-11-20 13:25:08 +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 ) ;
2020-11-21 13:29:20 +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 ( ) ;
} ) ;
} ) ;
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... ` ) ;
socket . emit ( 'hello' , {
gatewayStillNotConnectedTimeoutMS : config . gatewayStillNotConnectedTimeoutMS ,
resolvedUser : {
username : socket . user . username ,
_id : socket . user . _id
}
} ) ;
2020-11-16 21:16:25 +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
2020-12-05 21:06:28 +02:00
socket . on ( 'message' , async ( { category , content , nickAuthor , destUser } ) => {
2020-11-21 13:29:20 +02:00
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 ( ) ;
2020-11-20 21:35:43 +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 ] ;
2020-11-21 13:29:20 +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
2020-12-05 19:10:11 +02:00
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
}
}
}
2020-12-01 22:28:35 +02:00
if ( messageObject . content . startsWith ( this . _commandPrefix ) ) {
this . _processCommand ( socket , messageObject ) ;
return ;
}
2020-12-05 21:06:28 +02:00
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 ;
}
2020-11-20 20:15:20 +02:00
this . _gateway . in ( category . _id ) . emit ( 'message' , messageObject ) ;
2020-11-16 22:08:23 +02:00
} ) ;
2020-11-17 11:28:11 +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 ;
2020-11-20 13:25:08 +02:00
for ( const v of categories ) {
2020-11-21 13:29:20 +02:00
if ( ! v && ! ( typeof v === 'string' ) ) continue ;
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 category = await Category . findById ( v ) ;
if ( category && category . title && category . _id ) {
if ( ! socket . joinedCategories ) socket . joinedCategories = { } ;
2020-11-20 20:15:20 +02:00
if ( socket . joinedCategories [ v ] ) continue ;
socket . joinedCategories [ v ] = category . title ;
await socket . join ( v ) ;
2020-11-21 12:08:32 +02:00
console . log ( ` [*] [gateway] User ${ socket . user . username } subscribed to room ${ v } ( ${ category . title } ), sending updated user list to all members of that room... ` ) ;
2020-11-21 17:40:35 +02:00
const upd = await this . _generateClientListUpdateObject ( v , category . title ) ;
this . _gateway . in ( v ) . emit ( 'clientListUpdate' , upd ) ;
2020-11-17 15:27:23 +02:00
}
2020-11-16 22:08:23 +02:00
}
} ) ;
2020-11-21 12:08:32 +02:00
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 ) ;
2020-11-21 17:40:35 +02:00
const upd = await this . _generateClientListUpdateObject ( room , categoryTitle ) ;
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 ;
} ;
2020-11-21 12:08:32 +02:00
GatewayServer . prototype . _generateClientListUpdateObject = async function ( room , categoryTitle = 'UNKNOWN' ) {
const clientList = await this . _getSocketsInRoom ( room ) ;
return {
category : {
title : categoryTitle ,
_id : room
} ,
clientList
} ;
} ;
2020-11-16 21:16:25 +02:00
module . exports = GatewayServer ;