From 5a4787f38209a513ee28a39004d8c488d56a1f4e Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 26 Dec 2023 19:11:17 +0100 Subject: [PATCH] refactor: implemented core/rcon.js from PR 267 --- config.json | 3 - core/rcon.js | 936 ++++++++------------ squad-server/index.js | 5 +- squad-server/rcon.js | 2 - squad-server/templates/config-template.json | 3 - 5 files changed, 376 insertions(+), 573 deletions(-) diff --git a/config.json b/config.json index 47f77f4..d0dbd46 100644 --- a/config.json +++ b/config.json @@ -5,9 +5,6 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", - "rconPassThrough": true, - "rconPassThroughPort": 8124, - "dumpRconResponsesToFile": false, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { diff --git a/core/rcon.js b/core/rcon.js index 1cbe73a..af6b899 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -1,613 +1,427 @@ -/* eslint-disable */ -import { EventEmitter } from 'node:events'; -import net from 'node:net'; +import EventEmitter from 'events'; +import net from 'net'; +import util from 'util'; + import Logger from './logger.js'; -import fs from 'fs'; -import path from 'path'; -const RCON_LOG_FILEPATH = 'RCON_RECEIVED_MESSAGES.log'; + +const SERVERDATA_EXECCOMMAND = 0x02; +const SERVERDATA_RESPONSE_VALUE = 0x00; +const SERVERDATA_AUTH = 0x03; +const SERVERDATA_AUTH_RESPONSE = 0x02; +const SERVERDATA_CHAT_VALUE = 0x01; + +const MID_PACKET_ID = 0x01; +const END_PACKET_ID = 0x02; + export default class Rcon extends EventEmitter { constructor(options = {}) { super(); + + // store config for (const option of ['host', 'port', 'password']) if (!(option in options)) throw new Error(`${option} must be specified.`); + this.host = options.host; this.port = options.port; this.password = options.password; - this.client = null; - this.stream = new Buffer.alloc(0); - this.type = { auth: 0x03, command: 0x02, response: 0x00, server: 0x01 }; - this.soh = { size: 7, id: 0, type: this.type.response, body: '' }; - this.responseString = { id: 0, body: '' }; + this.autoReconnectDelay = options.autoReconnectDelay || 5000; + + // bind methods + this.connect = this.connect.bind(this); // we bind this as we call it on the auto reconnect timeout + this.onPacket = this.onPacket.bind(this); + this.onClose = this.onClose.bind(this); + this.onError = this.onError.bind(this); + this.decodeData = this.decodeData.bind(this); + this.encodePacket = this.encodePacket.bind(this); + + // setup socket + this.client = new net.Socket(); + this.client.on('data', this.decodeData); + this.client.on('close', this.onClose); + this.client.on('error', this.onError); + + // constants + this.maximumPacketSize = 4096; + + // internal variables this.connected = false; this.autoReconnect = false; - this.autoReconnectDelay = options.autoReconnectDelay || 1000; - this.connectionRetry; - this.msgIdLow = 6; - this.msgIdHigh = 16; - this.specialId = 19; - this.msgId = this.msgIdLow; - this.passThrough = options.passThrough ? true : false; - this.passThroughPort = options.passThroughPort || 8124; - this.passThroughTimeOut = options.passThroughTimeOut || 60000; - this.passThroughMaxClients = 1; //options.passThroughMaxClients || 10; - this.passThroughChallenge = options.passThroughChallenge || options.password; - this.dumpRconResponsesToFile = options.dumpRconResponsesToFile || false; - this.rconClients = {}; - for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null; - this.ptServer = null; + this.autoReconnectTimeout = null; - this.steamIndex = { '76561198799344716': '00026e21ce3d43c792613bdbb6dec1ba' }; // example dtata - this.eosIndex = { '00026e21ce3d43c792613bdbb6dec1ba': '76561198799344716' }; // example dtata + this.incomingData = Buffer.from([]); + this.incomingResponse = []; + this.responseCallbackQueue = []; + // Used For tracking Callbacks + this.callbackIds = []; + this.count = 1; + this.loggedin = false; + } - this.rotateLogFile(RCON_LOG_FILEPATH); - } - processChatPacket(decodedPacket) { - console.log(decodedPacket.body); - } // - async connect() { - return new Promise((resolve, reject) => { - if (this.client && this.connected && !this.client.destroyed) - return reject(new Error('Rcon.connect() Rcon already connected.')); - this.removeAllListeners('server'); - this.removeAllListeners('auth'); - this.on('server', (pkt) => this.processChatPacket(pkt)); - this.once('auth', () => { - Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`); - clearTimeout(this.connectionRetry); - this.connected = true; - if (this.passThrough) this.createServer(); - resolve(); - }); - Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`); - this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); - this.autoReconnect = true; - this.client = net - .createConnection({ port: this.port, host: this.host }, () => this.sendAuth()) - .on('data', (data) => this.onData(data)) - .on('end', () => this.onClose()) - .on('error', () => this.onNetError()); - }).catch((error) => { - Logger.verbose('RCON', 1, `Rcon.connect() ${error}`); - }); - } - async disconnect() { - return new Promise((resolve, reject) => { - Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`); - clearTimeout(this.connectionRetry); - this.removeAllListeners('server'); - this.removeAllListeners('auth'); - this.autoReconnect = false; - this.client.end(); - this.connected = false; - this.closeServer(); - resolve(); - }).catch((error) => { - Logger.verbose('RCON', 1, `Rcon.disconnect() ${error}`); - }); - } - async execute(body) { - let steamID = body.match(/\d{17}/); - if (steamID) { - steamID = steamID[0]; - body = body.replace(/\d{17}/, this.steamIndex[steamID]); - } - - return new Promise((resolve, reject) => { - if (!this.connected) return reject(new Error('Rcon not connected.')); - if (!this.client.writable) return reject(new Error('Unable to write to node:net socket')); - const string = String(body); - const length = Buffer.from(string).length; - if (length > 4154) Logger.verbose('RCON', 1, `Error occurred. Oversize, "${length}" > 4154`); - else { - const outputData = (data) => { - clearTimeout(timeOut); - resolve(data); - }; - const timedOut = () => { - console.warn('MISSED', listenerId); - this.removeListener(listenerId, outputData); - return reject(new Error(`Rcon response timed out`)); - }; - if (this.msgId > this.msgIdHigh - 2) this.msgId = this.msgIdLow; - const listenerId = `response${this.msgId}`; - const timeOut = setTimeout(timedOut, 10000); - this.once(listenerId, outputData); - this.send(string, this.msgId); - this.msgId++; - } - }).catch((error) => { - Logger.verbose('RCON', 1, `Rcon.execute() ${error}`); - }); - } - sendAuth() { - Logger.verbose('RCON', 1, `Sending Token to: ${this.host}:${this.port}`); - this.client.write(this.encode(this.type.auth, 0, this.password).toString('binary'), 'binary'); //2147483647 - } - send(body, id = 99) { - this.write(this.type.command, id, body); - this.write(this.type.command, id + 2); - } - write(type, id, body) { + onPacket(decodedPacket) { + // the logic in this method simply splits data sent via the data event into packets regardless of how they're + // distributed in the event calls Logger.verbose( 'RCON', 2, - `Writing packet with type "${type}", id "${id}" and body "${body || ''}"` + `Processing decoded packet: ${this.decodedPacketToString(decodedPacket)}` ); - this.client.write(this.encode(type, id, body).toString('binary'), 'binary'); - } - encode(type, id, body = '') { - const size = Buffer.byteLength(body) + 14; - const buffer = new Buffer.alloc(size); - buffer.writeInt32LE(size - 4, 0); - buffer.writeInt32LE(id, 4); - buffer.writeInt32LE(type, 8); - buffer.write(body, 12, size - 2, 'utf8'); - buffer.writeInt16LE(0, size - 2); - return buffer; - } - onData(data) { - Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`); - this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength); - while (this.stream.byteLength >= 7) { - const packet = this.decode(); - if (!packet) break; - else + + switch (decodedPacket.type) { + case SERVERDATA_RESPONSE_VALUE: + case SERVERDATA_AUTH_RESPONSE: + switch (decodedPacket.id) { + case MID_PACKET_ID: + this.incomingResponse.push(decodedPacket); + + break; + case END_PACKET_ID: + this.callbackIds = this.callbackIds.filter((p) => p.id !== decodedPacket.count); + + this.responseCallbackQueue.shift()( + this.incomingResponse.map((packet) => packet.body).join() + ); + this.incomingResponse = []; + + break; + default: + Logger.verbose( + 'RCON', + 1, + `Unknown packet ID ${decodedPacket.id} in: ${this.decodedPacketToString( + decodedPacket + )}` + ); + this.onClose('Unknown Packet'); + } + break; + + case SERVERDATA_CHAT_VALUE: + this.processChatPacket(decodedPacket); + break; + + default: Logger.verbose( 'RCON', - 3, - `Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}` + 1, + `Unknown packet type ${decodedPacket.type} in: ${this.decodedPacketToString( + decodedPacket + )}` ); - this.appendToFile(RCON_LOG_FILEPATH, packet.body); + this.onClose('Unknown Packet'); + } + } - if (packet.id > this.msgIdHigh) this.emit(`responseForward_1`, packet); - else if (packet.type === this.type.response) this.onResponse(packet); - else if (packet.type === this.type.server) this.onServer(packet); - else if (packet.type === this.type.command) this.emit('auth'); - } - } - onServer(packet) { - this.emit('server', packet); - for (const client in this.rconClients) - if (this.rconClients[client]) { - this.emit(`serverForward_${this.rconClients[client].rconIdClient}`, packet.body); + decodeData(data) { + Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`); + + this.incomingData = Buffer.concat([this.incomingData, data]); + + while (this.incomingData.byteLength >= 4) { + const size = this.incomingData.readInt32LE(0); + const packetSize = size + 4; + + if (this.incomingData.byteLength < packetSize) { + Logger.verbose( + 'RCON', + 4, + `Waiting for more data... Have: ${this.incomingData.byteLength} Expected: ${packetSize}` + ); + break; } - } - decode() { - if ( - this.stream[0] === 0 && - this.stream[1] === 1 && - this.stream[2] === 0 && - this.stream[3] === 0 && - this.stream[4] === 0 && - this.stream[5] === 0 && - this.stream[6] === 0 - ) { - this.stream = this.stream.subarray(7); - return this.soh; - } - const bufSize = this.stream.readInt32LE(0); - if (bufSize > 8192 || bufSize < 10) return this.badPacket(); - else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) { - const bufId = this.stream.readInt32LE(4); - const bufType = this.stream.readInt32LE(8); + const packet = this.incomingData.slice(0, packetSize); + + Logger.verbose('RCON', 4, `Processing packet: ${this.bufToHexString(packet)}`); + const decodedPacket = this.decodePacket(packet); + + const matchCount = this.callbackIds.filter((d) => d.id === decodedPacket.count); + if ( - this.stream[bufSize + 2] !== 0 || - this.stream[bufSize + 3] !== 0 || - bufId < 0 || - bufType < 0 || - bufType > 5 - ) - return this.badPacket(); - else { - const response = { - size: bufSize, - id: bufId, - type: bufType, - body: this.stream.toString('utf8', 12, bufSize + 2) - }; - this.stream = this.stream.subarray(bufSize + 4); - if ( - response.body === '' && - this.stream[0] === 0 && - this.stream[1] === 1 && - this.stream[2] === 0 && - this.stream[3] === 0 && - this.stream[4] === 0 && - this.stream[5] === 0 && - this.stream[6] === 0 - ) { - this.stream = this.stream.subarray(7); - response.body = ''; - } - return response; + matchCount.length > 0 || + [SERVERDATA_AUTH_RESPONSE, SERVERDATA_CHAT_VALUE].includes(decodedPacket.type) + ) { + this.onPacket(decodedPacket); + this.incomingData = this.incomingData.slice(packetSize); + continue; } - } else return null; + // The packet following an empty packet will report to be 10 long (14 including the size header bytes), but in + // it should report 17 long (21 including the size header bytes). Therefore, if the packet is 10 in size + // and there's enough data for it to be a longer packet then we need to probe to check it's this broken packet. + const probePacketSize = 21; + + if (size === 10 && this.incomingData.byteLength >= 21) { + // copy the section of the incoming data of interest + const probeBuf = this.incomingData.slice(0, probePacketSize); + // decode it + const decodedProbePacket = this.decodePacket(probeBuf); + // check whether body matches + if (decodedProbePacket.body === '\x00\x00\x00\x01\x00\x00\x00') { + // it does so it's the broken packet + // remove the broken packet from the incoming data + this.incomingData = this.incomingData.slice(probePacketSize); + Logger.verbose('RCON', 4, `Ignoring some data: ${this.bufToHexString(probeBuf)}`); + continue; + } + } + + // We should only get this far into the loop when we are done processing packets from this onData event. + break; + } } - onResponse(packet) { - if (packet.body === '') { - this.emit(`response${this.responseString.id - 2}`, this.responseString.body); - this.responseString.body = ''; - } else if (!packet.body.includes('')) { - this.responseString.body = this.responseString.body + packet.body; - this.responseString.id = packet.id; - } else this.badPacket(); + + decodePacket(packet) { + return { + size: packet.readUInt32LE(0), + id: packet.readUInt8(4), + count: packet.readUInt16LE(6), + type: packet.readUInt32LE(8), + body: packet.toString('utf8', 12, packet.byteLength - 2) + }; } - badPacket() { + + processChatPacket(decodedPacket) {} + + onClose(hadError) { + this.connected = false; + this.loggedin = false; Logger.verbose( 'RCON', 1, - `Bad packet, clearing: ${this.bufToHexString(this.stream)} Pending string: ${ - this.responseString - }` + `Socket closed ${hadError ? 'with' : 'without'} an error. ${hadError}` ); - this.stream = Buffer.alloc(0); - this.responseString.body = ''; - return null; - } - onClose() { - Logger.verbose('RCON', 1, `Socket closed`); - this.cleanUp(); - } - onNetError(error) { - Logger.verbose('RCON', 1, `node:net error:`, error); - this.emit('RCON_ERROR', error); - this.cleanUp(); - } - cleanUp() { - this.closeServer(); - this.connected = false; - this.removeAllListeners(); - clearTimeout(this.connectionRetry); + + // Cleanup all local state onClose + if (this.incomingData.length > 0) { + Logger.verbose('RCON', 2, `Clearing Buffered Data`); + this.incomingData = Buffer.from([]); + } + if (this.incomingResponse.length > 0) { + Logger.verbose('RCON', 2, `Clearing Buffered Response Data`); + this.incomingResponse = []; + } + if (this.responseCallbackQueue.length > 0) { + Logger.verbose('RCON', 2, `Clearing Pending Callbacks`); + + // Cleanup Pending Callbacks; We should maybe retry these on next connection + // However, depending on the reason we got disconnected it may be a while. + // IE, Squad server crash, Squad server shutdown for multiple minutes. + + while (this.responseCallbackQueue.length > 0) { + this.responseCallbackQueue.shift()(new Error('RCON DISCONNECTED')); + } + this.callbackIds = []; + } + if (this.autoReconnect) { - Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting`); - this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); + Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting.`); + setTimeout(this.connect, this.autoReconnectDelay); } } - createServer() { - this.ptServer = net.createServer((client) => this.onNewClient(client)); - this.ptServer.maxConnections = this.passThroughMaxClients; - this.ptServer.on('error', (error) => this.onSerErr(error)); - this.ptServer.on('drop', () => - Logger.verbose( - 'RCON', - 1, - `Pass-through Server: Max Clients Reached (${this.passThroughMaxClients}) rejecting new connection` - ) - ); - this.ptServer.listen(this.passThroughPort, () => - Logger.verbose('RCON', 1, `Pass-through Server: Listening on port ${this.passThroughPort}`) - ); - } - closeServer() { - for (const client in this.rconClients) - if (this.rconClients[client]) this.rconClients[client].end(); - if (!this.ptServer) return; - this.ptServer.close(() => this.onServerClose()); - } - onServerClose() { - if (!this.ptServer) return; - this.ptServer.removeAllListeners(); - this.ptServer = null; - Logger.verbose('RCON', 1, `Pass-through Server: Closed`); - } - onNewClient(client) { - client.setTimeout(this.passThroughTimeOut); - client.on('end', () => this.onClientEnd(client)); - client.on('error', () => this.onClientEnd(client)); - client.on('timeout', () => this.onClientTimeOut(client)); - client.on('data', (data) => this.onClientData(client, data)); - Logger.verbose('RCON', 1, `Pass-through Server: Client connecting`); - } - onSerErr(error) { - this.closeServer(); - Logger.verbose('RCON', 1, `Pass-through Server: ${error}`); - } - onClientEnd(client) { - if (!client.rconIdClient) return; - this.removeAllListeners(`serverForward_${client.rconIdClient}`); - this.removeAllListeners(`responseForward_${client.rconIdClient}`); - this.rconClients[`${client.rconIdClient}`] = null; - Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`); - } - onClientTimeOut(client) { - client.end(); - Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Timed Out`); - } - onClientData(client, data) { - if (!client.rconStream) client.rconStream = new Buffer.alloc(0); - client.rconStream = Buffer.concat( - [client.rconStream, data], - client.rconStream.byteLength + data.byteLength - ); - while (client.rconStream.byteLength >= 4) { - const packet = this.decodeClient(client); - if (!packet) break; - if (!client.rconHasAuthed) this.authClient(client, packet); - else { - if (!client.rconWheel || client.rconWheel > 20) client.rconWheel = 0; - else client.rconWheel++; - client.rconIdQueueNEW[`${client.rconWheel}`] = packet.id; + onError(err) { + Logger.verbose('RCON', 1, `Socket had error:`, err); + this.emit('RCON_ERROR', err); + } - const encoded = this.encode( - packet.type, - this.specialId + client.rconWheel, - this.steamToEosClient(packet.body) - ); //////////////////////////////////////////////// - this.client.write(encoded.toString('binary'), 'binary'); - // this.client.write(this.encode(packet.type, this.specialId * client.rconIdClient).toString("binary"), "binary") - } - } - } - decodeClient(client) { - const bufSize = client.rconStream.readInt32LE(0); - if (bufSize <= client.rconStream.byteLength - 4) { - const response = { - size: bufSize, - id: client.rconStream.readInt32LE(4), - type: client.rconStream.readInt32LE(8), - body: client.rconStream.toString('utf8', 12, bufSize + 2) - }; - client.rconStream = client.rconStream.subarray(bufSize + 4); - return response; - } else return null; - } - authClient(client, packet) { - if (packet.body !== this.passThroughChallenge) { - client.end(); - Logger.verbose('RCON', 1, `Pass-through Server: Client [Rejected] Password not matched`); - } else { - client.rconHasAuthed = true; - client.rconIdQueueNEW = {}; - for (let i = 1; i <= this.passThroughMaxClients; i++) { - if (this.rconClients[`${i}`] === null) { - client.rconIdClient = i; - this.rconClients[`${i}`] = client; - break; + connect() { + return new Promise((resolve, reject) => { + Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`); + + const onConnect = async () => { + this.client.removeListener('error', onError); + this.connected = true; + + Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`); + + try { + // connected successfully, now try auth... + await this.write(SERVERDATA_AUTH, this.password); + + // connected and authed successfully + this.autoReconnect = true; + resolve(); + } catch (err) { + reject(err); } + }; + + const onError = (err) => { + this.client.removeListener('connect', onConnect); + + Logger.verbose('RCON', 1, `Failed to connect to: ${this.host}:${this.port}`, err); + + reject(err); + }; + + this.client.once('connect', onConnect); + this.client.once('error', onError); + + this.client.connect(this.port, this.host); + }); + } + + disconnect() { + return new Promise((resolve, reject) => { + Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`); + + const onClose = () => { + this.client.removeListener('error', onError); + + Logger.verbose('RCON', 1, `Disconnected from: ${this.host}:${this.port}`); + + resolve(); + }; + + const onError = (err) => { + this.client.removeListener('close', onClose); + + Logger.verbose('RCON', 1, `Failed to disconnect from: ${this.host}:${this.port}`, err); + + reject(err); + }; + + this.client.once('close', onClose); + this.client.once('error', onError); + + // prevent any auto reconnection happening + this.autoReconnect = false; + // clear the timeout just in case the socket closed and then we DCed + clearTimeout(this.autoReconnectTimeout); + + this.client.end(); + }); + } + + execute(command) { + return this.write(SERVERDATA_EXECCOMMAND, command); + } + + write(type, body) { + return new Promise((resolve, reject) => { + if (!this.connected) { + reject(new Error('Not connected.')); + return; } - this.on(`serverForward_${client.rconIdClient}`, (body) => - client.write(this.encode(1, 0, this.eosToSteam(body)).toString('binary'), 'binary') - ); - this.on(`responseForward_${client.rconIdClient}`, (packet) => this.onForward(client, packet)); - client.write(this.encode(0, packet.id)); - client.write(this.encode(2, packet.id)); - Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Connected`); - } - } - onForward(client, packet) { - if (packet.body !== '' && packet.body !== '') { - const int = packet.id - this.specialId; - //console.log(client.rconIdQueueNEW);////////////////////////////////////////////////////////////////////////////////////////// + if (!this.client.writable) { + reject(new Error('Unable to write to socket.')); + return; + } - client.write( - this.encode(packet.type, client.rconIdQueueNEW[int], this.eosToSteam(packet.body)).toString( - 'binary' - ), - 'binary' + if (!this.loggedin && type !== SERVERDATA_AUTH) { + reject(new Error('RCON not Logged in')); + return; + } + + Logger.verbose('RCON', 2, `Writing packet with type "${type}" and body "${body}".`); + + const encodedPacket = this.encodePacket( + type, + type !== SERVERDATA_AUTH ? MID_PACKET_ID : END_PACKET_ID, + body ); - } else if (packet.body != '') { - const int = packet.id - this.specialId; - client.write(this.encode(0, client.rconIdQueueNEW[int]).toString('binary'), 'binary'); - client.write(this.encodeSpecial(client.rconIdQueueNEW[int]).toString('binary'), 'binary'); - } + + const encodedEmptyPacket = this.encodePacket(type, END_PACKET_ID, ''); + + if (this.maximumPacketSize < encodedPacket.length) { + reject(new Error('Packet too long.')); + return; + } + + const onError = (err) => { + Logger.verbose('RCON', 1, 'Error occurred. Wiping response action queue.', err); + this.responseCallbackQueue = []; + reject(err); + }; + + // the auth packet also sends a normal response, so we add an extra empty action to ignore it + + if (type === SERVERDATA_AUTH) { + this.callbackIds.push({ id: this.count, cmd: body }); + this.responseCallbackQueue.push(() => {}); + this.responseCallbackQueue.push((decodedPacket) => { + this.client.removeListener('error', onError); + if (decodedPacket.id === -1) { + Logger.verbose('RCON', 1, 'Authentication failed.'); + reject(new Error('Authentication failed.')); + } else { + Logger.verbose('RCON', 1, 'Authentication succeeded.'); + this.loggedin = true; + resolve(); + } + }); + } else { + this.callbackIds.push({ id: this.count, cmd: body }); + this.responseCallbackQueue.push((response) => { + this.client.removeListener('error', onError); + + if (response instanceof Error) { + // Called from onClose() + reject(response); + } else { + Logger.verbose( + 'RCON', + 2, + `Returning complete response: ${response.replace(/\r\n|\r|\n/g, '\\n')}` + ); + + resolve(response); + } + }); + } + + this.client.once('error', onError); + + if (this.count + 1 > 65535) { + this.count = 1; + } + + Logger.verbose('RCON', 4, `Sending packet: ${this.bufToHexString(encodedPacket)}`); + this.client.write(encodedPacket); + + if (type !== SERVERDATA_AUTH) { + Logger.verbose( + 'RCON', + 4, + `Sending empty packet: ${this.bufToHexString(encodedEmptyPacket)}` + ); + this.client.write(encodedEmptyPacket); + this.count++; + } + }); } - encodeSpecial(id) { - const buffer = new Buffer.alloc(21); - buffer.writeInt32LE(10, 0); - buffer.writeInt32LE(id, 4); - buffer.writeInt32LE(0, 8); - buffer.writeInt32LE(1, 15); - return buffer; + + encodePacket(type, id, body, encoding = 'utf8') { + const size = Buffer.byteLength(body) + 14; + const buf = Buffer.alloc(size); + + buf.writeUInt32LE(size - 4, 0); + buf.writeUInt8(id, 4); + buf.writeUInt8(0, 5); + buf.writeUInt16LE(this.count, 6); + buf.writeUInt32LE(type, 8); + buf.write(body, 12, size - 2, encoding); + buf.writeUInt16LE(0, size - 2); + + return buf; } + bufToHexString(buf) { return buf.toString('hex').match(/../g).join(' '); } + + decodedPacketToString(decodedPacket) { + return util.inspect(decodedPacket, { breakLength: Infinity }); + } + async warn(steamID, message) { - this.execute(`AdminWarn "${steamID}" ${message}`); + await this.execute(`AdminWarn "${steamID}" ${message}`); } + async kick(steamID, reason) { - this.execute(`AdminKick "${steamID}" ${reason}`); + await this.execute(`AdminKick "${steamID}" ${reason}`); } + async forceTeamChange(steamID) { - this.execute(`AdminForceTeamChange "${steamID}"`); - } - - addIds(steamId, eosId) { - this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; - this.eosIndex[eosId] = steamId; - } - - removeIds(eosId) { - // clean up ids on leave - } - - steamToEosClient(body) { - //assume client does not send more than 1 steamId per msg - const m = body.match(/[0-9]{17}/); - if (m && m[1] in this.steamIndex) return body.replaceAll(`${m[0]}`, this.steamIndex[m[0]]); - return body; - } - - eosToSteam(body) { - //split body to lines for matching (1 steamId per line) - const lines = body.split('\n'); - const nBody = []; - for (let line of lines) nBody.push(this.matchRcon(line)); - return nBody.join('\n'); - } - - matchRcon(line) { - for (const r of defs) { - const match = line.match(r.regex); - if (match && (match.groups.eosId in this.eosIndex || match.groups.steamId)) { - return r.rep( - line, - match.groups.steamId || this.eosIndex[match.groups.eosId], - match.groups.eosId - ); - } - } - return line; - } - - appendToFile(filePath, content) { - if (!this.dumpRconResponsesToFile) return; - const dir = path.dirname(filePath); - - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.appendFile(filePath, content + '\n', (err) => { - if (err) throw err; - }); - } - rotateLogFile(logFile) { - if (!this.dumpRconResponsesToFile) return; - if (fs.existsSync(logFile)) { - const ext = path.extname(logFile); - const base = path.basename(logFile, ext); - const dir = path.dirname(logFile); - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const newFile = path.join(dir, `${base}_${timestamp}${ext}`); - - fs.renameSync(logFile, newFile); - } + await this.execute(`AdminForceTeamChange "${steamID}"`); } } -const defs = [ - //strict matching to avoid 'name as steamId errors' - { - regex: - /^ID: [0-9]+ \| Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, - rep: (line, steamId, eosId) => { - return line.replace( - /\| Online IDs: EOS: [\w\d]{32} steam: \d{17} \|/, - `| SteamID: ${steamId} |` - ); - } - }, - { - regex: - /^ID: (?[0-9]+) \| Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, - `SteamID: ${steamId}` - ); - } - }, - { - regex: - /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Online IDs: EOS: (?[\d\w]{32}) steam: (?\d{17})/, - rep: (line, steamId, eosId) => { - console.log(line, steamId, eosId); - const ret = line.replace( - /\| Creator Online IDs: EOS: [\w\d]{32} steam: \d{17}/, - `| Creator Steam ID: ${steamId}` - ); - return ret; - } - }, - { - regex: - /^Forced team change for player (?[0-9]+). \[Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})] (.+)/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, - `steamid=${steamId}` - ); - } - }, - - { - regex: /^Could not find player (?[0-9a-f]{32})/, - rep: (line, steamId, eosId) => { - return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`); - } - }, - - { - regex: - /^\[Chat(All|Team|Squad|Admin)] \[Online IDs:EOS: (?[\d\w]{32}) steam: (?\d{17})] (?.+) : (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace(/Online IDs:EOS: [\d\w]{32} steam: \d{17}/, `SteamID:${steamId}`); - } - }, - { - regex: - /^(?.+) \(Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d+)\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d+)/, - `Steam ID: ${steamId}` - ); - } - }, - { - regex: - /^Kicked player (?[0-9]+). \[Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})] (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, - `steamid=${steamId}` - ); - } - }, - - { - regex: /^ERROR: Unable to find player with name or id \((?[0-9a-f]{32})\)$/, - rep: (line, steamId, eosId) => { - return line.replace(`name or id (${eosId})`, `name or id (${steamId})`); - } - }, - { - regex: - /^\[Online I(d|D)s:EOS: (?[0-9a-f]{32}) steam: (?)\d{17}] (?.+) has (un)?possessed admin camera\./, - rep: (line, steamId, eosId) => { - return line.replace(/Online I(d|D)s:EOS: [\w\d]{32} steam: \d{17}/, `SteamID:${steamId}`); - } - } -]; - -//////////////////////////////////////////////////ALL BELOW IS FOR STANDALONE TESTING/RUNNING -// const Logger = { -// level: 1, -// verbose(type, lvl, msg, msg1 = "") { -// if (lvl > this.level) return; -// console.log(type, lvl, msg, msg1); -// }, -// }; - -// const squadJsStyle = async () => { -// const getCurrentMap = async () => { -// const response = await rcon.execute("ShowCurrentMap"); -// const match = response.match(/^Current level is (?.+), layer is (?.+)/); -// if (!match) { -// debugger -// } -// return [match.groups.level, match.groups.layer]; -// }; - -// const rcon = new Rcon({ port: "port", host: "ip", password: "password", passThrough: true, passThroughPort: "8124", passThroughTimeOut: 30000, passThroughChallenge: "password" }); // - -// try { -// await rcon.connect(); -// } catch (e) { -// console.warn(e); -// } - -// rcon.interval = setInterval(async () => { - -// try { -// const currentMap = await getCurrentMap(); -// console.log(currentMap); -// } catch (e) { -// console.warn(e); -// } -// }, 5000); -// }; - -// squadJsStyle(); diff --git a/squad-server/index.js b/squad-server/index.js index 85e7deb..c40cfd6 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -94,10 +94,7 @@ export default class SquadServer extends EventEmitter { host: this.options.rconHost || this.options.host, port: this.options.rconPort, password: this.options.rconPassword, - autoReconnectInterval: this.options.rconAutoReconnectInterval, - dumpRconResponsesToFile: this.options.dumpRconResponsesToFile, - passThroughPort: this.options.rconPassThroughPort, - passThrough: this.options.rconPassThrough + autoReconnectInterval: this.options.rconAutoReconnectInterval }); this.rcon.on('CHAT_MESSAGE', async (data) => { diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 6367633..0fa1240 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -144,8 +144,6 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - if (this.addIds) this.addIds(match[3], match[2]); - const data = match.groups; data.isLeader = data.isLeader === 'True'; data.squadID = data.squadID !== 'N/A' ? data.squadID : null; diff --git a/squad-server/templates/config-template.json b/squad-server/templates/config-template.json index be87345..f8f6458 100644 --- a/squad-server/templates/config-template.json +++ b/squad-server/templates/config-template.json @@ -5,9 +5,6 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", - "rconPassThrough": true, - "rconPassThroughPort": 8124, - "dumpRconResponsesToFile": false, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": {