Last active
October 18, 2025 12:21
-
-
Save devsnek/f899a15b0f0d608416432bb25392f346 to your computer and use it in GitHub Desktop.
Proxmox noVNC Proxy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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