module.exports = class ESMWebSocketServer { constructor(bot) { this.ESMBot = bot; this.util = this.ESMBot.util; this.db = this.ESMBot.db; this.servers = {}; this.atob = require("atob"); this.locked = {}; } async initialize() { this.websocket = require('ws'); this.wss = new this.websocket.Server({ port: this.ESMBot.config.SOCKET_SERVER_PORT, verifyClient: this.verifyClient.bind(this) }); this.wss.on('connection', this.onConnection.bind(this)); this.wss.on("error", this.onError.bind(this)); this.servers = await this.db.initializeServers(); setInterval(() => { this.heartBeatThread(this.wss) }, 30000); this.ESMBot.logger.success(`ESMBot is now accepting connections from Arma Servers`); } heartBeatThread(wss) { wss.clients.forEach((ws) => { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(() => {}); }); } async onHeartbeatCheck() { this.isAlive = true; } async verifyClient(info, cb) { try { let auth = /basic (.+)/i.exec(info.req.headers.authorization); if (auth == null) { return cb(false, 401, "Unauthorized"); } let token = /(.+):(.+)/i.exec(this.atob(auth[1])); if (token == null) { return cb(false, 401, "Unauthorized"); } let server = await this.db.authenticateServer(token[2]); if (this.util.isEmpty(server)) { return cb(false, 401, "Unauthorized"); } cb(true); } catch (err) { this.ESMBot.logger.error(err); cb(false, 401, "Unauthorized"); } } onError(error) { if (error.code !== "ECONNRESET") { this.ESMBot.logger.error(error); } } async onConnection(ws, req) { try { let auth = /basic (.+)/i.exec(req.headers.authorization); if (auth == null) { return ws.close(4000); } let token = /.+:(.+)/i.exec(this.atob(auth[1])); if (token == null) { return ws.close(4001); } let server = await this.db.authenticateServer(token[1]); if (this.util.isEmpty(server)) { return ws.close(4001); } if (await this.db.isGuildDeactivated(server.community_id)) { return ws.close(4003); } this.servers[server.id] = ws; this.lockServer(server.id); ws.id = server.id; ws.ip = req.headers["x-real-ip"] || req.connection.remoteAddress; ws.isAlive = true; ws.on('message', (message) => this.onMessage(ws, message)); ws.on('close', (close) => this.onClose(ws, close)); ws.on('error', this.onError.bind(this)); ws.on('unexpected-response', this.onError.bind(this)); ws.on('pong', this.onHeartbeatCheck); if (await this.db.allowReconnectMessages(ws.id)) { this.ESMBot.send(await this.db.getLoggingChannel(ws.id), `\`${ws.id}\` has connected`); } } catch (err) { this.ESMBot.logger.error(err); } } async onClose(ws, close) { try { if (await this.db.allowReconnectMessages(ws.id)) { this.ESMBot.send(await this.db.getLoggingChannel(ws.id), `\`${ws.id}\` has disconnected`); } if (!this.servers.hasOwnProperty(ws.id)) return false; this.servers[ws.id] = null; } catch (err) { this.ESMBot.logger.error(err); } } async onMessage(ws, message) { try { message = JSON.parse(message); if (message.ignore) return; if (message.returned) { return ws.close(4002, "Unsupported DLL Version"); } if (!this.servers.hasOwnProperty(ws.id)) { return this.ESMBot.logger.warn(`'${ws.id}' is not a valid serverID\nMessage: ${JSON.stringify(message)}`); } if (this.ESMBot.config.DEBUG) { console.log(`RECEIVING:\n${this.ESMBot.inspect(message.parameters.length === 1 ? message.parameters[0] : message.parameters)}`); } let commandName = message.command; if (message.commandID) { let info = await this.db.getCommand(message.commandID); if (message.error) { await this.resetCooldownForCommand(this.db.r, info, this.ESMBot.util.getCommunityID(ws.id), ws.id); return this.ESMBot.send(info.channel, `${info.author.tag}, ${message.error}`); } if (!this.ESMBot.util.isEmpty(message.parameters) && message.parameters[0].error) { await this.resetCooldownForCommand(this.db.r, info, this.ESMBot.util.getCommunityID(ws.id), ws.id); return this.ESMBot.send(info.channel, `${message.parameters[0].error}`); } commandName = info.command; } if (this.util.isEmpty(commandName)) return; let command = new(this.ESMBot.commands[commandName])(this.ESMBot); command.id = message.commandID || ""; command.params = message.parameters.length === 1 ? message.parameters[0] : message.parameters; command.serverID = ws.id; command.exec(); } catch (err) { this.ESMBot.logger.error(err); } } isServerOnline(serverID) { try { if (!this.servers.hasOwnProperty(serverID)) return false; let ws = this.servers[serverID]; if (ws == null) return false; if (ws.isAlive !== true) { ws.terminate(); return false; } return ws.isAlive && ws.readyState === this.websocket.OPEN; } catch (err) { this.ESMBot.logger.error(err); return false; } } lockServer(serverID) { this.locked[serverID] = true; return true; } unlockServer(serverID) { if (!this.locked.hasOwnProperty(serverID)) return; delete this.locked[serverID]; } isServerLocked(serverID) { if (!this.locked.hasOwnProperty(serverID)) return false; return this.locked.hasOwnProperty(serverID); } async send(info) { let dllPackage = { command: info.command, commandID: info.id, authorInfo: [ info.author.tag, info.author.id ], parameters: info.parameters }; this.ESMBot.logger.info(JSON.stringify({ parameters: info.parameters, sender: this.util.isEmpty(info.author.tag) ? "SYSTEM" : info.author.tag, command: info.command, server_id: info.server_id }, null, 2)); this.servers[info.server_id].send(JSON.stringify(dllPackage)); } async removeServer(serverID) { await this.disconnect(serverID); delete this.servers[serverID]; } async disconnect(serverID) { if (this.isServerOnline(serverID)) { this.servers[serverID].terminate(); } } // I don't care anymore async resetCooldownForCommand(r, info, communityID, serverID) { let userID = info.author.id; // Get the user, we need the steam UID let user = await r.table("users").get(userID).run(); // Create the user in case they aren't? if (user == null) { user = _.cloneDeep({ id: "", steam_uid: "", preferences: {} }); user.id = userID; await r.table("users").insert(user).run(); } // Go and delete any cooldowns let ret = await r.table("cooldowns").filter((cooldown) => { return cooldown("community_id").eq(communityID || null).and( cooldown("command_name").eq(info.command) ).and( cooldown("server_id").eq(serverID || null).default(true) ).and( cooldown("steam_uid").eq(user.steam_uid || null).or(cooldown("user_id").eq(user.id)) ); }).delete().run(); return ret.errors === 0; } }