diff --git a/api/bun.lockb b/api/bun.lockb index c329b0f..10b8a5b 100755 Binary files a/api/bun.lockb and b/api/bun.lockb differ 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<Events>) { - 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