diff --git a/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/BurgerPanelIntegrator.java b/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/BurgerPanelIntegrator.java index 8d93ee0..210070d 100644 --- a/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/BurgerPanelIntegrator.java +++ b/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/BurgerPanelIntegrator.java @@ -63,9 +63,9 @@ public void onEnable() { @Override public void run() { JSONParser parser = new JSONParser(); - ByteBuffer bb = ByteBuffer.allocate(10_000); - int byteCount; while(true) { + int byteCount; + ByteBuffer bb = ByteBuffer.allocate(10_000); try { byteCount = client.read(bb); } catch (IOException e) { diff --git a/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/packets/StatusPacket.java b/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/packets/StatusPacket.java index e2fa4a3..a3a22c3 100644 --- a/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/packets/StatusPacket.java +++ b/Integrator/src/main/java/io/github/theblueburger/burgerpanelintegrator/packets/StatusPacket.java @@ -31,7 +31,7 @@ protected void execute(JSONObject data, String id) throws IOException { locationObj.put("y", location.getY()); locationObj.put("z", location.getZ()); locationObj.put("world", location.getWorld().getName()); - playerObject.put("position", locationObj); + playerObject.put("location", locationObj); playerArray.add(playerObject); } responseObj.put("players", playerArray); diff --git a/Server/Gulpfile.mjs b/Server/Gulpfile.mjs index eded22a..0796f87 100644 --- a/Server/Gulpfile.mjs +++ b/Server/Gulpfile.mjs @@ -32,6 +32,10 @@ async function buildWeb() { export async function packetsToFile() { let fileData = ""; let files = fs.readdirSync("src/packets"); + files = files.filter(f => { + if(fs.statSync("src/packets/" + f).isDirectory()) return false; + return true; + }); if(process.env.BURGERPANEL_SKIP_SERVER != "1") { files.forEach((f) => { fileData += `import ${f.replace(".ts", "")} from "./dist/Server/src/packets/${f.replace(".ts", ".js")}";\n`; diff --git a/Server/buildTools/packetsToFile.mjs b/Server/buildTools/packetsToFile.mjs index c646c6e..4dd67df 100644 --- a/Server/buildTools/packetsToFile.mjs +++ b/Server/buildTools/packetsToFile.mjs @@ -1,4 +1,8 @@ import fs from "node:fs"; let files = fs.readdirSync("src/packets"); +files = files.filter(f => { + if(fs.statSync("src/packets/" + f).isDirectory()) return false; + return true; +}); let shareTextData = `// Do not touch this file, it is automatically changed.\nexport default ${JSON.stringify(files.map(f => f.replace(".ts", "")))} as const`; fs.writeFileSync("../Share/Packets.ts", shareTextData); \ No newline at end of file diff --git a/Server/src/packets/integrator.ts b/Server/src/packets/integrator.ts new file mode 100644 index 0000000..55b32aa --- /dev/null +++ b/Server/src/packets/integrator.ts @@ -0,0 +1,29 @@ +import { OurClient, Packet, ServerPacketResponse } from "../index.js"; +import { servers } from "../db.js"; +import serverManager, { userHasAccessToServer } from "../serverManager.js"; +import { Permission, hasServerPermission } from "../../../Share/Permission.js"; +import { Request } from "../../../Share/Requests.js"; + +import IntegratorInstall from "./integrator/IntegratorInstall.js" +import serverIntegrator from "../serverIntegrator.js"; + +export default class IntegratorPacket extends Packet { + name: Request = "integrator"; + requiresAuth: boolean = true; + async handle(client: OurClient, data: any): ServerPacketResponse<"integrator"> { + const server = await servers.findById(data.id); + if(!server || !userHasAccessToServer(client.data.auth.user, server.toJSON())) return "Server not found!"; + switch(data.action) { + case "install": + if(!hasServerPermission(client.data.auth.user, server.toJSON(), "integrator.install")) return "You can't do that!"; + return await IntegratorInstall(server.toJSON(), client, data); + case "status": + const status = await serverIntegrator.request(server.toJSON(), "status"); + return { + type: "status", + status + } + } + return; + } +} \ No newline at end of file diff --git a/Server/src/packets/integrator/IntegratorInstall.ts b/Server/src/packets/integrator/IntegratorInstall.ts new file mode 100644 index 0000000..99133ba --- /dev/null +++ b/Server/src/packets/integrator/IntegratorInstall.ts @@ -0,0 +1,14 @@ +import { OurClient } from "../.."; +import { Server } from "../../../../Share/Server"; +import fs from "node:fs/promises"; +import { buildInfo } from "../../../../Share/BuildInfo.js"; +import { integratorJarPath } from "../../serverIntegrator.js"; +import logger from "../../logger.js"; + +export default async function install(server: Server, client: OurClient, data: any): Promise<{type: "install-success"}> { + logger.log(`${client.data.auth.user?.username} is installing the BurgerPanel integrator on server ${server.name}`, "server.integrator"); + await fs.copyFile(integratorJarPath, server.path + `/plugins/BurgerPanelIntegrator-${buildInfo.version}.jar`); + return { + type: "install-success" + } +} diff --git a/Server/src/serverIntegrator.ts b/Server/src/serverIntegrator.ts index 1eedd8e..dbbf742 100644 --- a/Server/src/serverIntegrator.ts +++ b/Server/src/serverIntegrator.ts @@ -1,9 +1,17 @@ const isProd = process.env.NODE_ENV == "production"; import fsSync from "node:fs"; +import fs from "node:fs/promises"; import net from "node:net"; import path from "node:path"; import logger, { LogLevel } from "./logger.js"; import type { Server } from "../../Share/Server.js"; +import { buildInfo } from "../../Share/BuildInfo.js"; +import nodeCrypto from "node:crypto"; +import url from "node:url"; +let __dirname = url.fileURLToPath(new URL('.', import.meta.url)); +export let integratorJarPath: string; +if(process.env.NODE_ENV == "production") integratorJarPath = __dirname + "/Integrator.jar"; +else integratorJarPath = __dirname + "/../../../../Integrator/build/libs/BurgerPanelIntegrator-1.0-SNAPSHOT.jar"; interface OurIntegratorClient extends net.Socket { burgerpanelData: { server?: string @@ -13,7 +21,9 @@ export default new class ServerIntegrator { path: string | undefined; filename = "connector.burgerpanelsock"; requestCallbacks: { - [id: string]: (resp: any) => void + [serverID: string]: { + [id: string]: (resp: any) => void + } } = {}; server: net.Server = undefined as any as net.Server; servers: { @@ -28,6 +38,20 @@ export default new class ServerIntegrator { if(fsSync.existsSync(this.path)) fsSync.rmSync(this.path); this.listen(); } + async updateIntegratorIfNeeded(server: Server) { + let files: string[]; + try { + files = await fs.readdir(server.path + "/plugins/"); + } catch { + return; + } + let currentIntegratorFilename = files.find(f => f.startsWith("BurgerPanelIntegrator-") && f.endsWith(".jar")); + if(!currentIntegratorFilename) return; + if(currentIntegratorFilename == `BurgerPanelIntegrator-${buildInfo.gitHash}.jar`) return; + logger.log(`Autoupdating BurgerPanel integrator on server ${server.name}`, "server.integrator") + await fs.unlink(server.path + "/plugins/" + currentIntegratorFilename); + await fs.copyFile(integratorJarPath, server.path + "/plugins/" + `BurgerPanelIntegrator-${buildInfo.gitHash}.jar`); + } getPath() { if(process.platform == "win32") { logger.log("Integrator cannot be used because microsoft", "debug", LogLevel.DEBUG); @@ -40,6 +64,9 @@ export default new class ServerIntegrator { let server = net.createServer(_c => { let c = _c as OurIntegratorClient; c.burgerpanelData = {}; + c.on("close", () => { + if(c.burgerpanelData.server) delete this.servers[c.burgerpanelData.server]; + }); c.on("data", d => { let json; try { @@ -52,8 +79,9 @@ export default new class ServerIntegrator { if(!["request", "response"].includes(json.dataType)) return; if(json.dataType == "response") { console.log(JSON.stringify(json, null, 2)); - if(!this.requestCallbacks[json.id]) return; - this.requestCallbacks[json.id](json.data); + if(!c.burgerpanelData.server) return; + if(!this.requestCallbacks[c.burgerpanelData.server] || !this.requestCallbacks[c.burgerpanelData.server][json.id]) return; + this.requestCallbacks[c.burgerpanelData.server][json.id](json.data); return; } // todo: proper handler @@ -81,5 +109,42 @@ export default new class ServerIntegrator { this.servers[server._id] = { server }; + this.requestCallbacks[server._id] = {}; + } + findUnusedID(server: Server): string { + for(let i = 0; i < 100; i++) { + let str = nodeCrypto.randomBytes(30).toString("base64url"); + if(this.requestCallbacks[server._id][str]) continue; + return str; + } + throw new Error("somehow didnt find a id in 100 tries"); + } + request(server: Server, packet: string, data: any = {}): Promise { + if(!this.isReadyForRequests(server)) throw new Error("Server isn't ready for requests!"); + const client = this.servers[server._id].client; + if(!client) throw new Error("bad"); + let id = this.findUnusedID(server); + let toSend = JSON.stringify({ + id, + packet, + data + }); + console.log("Sending", toSend); + client.write(toSend); + return new Promise((res, rej) => { + let timeout = setTimeout(() => { + delete this.requestCallbacks[server._id][id]; + rej("Request timed out"); + }, 10_000); + this.requestCallbacks[server._id][id] = (d) => { + clearTimeout(timeout); + res(d); + } + }); + } + isReadyForRequests(server: Server) { + const client = this.servers[server._id]?.client; + if(!client) return false; + return true; } } diff --git a/Server/src/serverManager.ts b/Server/src/serverManager.ts index aa47fba..703f0af 100644 --- a/Server/src/serverManager.ts +++ b/Server/src/serverManager.ts @@ -118,6 +118,7 @@ enforce-secure-profile=false args = ["-Dnojline=true", "-Xms" + server.mem + "M", "-Xmx" + server.mem + "M", ...jvmArgs]; } serverIntegrator.prepareServer(server); + await serverIntegrator.updateIntegratorIfNeeded(server); let childProcess = spawn("java", args, { cwd: server.path, stdio: "pipe", diff --git a/Share/Integrator.ts b/Share/Integrator.ts new file mode 100644 index 0000000..2b9aaab --- /dev/null +++ b/Share/Integrator.ts @@ -0,0 +1,14 @@ +export type IntegratorPlayerInformation = { + name: string, + uuid: string, + location: { + world: string, + x: number, + y: number, + z: number + } +}; +export type IntegratorServerInformation = { + tps: number, + players: IntegratorPlayerInformation[] +}; \ No newline at end of file diff --git a/Share/Packets.ts b/Share/Packets.ts index 60c92eb..a95be22 100644 --- a/Share/Packets.ts +++ b/Share/Packets.ts @@ -1,2 +1,2 @@ // Do not touch this file, it is automatically changed. -export default ["attachToServer","auth","createServer","createUser","deleteServer","deleteUser","detachFromServer","editUser","getAllServers","getAllSettings","getServer","getSetting","getUserData","getUserToken","getUsers","importServer","killServer","listSessions","logging","logout","ping","plugins","serverFiles","serverLogs","setServerOption","setSetting","startServer","stopServer","systemInformation","writeToConsole"] as const \ No newline at end of file +export default ["attachToServer","auth","createServer","createUser","deleteServer","deleteUser","detachFromServer","editUser","getAllServers","getAllSettings","getServer","getSetting","getUserData","getUserToken","getUsers","importServer","integrator","killServer","listSessions","logging","logout","ping","plugins","serverFiles","serverLogs","setServerOption","setSetting","startServer","stopServer","systemInformation","writeToConsole"] as const \ No newline at end of file diff --git a/Share/Permission.ts b/Share/Permission.ts index 2883af5..207c601 100644 --- a/Share/Permission.ts +++ b/Share/Permission.ts @@ -1,6 +1,6 @@ import { Server } from "./Server.js"; import { User } from "./User.js"; -export const _ServerPermissions = ["set.autostart", "set.autorestart", "set.port", "set.jvmArgs", "set.usejvmargs", "set.software", "set.version", "set.mem", "set.name", "set.allowedUsers.add", "set.allowedUsers.remove", "set.allowedUsers.permissions.write", "console.read", "console.write", "status", "stop", "start", "kill", "delete", "oldlogs.read", "serverfiles.read", "serverfiles.write", "serverfiles.delete", "serverfiles.upload", "serverfiles.download", "serverfiles.new", "serverfiles.rename", "plugins.download", "full"] as const; +export const _ServerPermissions = ["set.autostart", "set.autorestart", "set.port", "set.jvmArgs", "set.usejvmargs", "set.software", "set.version", "set.mem", "set.name", "set.allowedUsers.add", "set.allowedUsers.remove", "set.allowedUsers.permissions.write", "console.read", "console.write", "status", "stop", "start", "kill", "delete", "oldlogs.read", "serverfiles.read", "serverfiles.write", "serverfiles.delete", "serverfiles.upload", "serverfiles.download", "serverfiles.new", "serverfiles.rename", "plugins.download", "integrator.install", "full"] as const; export const _ServersPermissions = ["create", "import", "all.view"] as const; export const _UserPermissions = ["create", "view", "token.read", "token.reset", "delete", "permissions.read", "permissions.write", "password.change", "username.change.self", "username.change.all"] as const; export const _SettingPermissions = [`set`, "read", "logging.set"] as const; diff --git a/Share/Requests.ts b/Share/Requests.ts index 127c080..f78d604 100644 --- a/Share/Requests.ts +++ b/Share/Requests.ts @@ -1,5 +1,6 @@ import { Config, ConfigValue } from "./Config"; import { GeneralInformation, ServerPerformance } from "./SystemInformation"; +import { IntegratorServerInformation } from "./Integrator"; import { Server, ServerStatus, ServerStatuses } from "./Server"; import { ModrinthPluginResult, Plugin, Version } from "./Plugin"; import { User } from "./User"; @@ -122,7 +123,13 @@ export type RequestResponses = { } | { type: "downloadSuccess" }, - listSessions: {username?: string, _id?: string}[] + listSessions: {username?: string, _id?: string}[], + integrator: { + type: "install-success" + } | { + type: "status", + status: IntegratorServerInformation + } }