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
This commit is contained in:
Codex
2026-07-31 10:51:39 +03:00
parent 71e2fef4d9
commit 120d4e9498
5 changed files with 51 additions and 3 deletions
+18 -1
View File
@@ -58,7 +58,7 @@ internal/auth PBKDF2-HMAC-SHA256, HMAC session-cookie, rate-limit
internal/relay websocket.go — RFC6455 на stdlib; relay.go — WS<->TCP internal/relay websocket.go — RFC6455 на stdlib; relay.go — WS<->TCP
internal/vncspawner кросс-ОС поиск/запуск VNC-сервера (build-теги по ОС) internal/vncspawner кросс-ОС поиск/запуск VNC-сервера (build-теги по ОС)
internal/server HTTP-роуты, /api/status, middleware сессии, embed.FS internal/server HTTP-роуты, /api/status, middleware сессии, embed.FS
internal/server/static встроенные ассеты (vnc.html + noVNC core/app/vendor) internal/server/static встроенные ассеты (vnc.html, favicon.ico + noVNC core/app/vendor)
scripts/get-novnc.{ps1,sh} скачать noVNC scripts/get-novnc.{ps1,sh} скачать noVNC
scripts/get-vnc.ps1 скачать портативный UltraVNC в vnc/ (с верификацией zip) scripts/get-vnc.ps1 скачать портативный UltraVNC в vnc/ (с верификацией zip)
scripts/list-ips.ps1 список IPv4 машины (используется run.bat) scripts/list-ips.ps1 список IPv4 машины (используется run.bat)
@@ -92,6 +92,23 @@ run.bat запуск в один клик (спрашивае
`list-ips.ps1` имеет фолбэк на `ipconfig`). Запущенный web-vnc.exe из `list-ips.ps1` имеет фолбэк на `ipconfig`). Запущенный web-vnc.exe из
фонового job может остаться «зомби» (Stop-Process иногда access denied); фонового job может остаться «зомби» (Stop-Process иногда access denied);
используй разные порты для тестов и по возможности запускай killable-способом. используй разные порты для тестов и по возможности запускай killable-способом.
- **favicon.ico:** браузер запрашивает `/favicon.ico` на каждой странице
(включая форму логина). Роут `/favicon.ico` зарегистрирован **вне** session-
middleware (чтобы без сессии не редиректил на /login и не давал 404) и отдаёт
встроенный `internal/server/static/favicon.ico` (иконка-монитор, коммитится;
не входит в скачиваемые noVNC-ассеты). На логине и vnc.html есть
`<link rel="icon" href="/favicon.ico">`.
- **Secure context (TLS) в noVNC:** noVNC (`core/rfb.js`, `app/ui.js`) печатает
`Log.Error("noVNC requires a secure context (TLS). Expect crashes!")` при
`!window.isSecureContext` — т.е. на plain-http по LAN-IP. Для обычной VNC-
password-авторизации это **безобидно**: noVNC использует чистый-JS DES из
`core/des.js` и НЕ нуждается в `crypto.subtle`. Чтобы не пугать пользователя
красной ошибкой в консоли, обёртка `vnc.html` ДО импорта `core/rfb.js`
оборачивает `console.error` фильтром, гасящим **только** эту строку (всё
остальное проходит в оригинальный `console.error`). Файлы noVNC (core/, app/,
…) НЕ правятся — они gitignored и перезаливаются `scripts/get-novnc.*`.
Корректный способ сделать контекст действительно secure — HTTPS (self-signed),
но это отдельная фича; пока — фильтр в vnc.html.
- **Hijack WebSocket:** `internal/relay` сам делает апгрейд через - **Hijack WebSocket:** `internal/relay` сам делает апгрейд через
`http.Hijacker`; гейтвей НЕ использует gorilla/websocket. `http.Hijacker`; гейтвей НЕ использует gorilla/websocket.
+18
View File
@@ -58,6 +58,11 @@ func (s *Server) Handler() http.Handler {
mux.Handle(s.cfg.RelayPath, s.requireSession(http.HandlerFunc(s.handleRelay))) mux.Handle(s.cfg.RelayPath, s.requireSession(http.HandlerFunc(s.handleRelay)))
mux.Handle(s.cfg.NoVNCPath, s.requireSession(http.HandlerFunc(s.serveNoVNCPage))) mux.Handle(s.cfg.NoVNCPath, s.requireSession(http.HandlerFunc(s.serveNoVNCPage)))
// favicon is fetched by the browser on every page (incl. the login form);
// serve it without a session so it doesn't redirect to /login and 404.
mux.HandleFunc("/favicon.ico", s.handleFavicon)
mux.Handle("/", s.requireSession(http.HandlerFunc(s.handleIndexOrStatic))) mux.Handle("/", s.requireSession(http.HandlerFunc(s.handleIndexOrStatic)))
return s.logRequest(mux) return s.logRequest(mux)
@@ -110,6 +115,19 @@ func (s *Server) handleRelay(w http.ResponseWriter, r *http.Request) {
s.relay.ServeHTTP(w, r) s.relay.ServeHTTP(w, r)
} }
// handleFavicon serves the embedded favicon.ico with a long cache. It is
// registered outside requireSession so it loads on the login page too.
func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) {
data, err := fs.ReadFile(s.static, "favicon.ico")
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image/x-icon")
w.Header().Set("Cache-Control", "public, max-age=604800")
_, _ = w.Write(data)
}
func (s *Server) serveNoVNCPage(w http.ResponseWriter, r *http.Request) { func (s *Server) serveNoVNCPage(w http.ResponseWriter, r *http.Request) {
if data, err := fs.ReadFile(s.static, "vnc.html"); err == nil { if data, err := fs.ReadFile(s.static, "vnc.html"); err == nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

+14 -1
View File
@@ -4,6 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Web VNC</title> <title>Web VNC</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<style> <style>
html,body{margin:0;padding:0;width:100%;height:100%;background:#000;overflow:hidden} html,body{margin:0;padding:0;width:100%;height:100%;background:#000;overflow:hidden}
#screen{width:100%;height:100%;display:block} #screen{width:100%;height:100%;display:block}
@@ -21,6 +22,18 @@
<div id="screen"></div> <div id="screen"></div>
<div id="banner" style="display:none"></div> <div id="banner" style="display:none"></div>
<script type="module"> <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 wsURL = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/vnc";
const statusURL = "/api/status"; const statusURL = "/api/status";
const banner = document.getElementById("banner"); const banner = document.getElementById("banner");
@@ -85,7 +98,7 @@ async function connect(){
showBanner(` showBanner(`
<h2>Disconnected</h2> <h2>Disconnected</h2>
<div>Reason: ${clean ? "connection closed" : "connection lost"}. See the browser console (F12) for the exact error.</div> <div>Reason: ${clean ? "connection closed" : "connection lost"}. See the browser console (F12) for the exact error.</div>
<div class="hint">The "noVNC requires a secure context (TLS)" console warning is harmless for plain VNC-password auth. A real failure is usually a VNC password mismatch.</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>`); <button onclick="location.reload()">Retry</button>`);
}); });
window.addEventListener("beforeunload", () => { try { rfb.disconnect(); } catch(e){} }); window.addEventListener("beforeunload", () => { try { rfb.disconnect(); } catch(e){} });
+1 -1
View File
@@ -19,7 +19,7 @@ func (s *Server) renderLogin(w http.ResponseWriter, errMsg string) {
const loginPageHTML = `<!doctype html> const loginPageHTML = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"> <html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Web VNC</title><style> <title>Web VNC</title><link rel="icon" href="/favicon.ico" type="image/x-icon"><style>
*{box-sizing:border-box} *{box-sizing:border-box}
body{font-family:system-ui,Segoe UI,sans-serif;background:#0f1720;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0} body{font-family:system-ui,Segoe UI,sans-serif;background:#0f1720;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
.card{background:#111827;padding:2rem 2.25rem;border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.5);width:320px} .card{background:#111827;padding:2rem 2.25rem;border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.5);width:320px}