From 047547b454b92bc30967f2728070621a5a93c80b Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 28 Feb 2025 22:30:19 +0200 Subject: [PATCH] use rconts --- api/bun.lockb | Bin 37228 -> 38045 bytes api/index.ts | 46 +++------- api/package.json | 1 + api/rcon.ts | 235 ----------------------------------------------- 4 files changed, 15 insertions(+), 267 deletions(-) delete mode 100644 api/rcon.ts diff --git a/api/bun.lockb b/api/bun.lockb index c329b0fd646a9dec77f1ff69a80146a601390eef..10b8a5bfb171ad16e613e986c45818ecf1e9c5cf 100755 GIT binary patch delta 6912 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 N%N)OhOj2B;>XWBoVqn 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 diff --git a/api/index.ts b/api/index.ts index a94bcc6..ee61a2a 100644 --- a/api/index.ts +++ b/api/index.ts @@ -12,7 +12,7 @@ import express, { type Request, type Response, type NextFunction } from 'express'; import { InstantVector, PrometheusDriver, ResponseType, SampleValue } from 'prometheus-query'; -import { PacketType, Rcon } from "./rcon"; +import { Rcon } from 'rconts'; if(!process.env.prometheusHost) { throw new Error("missing prometheus host") @@ -28,36 +28,19 @@ if(process.env.prometheusUsername || process.env.prometheusPassword) { if(!process.env.rconHost || !process.env.rconPassword) { throw new Error("missing rcon host or rcon password") } -const rcon = new Rcon(process.env.rconHost.split(":")[0], +process.env.rconHost.split(":")[1], process.env.rconPassword); -let rconReconnectTimeout: Timer | null | undefined; -let whitelistedPlayers: string[] = []; + +const rcon = new Rcon({ + host: process.env.rconHost, + password: process.env.rconPassword +}); + let lastRequested = 0; +let whitelistedPlayers: string[] = []; -const playerRegex = /There are \d* whitelisted player\(s\): /gm; - -rcon.on("connect", () => { +rcon.on("connected", () => { console.log("RCON connected."); }); -rcon.on("auth", () => { - console.log("RCON authenicated."); -}); - -rcon.on("response", (a: string) => { - if (playerRegex.test(a)) { - whitelistedPlayers = a.replace(playerRegex, "").split(", "); - } -}) - -rcon.on("end", () => { - console.log("RCON ended. Reconnecting in 2000ms.") - if(rconReconnectTimeout) { - clearTimeout(rconReconnectTimeout); - } - rconReconnectTimeout = setTimeout(() => { - rcon.connect() - }, 2000) -}) const app = express(); //@ts-expect-error @@ -103,13 +86,12 @@ app.get("/api/uptime", async (req, res) => { app.get("/api/whitelist", async (req, res) => { if ( Date.now() - lastRequested > 60000) { lastRequested = Date.now(); - rcon.send("whitelist list", PacketType.COMMAND); - rcon.once("response", () => { - res.json(whitelistedPlayers); - }); - } else { - res.json(whitelistedPlayers); + + const list = await rcon.exec("whitelist list"); + whitelistedPlayers = list.replace(/There are \d* whitelisted player\(s\): /gm, "").split(", "); } + res.json(whitelistedPlayers); + }) app.listen(port, host, () => { rcon.connect() diff --git a/api/package.json b/api/package.json index 2912ac0..6f44ca8 100644 --- a/api/package.json +++ b/api/package.json @@ -12,6 +12,7 @@ "@types/express": "^5.0.0", "express": "^4.21.2", "prometheus-query": "^3.4.1", + "rconts": "git+https://git.sad.ovh/sophie/rconts.git#8f6d5a48330a35f7e4e288b5a3f0104e249aa575", "typed-emitter": "^2.1.0" } } \ No newline at end of file diff --git a/api/rcon.ts b/api/rcon.ts deleted file mode 100644 index bbe39a0..0000000 --- a/api/rcon.ts +++ /dev/null @@ -1,235 +0,0 @@ -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); - }); - this._tcpSocket.on("connect", () => { - this.socketOnConnect(); - }); - this._tcpSocket.on("error", (err) => { - //this.emit("error", err); - this.socketOnEnd() - }); - this._tcpSocket.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