- Go stdlib-only gateway: serves noVNC client, password auth, WS<->TCP relay - auth: PBKDF2-HMAC-SHA256 password hash, HMAC session cookies, login rate limit - relay: hand-written RFC 6455 WebSocket + transparent RFB bridge - vncspawner: cross-OS VNC server detection/launch (Windows/Linux/macOS) - server: /login /logout /vnc /api/status routes, session middleware, embed.FS - scripts: get-novnc, get-vnc (zip-verified), list-ips, open-firewall - run.bat: one-click launcher (asks only password), lists access IPs - README/AGENTS.md (Russian), .gitignore (root-anchored binaries)
106 lines
4.3 KiB
HTML
106 lines
4.3 KiB
HTML
<!doctype html>
|
|
<html lang="en" style="width:100%;height:100%">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Web VNC</title>
|
|
<style>
|
|
html,body{margin:0;padding:0;width:100%;height:100%;background:#000;overflow:hidden}
|
|
#screen{width:100%;height:100%;display:block}
|
|
#banner{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);max-width:580px;width:90%;
|
|
background:#0b1220;color:#e2e8f0;padding:1.4rem 1.6rem;border-radius:12px;font-family:system-ui,sans-serif;
|
|
box-shadow:0 10px 40px rgba(0,0,0,.6);line-height:1.45}
|
|
#banner h2{margin:0 0 .5rem;font-size:1.05rem}
|
|
#banner .addr{color:#93c5fd;font-family:monospace;background:#050a14;padding:.5rem .6rem;border-radius:8px;display:inline-block;margin:.3rem 0}
|
|
#banner .hint{color:#94a3b8;font-size:.85rem;margin-top:.7rem}
|
|
#banner button{margin-top:.9rem;padding:.5rem .9rem;border:none;border-radius:8px;background:#2563eb;color:#fff;font-weight:600;cursor:pointer;font-size:.9rem}
|
|
#banner button:hover{background:#1d4ed8}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="screen"></div>
|
|
<div id="banner" style="display:none"></div>
|
|
<script type="module">
|
|
const wsURL = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/vnc";
|
|
const statusURL = "/api/status";
|
|
const banner = document.getElementById("banner");
|
|
let VNC_PASSWORD = "";
|
|
function showBanner(html){ banner.innerHTML = html; banner.style.display = "block"; }
|
|
function hideBanner(){ banner.style.display = "none"; }
|
|
|
|
async function fetchStatus(){
|
|
try {
|
|
const r = await fetch(statusURL, { credentials: "same-origin" });
|
|
return await r.json();
|
|
} catch (e) { return null; }
|
|
}
|
|
|
|
async function loadRFB(){
|
|
try {
|
|
const mod = await import("./core/rfb.js");
|
|
return mod.default || mod.RFB || mod;
|
|
} catch (e) {
|
|
showBanner(`
|
|
<h2>noVNC client not bundled</h2>
|
|
<div>The noVNC web client assets are missing on the server.</div>
|
|
<div class="hint">On the server, run <code>scripts/get-novnc.ps1</code> (or <code>.sh</code>), then rebuild, then restart.</div>
|
|
<div class="hint">Expected file: <code>internal/server/static/core/rfb.js</code></div>`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
let rfb = null;
|
|
async function connect(){
|
|
const RFB = await loadRFB();
|
|
rfb = new RFB(document.getElementById("screen"), wsURL);
|
|
rfb.scaleViewport = true;
|
|
rfb.resizeSession = false;
|
|
rfb.showDotCursor = true;
|
|
rfb.addEventListener("connect", hideBanner);
|
|
// Auto-send the VNC server password so the user only types the web password.
|
|
rfb.addEventListener("credentialsrequired", (ev) => {
|
|
if (VNC_PASSWORD) {
|
|
rfb.sendCredentials({ password: VNC_PASSWORD });
|
|
}
|
|
});
|
|
rfb.addEventListener("securityfailure", (ev) => {
|
|
const reason = (ev.detail && ev.detail.reason) ? ev.detail.reason : "";
|
|
showBanner(`
|
|
<h2>VNC authentication failed</h2>
|
|
<div>The VNC server rejected the password${reason ? (": " + reason) : "."}</div>
|
|
<div class="hint">Configure the VNC server (UltraVNC/TightVNC) with the same password you use for the web gate, then retry.</div>
|
|
<button onclick="location.reload()">Retry</button>`);
|
|
});
|
|
rfb.addEventListener("disconnect", (ev) => {
|
|
const d = ev.detail || {};
|
|
showBanner(`
|
|
<h2>Disconnected</h2>
|
|
<div>Reason: ${d.reason || "unknown"}</div>
|
|
<button onclick="location.reload()">Retry</button>`);
|
|
});
|
|
window.addEventListener("beforeunload", () => { try { rfb.disconnect(); } catch(e){} });
|
|
}
|
|
|
|
async function start(){
|
|
const st = await fetchStatus();
|
|
if (st) VNC_PASSWORD = st.vncPassword || "";
|
|
if (st && st.vnc === false) {
|
|
const addr = st.vncAddr || "127.0.0.1:5900";
|
|
const hint = st.spawned
|
|
? "Auto-launch was attempted but the VNC server is not listening yet. Install UltraVNC/TightVNC on Windows (or x11vnc/TigerVNC on Linux), then restart."
|
|
: "No VNC server is running at that address. Start one (UltraVNC/TightVNC on Windows, or run <code>run.bat</code> with <code>--spawn</code>), then click Retry.";
|
|
showBanner(`
|
|
<h2>VNC server is not reachable</h2>
|
|
<div>The gateway cannot connect to the VNC server at:</div>
|
|
<div class="addr">${addr}</div>
|
|
<div class="hint">${hint}</div>
|
|
<button onclick="location.reload()">Retry</button>`);
|
|
connect().catch(()=>{});
|
|
} else {
|
|
connect().catch(()=>{});
|
|
}
|
|
}
|
|
start();
|
|
</script>
|
|
</body>
|
|
</html> |