Files
web-vnc/internal/server/static/vnc.html
T
Codex 120d4e9498 Add embedded favicon and suppress noVNC secure-context warning
- serve /favicon.ico outside session middleware so it loads on the login
  page too (was 404 via the catch-all -> requireSession redirect)
- add embedded internal/server/static/favicon.ico (monitor icon) and
  <link rel=icon> on the login page and vnc.html
- in vnc.html wrap console.error before importing core/rfb.js to drop only
  the harmless 'noVNC requires a secure context (TLS). Expect crashes!' line
  (plain VNC-password auth uses pure-JS DES, not crypto.subtle); reword the
  now-redundant disconnect hint
- update AGENTS.md with the favicon route and the secure-context note
2026-07-31 10:51:39 +03:00

130 lines
5.9 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>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<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">
// noVNC prints "noVNC requires a secure context (TLS). Expect crashes!" whenever
// window.isSecureContext is false (i.e. plain http over a LAN IP). For ordinary
// VNC-password auth noVNC uses its own pure-JS DES (core/des.js) and does NOT need
// crypto.subtle, so the warning is purely cosmetic. Suppress exactly that one line
// (let every other console.error through) by wrapping console.error before rfb.js
// imports and binds Log.Error to it.
const _origConsoleError = console.error.bind(console);
const _SECURE_CTX_RE = /noVNC requires a secure context \(TLS\)/;
console.error = function(...args) {
if (args.length && typeof args[0] === "string" && _SECURE_CTX_RE.test(args[0])) return;
_origConsoleError(...args);
};
const wsURL = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/vnc";
const statusURL = "/api/status";
const banner = document.getElementById("banner");
let VNC_PASSWORD = "";
// a securityfailure / unreachable-VNC banner is more specific than the generic
// disconnect event, so we must not let "disconnect" clobber it.
let specificError = false;
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) => {
specificError = true;
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">The VNC server password (UltraVNC: the "VNC Password" field) must equal the password you type in run.bat. VNC passwords are effectively the first 8 bytes — use plain ASCII, max 8 chars.</div>
<button onclick="location.reload()">Retry</button>`);
});
rfb.addEventListener("disconnect", (ev) => {
// noVNC's "disconnect" event detail is { clean } only — it never carries a reason.
// The real cause was already reported via "securityfailure" or the unreachable-VNC
// banner, so do not overwrite it with a useless "Reason: unknown".
if (specificError) return;
const d = ev.detail || {};
const clean = !!(d && d.clean);
showBanner(`
<h2>Disconnected</h2>
<div>Reason: ${clean ? "connection closed" : "connection lost"}. See the browser console (F12) for the exact error.</div>
<div class="hint">A real failure is usually a VNC password mismatch. See the browser console (F12) for details.</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) {
specificError = true;
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>