UNPKG

@ohd-tools/rcon

Version:

An RCON Interface for Operation: Harsh Doorstop

668 lines (667 loc) 22 kB
"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;