Skip to content

Instantly share code, notes, and snippets.

@devsnek
Last active October 18, 2025 12:21
Show Gist options
  • Select an option

  • Save devsnek/f899a15b0f0d608416432bb25392f346 to your computer and use it in GitHub Desktop.

Select an option

Save devsnek/f899a15b0f0d608416432bb25392f346 to your computer and use it in GitHub Desktop.
Proxmox noVNC Proxy
<!DOCTYPE html>
<html>
<head>
<title>VNC</title>
<style>
html, body {
margin: 0;
height: 100%;
width: 100%;
}
#screen {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="screen"></div>
<script type="module">
import RFB from "/noVNC/core/rfb.js";
const ws = new WebSocket(window.location.href);
ws.addEventListener("message", (e) => {
const rfb = new RFB(document.getElementById("screen"), ws, {
credentials: { password: e.data },
});
rfb.addEventListener("desktopname", (e) => {
document.title = e.detail.name;
});
}, { once: true });
</script>
</body>
</html>
import fs from "node:fs/promises";
import { contentType } from "jsr:@std/media-types";
const TOKEN_ID = "proxmox token id";
const TOKEN_SECRET = "proxmox token secret";
const PVE = "http://my-pve:8006";
async function makeVnc(node: string, vmid: string) {
node = encodeURIComponent(node);
vmid = encodeURIComponent(vmid);
const vncproxy = await fetch(
`${PVE}/api2/json/nodes/${node}/qemu/${vmid}/vncproxy`,
{
method: "POST",
headers: {
Authorization: `PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}`,
"content-type": "application/json",
},
body: JSON.stringify({ websocket: 1, "generate-password": 1 }),
},
).then((r) => r.json());
return {
password: vncproxy.data.password,
url: `${PVE}/api2/json/nodes/${node}/qemu/${vmid}/vncwebsocket?port=${vncproxy.data.port}&vncticket=${encodeURIComponent(vncproxy.data.ticket)}`,
};
}
const STATIC: Record<string, { data: Uint8Array; contentType: string }> = {
"/": {
data: Deno.readFileSync("./index.html"),
contentType: contentType(".html"),
},
};
for await (const item of fs.glob("./noVNC/{core,vendor}/**/*.js")) {
STATIC[`/${item}`] = {
data: Deno.readFileSync(item),
contentType: contentType(".js"),
};
}
Deno.serve(async (req) => {
const url = new URL(req.url);
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
const result = await makeVnc(
url.searchParams.get("node")!,
url.searchParams.get("vmid")!,
);
const { socket, response } = Deno.upgradeWebSocket(req);
const ws = new WebSocket(result.url, {
headers: {
Authorization: `PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}`,
},
});
ws.onclose = (e) => socket.close(e.code > 1000 ? 1000 : e.code, e.reason);
ws.onmessage = (e) => socket.send(e.data);
socket.onopen = () => socket.send(result.password);
socket.onclose = (e) => ws.close(e.code > 1000 ? 1000 : e.code, e.reason);
socket.onmessage = (e) => ws.send(e.data);
return response;
}
const entry = STATIC[url.pathname];
if (!entry) {
return new Response("Not found", { status: 404 });
}
return new Response(entry.data, {
headers: {
"content-type": entry.contentType,
},
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment