UNPKG

yamrc

Version:

Yet another Minecraft RCON client

210 lines (186 loc) 7.56 kB
/* YAMRC establishes a connection to an RCON server, authenticates using a password, and sends commands to the server. Copyright (C) 2023 Necrozma This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import net from 'net'; /** * Represents a remote console (RCON) connection to interact with a server. */ export class Rcon { // Private properties private socket: net.Socket; private id: number; private callbacks: Map<number, (packet: any) => void> = new Map(); private authenticated: boolean = false; /** * Creates an instance of Rcon. * @param host - The host address of the server. * @param port - The port number to connect to. * @param password - The RCON password for authentication. */ constructor(private host: string, private port: number, private password: string) { this.socket = new net.Socket(); this.id = this.randInt32(); } /** * Establishes a connection to the RCON server. * @returns A promise that resolves when the connection is established successfully. * @throws Rejects with an error if the connection fails. */ public async connect() { return new Promise<void>((resolve, reject) => { this.socket.connect(this.port, this.host, () => { console.log('Connected to RCON server, authenticating...'); this.auth(this.password).then(() => resolve()).catch(err => reject(err)); }); this.socket.on('error', (err) => { reject(err); }); this.socket.on('data', (data) => { const packet = this.read(data); if (packet.id === -1) { console.log('Authentication failed: Wrong password'); reject('Authentication failed: Wrong password'); return; } const callback = this.callbacks.get(packet.id); if (callback) { callback(packet); this.callbacks.delete(packet.id); } }); }); } /** * Closes the connection to the RCON server. * @returns A promise that resolves when the connection is closed. */ public async disconnect() { return new Promise<void>((resolve, reject) => { this.socket.end(() => { resolve(); }); }); } /** * Sends a command to the RCON server. * @param command - The command string to be sent. * @returns A promise that resolves with the response from the server. * @throws Rejects with an error if authentication is required before sending commands. */ public async send(command: string) { return new Promise((resolve, reject) => { // Check if we are authenticated before sending commands if (!this.authenticated) { reject('Authentication required before sending commands.'); return; } // Generate a unique ID for the command and add a callback to the map const id = this.id++; this.callbacks.set(id, (data) => { resolve(data); }); // Create a buffer with the command and send it to the server const buffer = this.createBuffer(id, 2, command); this.socket.write(buffer); }); } /** * Returns the current connection status. * @returns True if connected, false otherwise. */ public isConnected() { return this.socket.writable; } /** * Returns the current authentication status. * @returns True if authenticated, false otherwise. */ public isAuthenticated() { return this.authenticated; } // Private methods /** * Performs authentication with the RCON server using the provided password. * @param password - The RCON password for authentication. * @returns A promise that resolves when authentication is successful. * @throws Rejects with an error if authentication fails. */ private async auth(password: string) { return new Promise<void>((resolve, reject) => { const id = this.id++; this.callbacks.set(id, (packet) => { if (packet.id === id) { console.log('Authentication successful'); this.authenticated = true; resolve(); } else { // Need to fix this, will never happen console.log('Authentication failed: Unexpected response ID'); reject('Authentication failed: Unexpected response ID'); } }); // Create a buffer with the password and send it to the server const buffer = this.createBuffer(id, 3, password); this.socket.write(buffer); }); } /** * Creates a buffer to be sent over the socket. * @param id - The packet ID. * @param type - The packet type. * @param body - The content/body of the packet. * @returns A buffer containing the formatted packet. */ private createBuffer(id: number, type: number, body: string) { const bodyBuffer = Buffer.from(body, 'utf8'); const buffer = Buffer.alloc(14 + bodyBuffer.length); buffer.writeInt32LE(10 + bodyBuffer.length, 0); buffer.writeInt32LE(id, 4); buffer.writeInt32LE(type, 8); buffer.write(body, 12, 'utf8'); buffer.writeInt8(0, 12 + bodyBuffer.length); buffer.writeInt8(0, 13 + bodyBuffer.length); return buffer; } /** * Reads a packet received from the server. * @param packet - The packet buffer received from the server. * @returns An object representing the parsed packet. * @throws Error if the packet is invalid. * @author github.com/tehbeard */ private read(packet: Buffer) { // Length of the rest of the packet const length = packet.readInt32LE(0); // Check if we have a valid packet with 2 null bytes of padding in the end if (packet.length === 4 + length && !packet.readInt16LE(packet.length - 2)) { // Offsets are hardcoded for speed return { length: length, id: packet.readInt32LE(4), type: packet.readInt32LE(8), payload: packet.toString('ascii', 12, packet.length - 2) }; } else { throw new Error(`Invalid packet! [${packet}]`); } } /** * Generates a random 32-bit integer for use as the initial packet ID. * @returns A random 32-bit integer. */ private randInt32() { return Math.floor(Math.random() * 0xFFFFFFFF) | 0; // Use bitwise OR to limit to 32-bit range } }