@afocommunity/eco-rcon
Version:
An RCON Interface for ECO
290 lines (289 loc) • 10.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = __importDefault(require("events"));
const events_2 = __importDefault(require("events"));
const net_1 = __importDefault(require("net"));
const RCONParser_1 = __importDefault(require("./parser/RCONParser"));
var PacketType;
(function (PacketType) {
PacketType[PacketType["COMMAND"] = 2] = "COMMAND";
PacketType[PacketType["AUTH"] = 3] = "AUTH";
PacketType[PacketType["RESPONSE_VALUE"] = 0] = "RESPONSE_VALUE";
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 {
host;
port;
password;
outstandingData = null;
hasAuthed;
_tcpSocket;
constructor(host, port, password) {
super();
this.host = host;
this.port = port;
this.password = password;
this.hasAuthed = false;
events_2.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);
}).bind(this));
this._tcpSocket
.on('connect', (() => {
this.socketOnConnect();
}).bind(this));
this._tcpSocket
.on('error', ((err) => {
this.emit('error', err);
}).bind(this));
this._tcpSocket
.on('end', (() => {
this.socketOnEnd();
}).bind(this));
}
catch (error) {
this.emit('error', error);
}
}
send(data, cmd, id = 0x0012d4a6) {
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();
}).bind(this));
}
_tcpSocketOnData(data) {
if (this.outstandingData != null) {
data = Buffer.concat([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.charAt(str.length - 1) === '\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 ECO servers.
*/
class ECO {
/**
* Rejects if an error occurs when connecting.
*/
onReady;
rconParser;
messageID;
_conn;
_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 });
Object.defineProperty(this, '_responsePromiseQueue', { enumerable: false, value: new Map });
Object.defineProperty(this, 'rconParser', { enumerable: false, value: new RCONParser_1.default(this) });
Object.defineProperty(this, '_onResponse', { enumerable: false, value: this._onResponse.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
.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);
});
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);
}
/**
* 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);
}
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').replaceAll('\\r', '');
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.default = ECO;