mirror of
https://github.com/AsgardEternal/SquadJS.git
synced 2024-09-28 11:54:23 -05:00
refactor: implemented core/rcon.js from PR 267
This commit is contained in:
parent
2195006765
commit
5a4787f382
@ -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": {
|
||||
|
936
core/rcon.js
936
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: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\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: (?<Id>[0-9]+) \| Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17}) \| Since Disconnect: (?<SinceDc>.+) \| Name: (?<name>.+)$/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
|
||||
`SteamID: ${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^ID: (?<sqdId>[0-9]+) \| Name: (?<sqdName>.+) \| Size: (?<sqdSize>[0-9]) \| Locked: (?<locked>True|False) \| Creator Name: (?<creatorName>.+) \| Creator Online IDs: EOS: (?<eosID>[\d\w]{32}) steam: (?<steamId>\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 (?<id>[0-9]+). \[Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})] (.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
|
||||
`steamid=${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
regex: /^Could not find player (?<eosId>[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: (?<eosId>[\d\w]{32}) steam: (?<steamId>\d{17})] (?<name>.+) : (?<msg>.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(/Online IDs:EOS: [\d\w]{32} steam: \d{17}/, `SteamID:${steamId}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^(?<name>.+) \(Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d+)\) has created Squad (?<squadNum>[0-9]+) \(Squad Name: (?<squadName>.+)\) on (?<teamName>.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d+)/,
|
||||
`Steam ID: ${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^Kicked player (?<Id>[0-9]+). \[Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})] (?<name>.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
|
||||
`steamid=${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
regex: /^ERROR: Unable to find player with name or id \((?<eosId>[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: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>)\d{17}] (?<name>.+) 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 (?<level>.+), layer is (?<layer>.+)/);
|
||||
// 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();
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user