yamrc
Version:
Yet another Minecraft RCON client
216 lines (212 loc) • 8.82 kB
JavaScript
"use strict";
/*
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/>.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Rcon = void 0;
const net_1 = __importDefault(require("net"));
/**
* Represents a remote console (RCON) connection to interact with a server.
*/
class Rcon {
/**
* 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(host, port, password) {
this.host = host;
this.port = port;
this.password = password;
this.callbacks = new Map();
this.authenticated = false;
this.socket = new net_1.default.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.
*/
connect() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((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.
*/
disconnect() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((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.
*/
send(command) {
return __awaiter(this, void 0, void 0, function* () {
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.
*/
isConnected() {
return this.socket.writable;
}
/**
* Returns the current authentication status.
* @returns True if authenticated, false otherwise.
*/
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.
*/
auth(password) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((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.
*/
createBuffer(id, type, body) {
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
*/
read(packet) {
// 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.
*/
randInt32() {
return Math.floor(Math.random() * 0xFFFFFFFF) | 0; // Use bitwise OR to limit to 32-bit range
}
}
exports.Rcon = Rcon;