From baf102b9ad65623dd3de0f9de9632d1c6c69a75e Mon Sep 17 00:00:00 2001 From: sergeyyarkov Date: Wed, 31 May 2023 22:33:48 +0300 Subject: [PATCH] first --- config.json | 6 ++ index.js | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 ++++ utils.js | 8 +++ 4 files changed, 213 insertions(+) create mode 100644 config.json create mode 100644 index.js create mode 100644 package.json create mode 100644 utils.js diff --git a/config.json b/config.json new file mode 100644 index 0000000..5906299 --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "host": "0.0.0.0", + "port": 27015, + "password": "password", + "readlineEnabled": true +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..ada4943 --- /dev/null +++ b/index.js @@ -0,0 +1,186 @@ +import { Buffer } from 'node:buffer'; +import net from 'node:net'; +import crypto from 'node:crypto'; +import config from './config.json' assert { type: 'json' }; + +var PACKET_TYPES = { + SERVERDATA_AUTH: 3, + SERVERDATA_AUTH_RESPONSE: 2, + SERVERDATA_EXECCOMMAND: 2, + SERVERDATA_RESPONSE_VALUE: 0, +}; + +var rcon = await rcon_connect(config.host, config.port); +await rcon.auth(config.password); + +/** + * Send commands using user input + */ +if (config.readlineEnabled) { + var readline = await import('node:readline/promises'); + var rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + rl.on('SIGINT', exit); + + while (1) { + var command = await rl.question('> '); + if (command == '.exit') exit(); + console.log(`[RCON]: Executing "${command}"`); + var response = await rcon.command(command); + console.log(`[RCON]: Response:`, response); + } +} + +/** + * Creates Basic Packet Structure + * Link: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Basic_Packet_Structure + * + * @param {number} id + * @param {number} type + * @param {string} body + * @returns {Buffer} + */ +function rcon_create_packet_structure(id, type, body) { + var size = Buffer.byteLength(body) + 14; + var buffer = Buffer.alloc(size); + + buffer.writeInt32LE(size - 4, 0); + buffer.writeInt32LE(id, 4); + buffer.writeInt32LE(type, 8); + buffer.write(body, 12, size - 2, 'ascii'); + buffer.writeInt16LE(0x00, size - 2); + + return buffer; +} + +/** + * The complete Triforce, or one or more components of the Triforce. + * @typedef {Object} PacketStructure + * @property {number} size + * @property {number} id + * @property {number} type + * @property {string} body + */ + +/** + * Return object that contains packet data + * + * @param {Buffer} buffer + * @returns {PacketStructure} + */ +function rcon_get_packet_structure(buffer) { + return { + size: buffer.readInt32LE(0), + id: buffer.readInt32LE(4), + type: buffer.readInt32LE(8), + body: buffer.toString('ascii', 12, buffer.length - 2), + }; +} + +/** + * Will create a new socket + * + * @param {string} host + * @param {number} port + * @returns {Promise} + */ +async function rcon_create_connection(host, port) { + return new Promise((resolve, reject) => { + var socket = net + .createConnection( + { + host, + port, + }, + () => { + console.log('[RCON]: Successfully connected to the server!'); + resolve(socket); + } + ) + .on('error', (error) => reject(error)); + }); +} + +/** + * Creates a new connection + * + * @param {string} host + * @param {number} port + */ +async function rcon_connect(host, port) { + try { + var connection = await rcon_create_connection(host, port); + + return { + connection, + + close() { + connection.end(); + }, + + /** + * Send RCON command + * + * @param {string} name + * @returns + */ + async command(name) { + var response = await this.send( + rcon_create_packet_structure( + crypto.randomInt(10000), + PACKET_TYPES.SERVERDATA_EXECCOMMAND, + name + ) + ); + return response; + }, + + /** + * Auhtenticate RCON client + * + * @param {string} password + * @returns + */ + async auth(password) { + var response = await this.send( + rcon_create_packet_structure(10, PACKET_TYPES.SERVERDATA_AUTH, password) + ); + return response; + }, + + /** + * Send RCON packet + * + * @param {PacketStructure} packet + * @returns {Promise} + */ + async send(packet) { + connection.write(packet); + return new Promise((resolve, reject) => { + connection.removeAllListeners('data'); + connection.removeAllListeners('error'); + connection.on('data', (data) => { + var packet = rcon_get_packet_structure(data); + + /** + * Check on password valid + */ + if (packet.type === 2 && packet.id === -1) { + throw new Error('Wrong RCON password!'); + } + + resolve(packet); + }); + connection.on('error', (error) => reject(error)); + }); + }, + }; + } catch (error) { + throw error; + } +} + +function exit() { + console.log('\n[RCON]: Closing connection...'); + rcon.close(); + process.exit(1); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..33b83cc --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "rcon", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node --no-warnings index.js" + }, + "keywords": [], + "author": "Sergey Yarkov", + "license": "ISC" +} diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..67419e0 --- /dev/null +++ b/utils.js @@ -0,0 +1,8 @@ +export async function wait(ms = 1000, cb = () => undefined) { + return new Promise((resolve) => { + setTimeout(() => { + cb(); + resolve(); + }, ms); + }); +}