@ohd-tools/rcon
Version:
An RCON Interface for Operation: Harsh Doorstop
668 lines (667 loc) • 22 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OHD = void 0;
const events_1 = __importDefault(require("events"));
const net_1 = __importDefault(require("net"));
const utils_1 = require("@ohd-tools/utils");
const parser_1 = require("./parser");
const Variables_1 = require("./utils/Variables");
const Message_1 = require("./Message");
var PacketType;
(function (PacketType) {
PacketType[PacketType["COMMAND"] = 2] = "COMMAND";
PacketType[PacketType["AUTH"] = 3] = "AUTH";
PacketType[PacketType["RESPONSE_VALUE"] = 0] = "RESPONSE_VALUE";
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
PacketType[PacketType["RESPONSE_AUTH"] = 2] = "RESPONSE_AUTH";
})(PacketType || (PacketType = {}));
/**!
* node-rcon
* Copyright(c) 2012 Justin Li <j-li.net>
* MIT Licensed
* Stripped Down and modified by @bombitmanbomb
*/
class Rcon extends events_1.default.EventEmitter {
host;
port;
password;
outstandingData = null;
hasAuthed;
autoReconnect = true;
_tcpSocket;
constructor(host, port, password) {
super();
this.host = host;
this.port = port;
this.password = password;
this.hasAuthed = false;
events_1.default.EventEmitter.call(this);
}
_sendSocket(buf) {
this._tcpSocket.write(buf.toString('binary'), 'binary');
}
connect() {
try {
this._tcpSocket = net_1.default.createConnection(this.port, this.host);
this._tcpSocket.on('data', (data) => {
this._tcpSocketOnData(data);
});
this._tcpSocket.on('connect', () => {
this.socketOnConnect();
});
this._tcpSocket.on('error', (err) => {
this.emit('error', err);
});
this._tcpSocket.on('end', () => {
this.socketOnEnd();
});
}
catch (error) {
this.emit('error', error);
}
}
send(data, cmd, id = 0x0012d4a6) {
if (this.autoReconnect && !this.hasAuthed && cmd != PacketType.AUTH) {
// Needs a proper solution.
this.connect();
this.send(this.password, PacketType.AUTH);
return;
}
cmd = cmd || PacketType.COMMAND;
const length = Buffer.byteLength(data);
const sendBuf = Buffer.alloc(length + 14);
sendBuf.writeInt32LE(length + 10, 0);
sendBuf.writeInt32LE(id, 4);
sendBuf.writeInt32LE(cmd, 8);
sendBuf.write(data, 12);
sendBuf.writeInt16LE(0, length + 12);
this._sendSocket(sendBuf);
}
disconnect() {
if (this._tcpSocket)
this._tcpSocket.end();
}
setTimeout(timeout, callback) {
if (!this._tcpSocket)
return;
this._tcpSocket.setTimeout(timeout, () => {
this._tcpSocket.end();
if (callback)
callback();
});
}
_tcpSocketOnData(data) {
if (this.outstandingData != null) {
data = Buffer.concat(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[this.outstandingData, data], this.outstandingData.length + data.length);
this.outstandingData = null;
}
while (data.length >= 12) {
const len = data.readInt32LE(0); // Size of entire packet, not including the 4 byte length field
if (!len)
return; // No valid packet header, discard entire buffer
const packetLen = len + 4;
if (data.length < packetLen)
break; // Wait for full packet, TCP may have segmented it
const bodyLen = len - 10; // Subtract size of ID, type, and two mandatory trailing null bytes
if (bodyLen < 0) {
data = data.subarray(packetLen); // Length is too short, discard malformed packet
break;
}
const id = data.readInt32LE(4);
const type = data.readInt32LE(8);
if (id == -1)
return this.emit('error', new Error('Authentication failed'));
if (!this.hasAuthed && type == PacketType.RESPONSE_AUTH) {
this.hasAuthed = true;
this.emit('auth');
}
else if (type == PacketType.RESPONSE_VALUE) {
// Read just the body of the packet (truncate the last null byte)
// See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol for details
let str = data.toString('utf8', 12, 12 + bodyLen);
if (str.endsWith('\n')) {
// Emit the response without the newline.
str = str.substring(0, str.length - 1);
}
const response = {
size: data.readInt32LE(0),
id: data.readInt32LE(4),
type: data.readInt32LE(8),
body: str,
};
this.emit('response', response);
}
data = data.subarray(packetLen);
}
// Keep a reference to remaining data, since the buffer might be split within a packet
this.outstandingData = data;
}
socketOnConnect() {
this.emit('connect');
this.send(this.password, PacketType.AUTH);
}
socketOnEnd() {
this.emit('end');
this.hasAuthed = false;
}
}
/**
* Primary Interface Object for OHD servers.
*/
class OHD {
/**
* Rejects if an error occurs when connecting.
*/
onReady;
rconParser;
logParser;
/**
* Online Players
*/
players = new Map();
/**
* Players who have recently Left
*/
recentPlayers = [];
recentPlayerLimit = 10;
serverStatus = {};
messageID;
_conn;
_log;
_isAuthorized;
_responsePromiseQueue;
debug = false;
/**Reconnect to the client */
reconnect;
_events;
constructor(ip, port, password, options) {
Object.defineProperty(this, '_isAuthorized', {
enumerable: false,
value: false,
});
Object.defineProperty(this, 'messageID', { enumerable: false, value: 1 });
Object.defineProperty(this, '_events', {
enumerable: false,
value: new events_1.default.EventEmitter(),
});
Object.defineProperty(this, '_responsePromiseQueue', {
enumerable: false,
value: new Map(),
});
Object.defineProperty(this, '_updateServerStatus', {
enumerable: false,
value: this._updateServerStatus.bind(this),
});
Object.defineProperty(this, 'rconParser', {
enumerable: false,
value: new parser_1.RCONParser(this),
});
Object.defineProperty(this, 'logParser', {
enumerable: false,
value: new parser_1.LogParser(this),
});
Object.defineProperty(this, '_onResponse', {
enumerable: false,
value: this._onResponse.bind(this),
});
Object.defineProperty(this, '_updatePlayers', {
enumerable: false,
value: this._updatePlayers.bind(this),
});
Object.defineProperty(this, '_onError', {
enumerable: false,
value: this._onError.bind(this),
});
Object.defineProperty(this, '_onEnd', {
enumerable: false,
value: this.onEnd.bind(this),
});
Object.defineProperty(this, '_conn', {
enumerable: false,
value: new Rcon(ip, port, password),
});
const executor = (res, rej) => {
let handled = false;
this._conn.once('error', (err) => {
if (handled)
return;
handled = true;
rej(err);
});
this._conn.once('auth', () => {
if (handled)
return;
handled = true;
res(null);
});
this._conn.connect();
};
this._conn.autoReconnect =
options?.autoReconnect ?? this._conn.autoReconnect ?? true;
this._conn
.on('response', this._onResponse)
.on('error', this._onError)
.on('end', this.onEnd);
Object.defineProperty(this, 'onReady', {
enumerable: false,
value: new Promise(executor),
});
Object.defineProperty(this, 'reconnect', {
enumerable: false,
value: () => {
Object.defineProperty(this, 'onReady', {
enumerable: false,
value: new Promise(executor),
});
return this.onReady;
},
});
this.onReady
.then(() => {
this._events.emit('READY');
})
.catch((err) => {
this._events.emit('error', err);
});
if (!options?.disableAutoStatus) {
const getStatus = () => {
this.status().then((status) => {
if (status.Players != null)
this._updatePlayers(new Map(status.Players.map((player) => [player.id, player])));
this._updateServerStatus(status);
});
};
this.onReady
.then(async () => {
getStatus();
setInterval(getStatus, 8000);
//! Log Handler
if (options?.logParsing != null) {
const handleLogLine = (line) => {
const logParsed = this.logParser.parse(line);
if (logParsed == null)
return;
if (logParsed.type ===
'chat') {
const message = new Message_1.Message(this, logParsed.data);
this._events.emit('CHAT', message);
}
};
switch (options.logParsing.type) {
case 'tail':
this._log = new utils_1.Readers.TailLogReader(handleLogLine.bind(this),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options.logParsing.options);
await this._log.setup();
await this._log.watch();
break;
case 'ftp':
this._log = new utils_1.Readers.FTPLogReader(handleLogLine.bind(this),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options.logParsing.options);
await this._log.setup();
await this._log.watch();
break;
case 'sftp':
this._log = new utils_1.Readers.SFTPLogReader(handleLogLine.bind(this),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options.logParsing.options);
await this._log.setup();
await this._log.watch();
break;
}
}
})
.catch(() => {
//OOPS
});
}
this._events.on('error', () => {
//Handle Error. Hookable
});
}
on(event, cb) {
return this._events.on(event, cb);
}
removeListener(event, cb) {
return this._events.removeListener(event, cb);
}
_updateServerStatus(status) {
for (const prop in status) {
if (prop == 'Players')
continue;
Reflect.set(this.serverStatus, prop, Reflect.get(status, prop));
}
}
_updatePlayers(players) {
for (const [id] of this.players) {
if (!players.has(id)) {
const oldPlayer = this.players.get(id);
this._events.emit('PLAYER_LEFT', oldPlayer);
oldPlayer._events.emit('PLAYER_LEFT');
this.recentPlayers.push(oldPlayer);
this.players.delete(id);
while (this.recentPlayers.length > this.recentPlayerLimit) {
const p = this.recentPlayers.shift();
this._events.emit('PLAYER_DELETED', p);
p._events.emit('PLAYER_DELETED');
}
}
}
for (const [id, player] of players) {
if (!this.players.has(id)) {
this._events.emit('PLAYER_JOINED', player);
this.players.set(id, player);
}
}
}
/**
* Get the Server Status.
*/
status() {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
return this.send('status').then((status) => {
return {
...status.status,
Players: status.players,
};
});
}
/**
* Give the user Admin Access
*/
addAdmin(name) {
if (typeof name === 'number')
return this.addAdminById(name);
return this.send(`admin add "${name}"`);
}
/**
* Give the user Admin Access
*/
addAdminById(id) {
return this.send(`admin addId ${id}`);
}
/**
* Revoke the users Admin Access
*/
removeAdmin(name) {
if (typeof name === 'number')
return this.removeAdminById(name);
return this.send(`admin remove "${name}"`);
}
/**
* Revoke the users Admin Access
*/
removeAdminById(id) {
return this.send(`admin removeId ${id}`);
}
/**
* Add `amount` bots to the server.
*/
addBots(amount = 1) {
return this.send(`addBots ${amount}`);
}
/**
* Add a Named Bot to the server.
*/
addNamedBot(name = 'Chuck Norris') {
return this.send(`addNamedBot ${name}`);
}
/**
* Add bots to a specified Team
*/
addTeamBots(team, amount) {
return this.send(`addTeamBots ${team} ${amount}`);
}
/**
* Add bots to the Opfor Team
*/
addOpforBots(amount) {
return this.send(`addOpforBots ${amount}`);
}
/**
* Add bots to the Blufor Team
*/
addBluforBots(amount) {
return this.send(`addBluforBots ${amount}`);
}
/**
* Remove bots from the specified Team
*/
removeTeamBots(team, amount) {
return this.send(`removeTeamBots ${team} ${amount}`);
}
/**
* Remove bots from the Opfor Team
*/
removeOpforBots(amount) {
return this.send(`removeOpforBots ${amount}`);
}
/**
* Remove bots from the Blufor Team
*/
removeBluforBots(amount) {
return this.send(`removeBluforBots ${amount}`);
}
/**
* Remove all bots from the server.
*/
removeAllBots() {
return this.send('removeAllBots');
}
/**
* Kick a `Player` from the server by Username
*/
async kick(name, reason = 'You have been Kicked') {
if (typeof name === 'number')
return this.kickId(name, reason);
const kicked = await this.send(`kick "${name}" "${reason}"`);
if (kicked.success) {
const player = [...this.players.entries()].find((p) => p[1].name == name);
if (player != undefined) {
this._events.emit('PLAYER_KICKED', player[1], kicked);
player[1]._events.emit('PLAYER_KICKED', kicked);
}
}
return kicked;
}
/**
* Kick a `Player` from the server by PlayerID
*/
async kickId(id, reason = 'You have been Kicked') {
const kicked = await this.send(`kickId ${id} "${reason}"`);
if (kicked.success) {
const player = this.players.get(id);
if (player != undefined) {
this._events.emit('PLAYER_KICKED', player, kicked);
player._events.emit('PLAYER_KICKED', kicked);
}
}
return kicked;
}
/**
* Ban a `Player` from the server by Username.
*/
async ban(name,
/** Duration in Seconds*/ duration = 0, reason) {
if (typeof name === 'number')
return this.banId(name, duration, reason);
if (reason == null)
reason =
duration == 0
? 'You have been Permanently Banned!'
: `You have been Banned for ${duration} minutes!`;
const banned = await this.send(`ban "${name}" "${reason}" ${duration}`);
if (banned.success) {
const player = [...this.players.entries()].find((p) => p[1].name == name);
if (player != undefined) {
this._events.emit('PLAYER_BANNED', player[1], banned);
player[1]._events.emit('PLAYER_BANNED', banned);
}
}
return banned;
}
/**
* Ban a `Player` from the server by PlayerID.
*/
async banId(id,
/** Duration in Seconds*/ duration = 0, reason) {
if (reason == null)
reason =
duration == 0
? 'You have been Permanently Banned!'
: `You have been Banned for ${duration} minutes!`;
const banned = await this.send(`banId ${id} "${reason}" ${duration}`);
if (banned.success) {
const player = this.players.get(id);
if (player != undefined) {
this._events.emit('PLAYER_BANNED', player, banned);
player._events.emit('PLAYER_BANNED', banned);
}
}
return banned;
}
/**
* Create a new MapQuery Object to use with `serverTravel()`.
*/
createMapQuery(options) {
return new utils_1.MapQuery(options);
}
/**
* Force the team of a `Player` by Username.
*
* 1: Blufor
*
* 0: Opfor
*/
forceTeam(name, teamId) {
return this.send(`ForceTeam "${name}" ${teamId}`);
}
/**
* Force the team of a `Player` by PlayerID.
*
* 1: Blufor
*
* 0: Opfor
*/
forceTeamId(id, teamId) {
return this.send(`ForceTeamId ${id} ${teamId}`);
}
/**
* Dot Access Setter for Server Variables.
*
* Do not ever set an accessor to a variable, always use .read() and .write()
*/
get variables() {
return (0, Variables_1.setupVariableProxy)(this);
}
/**
* Dot Access Setter for Server Variables.
*
* Do not ever set an accessor to a variable, always use .read() and .write()
*/
get variablesUnsafe() {
return (0, Variables_1.setupVariableProxy)(this);
}
/**
* Change the current Level.
*/
serverTravel(map) {
let mapString;
if (map instanceof utils_1.MapQuery) {
mapString = map.toString();
}
else {
mapString = map;
}
return this.send(`serverTravel ${mapString}`);
}
/**
* Send a message to all players.
*
* @note These messages only display from the in-game console currently.
*/
say(message) {
return this.send(`say "${message}"`);
}
/**
* Send a Raw RCON command.
*/
send(cmd) {
const id = this.messageID++;
return new Promise((resolve, reject) => {
const ob = {
time: new Date(),
buffer: '',
handled: false,
};
ob.resolve = (dat) => {
ob.handled = true;
resolve(dat);
};
ob.reject = (dat) => {
ob.handled = true;
reject(dat);
};
//? Timeout in the event of the final packet is Max packet length
ob.timeOut = setTimeout(() => {
if (ob.handled)
return;
ob.handled = true;
ob.resolve(this._parseResponse(ob.buffer) ?? ob.buffer);
}, 5000);
this._responsePromiseQueue.set(id, ob);
this._conn.send(cmd, 0x02, id);
});
}
_onResponse(response) {
const size = response?.size;
if (this._responsePromiseQueue.has(response.id)) {
const q = this._responsePromiseQueue.get(response.id);
if (size >= 4092) {
//MAX PACKET SIZE, MIGHT BE SPLIT
q.buffer += response.body;
return;
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const res = this._parseResponse((q.buffer ?? '') + response.body) ?? response;
if (res.success === false) {
q.reject(res.data ?? res);
}
else {
q.resolve(res.data ?? res);
}
this._responsePromiseQueue.delete(response.id);
return;
}
// Non command
//TODO
}
/**
* Disconnect the client.
*/
disconnect() {
this._conn.disconnect();
}
_parseResponse(res) {
const data = res?.replaceAll('\\n', '\n');
if (data == null || data?.trim?.() == '')
return null;
return this.rconParser.parse(data);
}
_onError(err) {
if (this.debug) {
console.error(err);
}
//Handle Error. Hookable
}
onEnd() {
//TODO: onEnd
}
}
exports.OHD = OHD;