From 1aeebc8ae930a8ea2b13d5ac86b1cdad2f6fda32 Mon Sep 17 00:00:00 2001 From: sophie Date: Mon, 1 Jul 2024 14:13:27 +0300 Subject: [PATCH] cooked ass library --- advancementAPI.ts | 8 +- bun.lockb | Bin 17861 -> 18588 bytes package.json | 3 +- rcon.ts | 233 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 rcon.ts diff --git a/advancementAPI.ts b/advancementAPI.ts index 7a4f83f..f42b4a2 100644 --- a/advancementAPI.ts +++ b/advancementAPI.ts @@ -1,5 +1,5 @@ -//@ts-nocheck -import Rcon from "ts-rcon"; +import {Rcon, PacketType} from "./rcon"; + import config from "./config.json" assert { type: "json" }; const advancements = new Map(); @@ -34,7 +34,7 @@ client.on("response", (a: string) => { for (let i = 0; i < whitelistedPlayers.length; i++) { setTimeout(() => { const name = whitelistedPlayers[i]; - client.send(`scoreboard players get ${name} bac_advancements`, 0x02); + client.send(`scoreboard players get ${name} bac_advancements`, PacketType.COMMAND); }, i * 20); } } @@ -67,7 +67,7 @@ export function getAdvancements(): Promise> { } advancementsRequired = 0; advancements.clear(); - client.send("whitelist list", 0x02); + client.send("whitelist list", PacketType.COMMAND); client.once("done", () => { res(advancements); }); diff --git a/bun.lockb b/bun.lockb index 5761d1e5383ab27cfc09dc73859c890ae5705c00..07d1635efd048484bf70c7a75aaa0f2e044950ca 100755 GIT binary patch delta 2913 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 Hb&&rBtDC#3 diff --git a/package.json b/package.json index 8aa3a36..57e440e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "module": "index.ts", "type": "module", "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "typed-emitter": "^2.1.0" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/rcon.ts b/rcon.ts new file mode 100644 index 0000000..136a8a3 --- /dev/null +++ b/rcon.ts @@ -0,0 +1,233 @@ +import EventEmitter from 'events'; +import * as net from 'net'; +import * as dgram from 'dgram'; +import { Buffer } from 'buffer'; +import type TypedEmitter from "typed-emitter" + +type Events = { + error: (error: Error) => void, + auth: () => void, + response: (response: string) => void, + connect: () => void, + end: () => void, + done: () => void +} + +export const PacketType = { + COMMAND: 0x02, + AUTH: 0x03, + RESPONSE_VALUE: 0x00, + RESPONSE_AUTH: 0x02, +}; + +interface Options { + tcp?: boolean; + challenge?: boolean; + id?: number; +} +export class Rcon extends (EventEmitter as new () => TypedEmitter) { + private host: string; + private port: number; + private password: string; + private rconId: number; + private hasAuthed: boolean; + private outstandingData: Uint8Array | null; + private tcp: boolean; + private challenge: boolean; + private _challengeToken: string; + private _tcpSocket!: net.Socket; + private _udpSocket!: dgram.Socket; + + constructor(host: string, port: number, password: string, options?: Options) { + super(); + options = options || {}; + this.host = host; + this.port = port; + this.password = password; + this.rconId = options.id || 0x0012d4a6; // This is arbitrary in most cases + this.hasAuthed = false; + this.outstandingData = null; + this.tcp = options.tcp ? options.tcp : true; + this.challenge = options.challenge ? options.challenge : true; + this._challengeToken = ''; + } + + public send = (data: string, cmd?: number, id?: number): void => { + let sendBuf: Buffer; + if (this.tcp) { + cmd = cmd || PacketType.COMMAND; + id = id || this.rconId; + + const length = Buffer.byteLength(data); + 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); + } else { + if (this.challenge && !this._challengeToken) { + this.emit('error', new Error('Not authenticated')); + return; + } + let str = 'rcon '; + if (this._challengeToken) str += this._challengeToken + ' '; + if (this.password) str += this.password + ' '; + str += data + '\n'; + sendBuf = Buffer.alloc(4 + Buffer.byteLength(str)); + sendBuf.writeInt32LE(-1, 0); + sendBuf.write(str, 4); + } + this._sendSocket(sendBuf); + }; + + private _sendSocket = (buf: Buffer) => { + if (this._tcpSocket) { + this._tcpSocket.write(buf.toString('binary'), 'binary'); + } else if (this._udpSocket) { + this._udpSocket.send(buf, 0, buf.length, this.port, this.host); + } + }; + + public connect = (): void => { + if (this.tcp) { + this._tcpSocket = net.createConnection(this.port, this.host); + this._tcpSocket + .on('data', data => { + this._tcpSocketOnData(data); + }) + .on('connect', () => { + this.socketOnConnect(); + }) + .on('error', err => { + this.emit('error', err); + }) + .on('end', () => { + this.socketOnEnd(); + }); + } else { + this._udpSocket = dgram.createSocket('udp4'); + this._udpSocket + .on('message', data => { + this._udpSocketOnData(data); + }) + .on('listening', () => { + this.socketOnConnect(); + }) + .on('error', err => { + this.emit('error', err); + }) + .on('close', () => { + this.socketOnEnd(); + }); + this._udpSocket.bind(0); + } + }; + + public disconnect = (): void => { + if (this._tcpSocket) this._tcpSocket.end(); + if (this._udpSocket) this._udpSocket.close(); + }; + + public setTimeout = (timeout: number, callback: () => void): void => { + if (!this._tcpSocket) return; + this._tcpSocket.setTimeout(timeout, () => { + this._tcpSocket.end(); + if (callback) callback(); + }); + }; + + private _udpSocketOnData = (data: Buffer) => { + const a = data.readUInt32LE(0); + if (a === 0xffffffff) { + const str = data.toString('utf-8', 4); + const tokens = str.split(' '); + if ( + tokens.length === 3 && + tokens[0] === 'challenge' && + tokens[1] === 'rcon' + ) { + this._challengeToken = tokens[2].substring(0, tokens[2].length - 1).trim(); + this.hasAuthed = true; + this.emit('auth'); + } else { + this.emit('response', str.substring(1, str.length - 2)); + } + } else { + this.emit('error', new Error('Received malformed packet')); + } + }; + + private _tcpSocketOnData = (data: Buffer) => { + if (this.outstandingData != null) { + data = Buffer.concat( + [this.outstandingData, data], + this.outstandingData.length + data.length + ); + this.outstandingData = null; + } + + while (data.length) { + const len = data.readInt32LE(0); + if (!len) return; + + const id = data.readInt32LE(4); + const type = data.readInt32LE(8); + + if (len >= 10 && data.length >= len + 4) { + if (id === this.rconId) { + 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 + len - 10); + + if (str.charAt(str.length - 1) === '\n') { + // Emit the response without the newline. + str = str.substring(0, str.length - 1); + } + + this.emit('response', str); + } + } else { + this.emit('error', new Error('Authentication failed')); + } + + data = data.slice(12 + len - 8); + } else { + // Keep a reference to the chunk if it doesn't represent a full packet + this.outstandingData = data; + break; + } + } + }; + + public socketOnConnect = (): void => { + this.emit('connect'); + + if (this.tcp) { + this.send(this.password, PacketType.AUTH); + } else if (this.challenge) { + const str = 'challenge rcon\n'; + const sendBuf = Buffer.alloc(str.length + 4); + sendBuf.writeInt32LE(-1, 0); + sendBuf.write(str, 4); + this._sendSocket(sendBuf); + } else { + const sendBuf = Buffer.alloc(5); + sendBuf.writeInt32LE(-1, 0); + sendBuf.writeUInt8(0, 4); + this._sendSocket(sendBuf); + + this.hasAuthed = true; + this.emit('auth'); + } + }; + + public socketOnEnd = (): void => { + this.emit('end'); + this.hasAuthed = false; + }; +} \ No newline at end of file