diff --git a/config/template.localdb.json b/config/template.localdb.json index f083e4d..c2f801a 100644 --- a/config/template.localdb.json +++ b/config/template.localdb.json @@ -87,6 +87,11 @@ "whitelist": true, "display": true } + }, + "clientsync": { + "always": true, + "hash": true, + "interrupt": true } }, "users": { diff --git a/package.json b/package.json index d684e2d..69425cf 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,12 @@ "dependencies": { "axios": "^1.3.2", "body-parser": "^1.20.1", + "cookie": "^0.5.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^4.18.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "ws": "^8.13.0" }, "devDependencies": { "eslint": "^8.43.0", diff --git a/src/clientsync.js b/src/clientsync.js new file mode 100644 index 0000000..f3bcf5b --- /dev/null +++ b/src/clientsync.js @@ -0,0 +1,69 @@ +import { WebSocketServer } from "ws"; +import * as cookie from "cookie"; + +import { requestPVE } from "./pve.js"; +import { checkAuth, getObjectHash } from "./utils.js"; + +// maps usernames to socket object(s) +const userSocketMap = {}; +// maps proxmox resource ids to user(s) who can access the resource +const resourceUserMap = {}; + +export function setupClientSync (app, server, schemes) { + /** + * GET - get list of supported synchronization schemes + * responses: + * - 200 : {always: boolean, hash: boolean, interrupt: boolean} + */ + app.get("/api/sync/schemes", async (req, res) => { + res.send(schemes); + }); + + if (schemes.hash) { + /** + * GET - get hash of current cluster resources states + * Client can use this endpoint to check for cluster state changes to avoid costly data transfers to the client. + * responses: + * - 401: {auth: false} + * - 200: string + */ + app.get("/api/sync/hash", async (req, res) => { + // check auth + const auth = await checkAuth(req.cookies, res); + if (!auth) { + return; + } + // get current cluster resources + const status = (await requestPVE("/cluster/resources", "GET", req.cookies)).data.data; + // filter out just state information of resources that are needed + const resources = ["lxc", "qemu", "node"]; + const state = {}; + status.forEach((element) => { + if (resources.includes(element.type)) { + state[element.id] = element.status; + } + }); + res.status(200).send(getObjectHash(state)); + }); + } + if (schemes.interrupt) { + const wsServer = new WebSocketServer({ noServer: true, path: "/api/sync/interrupt" }); + wsServer.on("connection", (socket, username) => { + socket.on("message", (message) => { + console.log(message.toString()); + }); + }); + server.on("upgrade", async (req, socket, head) => { + const cookies = cookie.parse(req.headers.cookie || ""); + const auth = (await requestPVE("/version", "GET", cookies)).status === 200; + if (!auth) { + socket.destroy(); + } + else { + wsServer.handleUpgrade(req, socket, head, (socket) => { + wsServer.emit("connection", socket, cookies.username); + }); + } + }); + } +} diff --git a/src/main.js b/src/main.js index c5df359..c9955f3 100644 --- a/src/main.js +++ b/src/main.js @@ -6,8 +6,9 @@ import morgan from "morgan"; import { api } from "./package.js"; import { requestPVE, handleResponse, getDiskInfo, getDeviceInfo, getNodeAvailDevices } from "./pve.js"; -import { checkAuth, approveResources, getUserResources, getObjectHash } from "./utils.js"; +import { checkAuth, approveResources, getUserResources } from "./utils.js"; import { db, pveAPIToken, listenPort, hostname, domain } from "./db.js"; +import { setupClientSync } from "./clientsync.js"; const app = express(); app.use(bodyParser.urlencoded({ extended: true })); @@ -1196,32 +1197,7 @@ app.delete(`/api/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})/ await handleResponse(params.node, result, res); }); -/** - * GET - get hash of current cluster resources states - * Client can use this endpoint to check for cluster state changes to avoid costly data transfers to the client. - * responses: - * - 401: {auth: false} - * - 200: string - */ -app.get(`/api/cluster/statushash`, async (req, res) => { - // check auth - const auth = await checkAuth(req.cookies, res); - if (!auth) { - return; - } - // get current cluster resources - let status = (await requestPVE("/cluster/resources", "GET", req.cookies)).data.data; - // filter out just state information of resources that are needed - let resources = ["lxc", "qemu", "node"]; - let state = {}; - status.forEach((element) => { - if (resources.includes(element.type)) { - state[element.id] = element.status; - } - }); - res.status(200).send(getObjectHash(state)); -}); - -app.listen(listenPort, () => { +const server = app.listen(listenPort, () => { console.log(`proxmoxaas-api v${api.version} listening on port ${listenPort}`); }); +setupClientSync(app, server, db.getGlobalConfig().clientsync); diff --git a/src/utils.js b/src/utils.js index 0185849..a1fdd41 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,4 @@ -import {createHash} from "crypto"; +import { createHash } from "crypto"; import { getUsedResources, requestPVE } from "./pve.js"; import { db } from "./db.js"; @@ -94,9 +94,8 @@ export async function approveResources (req, username, request) { return approved; // if all requested resources pass, allow } - export function getObjectHash (object, alg = "sha256", format = "hex") { const hash = createHash(alg); hash.update(JSON.stringify(object, Object.keys(object).sort())); return hash.digest(format); -} \ No newline at end of file +}