Drop the iframe wrapper page and fix two real certificate bugs
Video and audio no longer share one iframe-based /screen/<name> page. Firefox refuses to load iframe content signed by a certificate whose warning hasn't been accepted at the top level, with no way to click through inside the iframe (Chrome is more lenient, which is why this briefly looked fine there). "Open Screen" is now a plain top-level link straight to noVNC's own URL (new tab, normal Accept-the-Risk applies), and the "Enable Sound" toggle moved to the main setup page - meant to be left open in its own tab while playing. Because that page now hosts a persistent AudioContext/WebSocket, its auto-refresh no longer fires just because a game is running (only during active installs), since a full-page refresh would have killed that connection every 3s. Two real certificate bugs found and fixed along the way: - CN=NY (a meaningless placeholder) -> CN=localhost + SAN (DNS:localhost, IP:127.0.0.1). Firefox validates the WebSocket-Secure connection's cert against the hostname independently of the page load and rejected the CN mismatch there even after the page-level warning was accepted. - Worse: adding -addext without also pinning basicConstraints=CA:FALSE left the cert defaulting to CA:TRUE - i.e. flagged as a Certificate Authority, not a server cert. Firefox hard-refuses that with no override option at all (not a normal clickable warning), which is why "the warning appears but won't let me proceed" even after the CN fix. Fixed by explicitly setting basicConstraints=critical,CA:FALSE plus keyUsage/extendedKeyUsage=serverAuth. Confirmed fixed: Firefox now shows the same clickable self-signed warning Chrome always did, and the noVNC connection completes successfully after accepting it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
This commit is contained in:
+24
-34
@@ -33,9 +33,21 @@ Audio is deliberately *not* per-game: server.sh starts one PulseAudio daemon
|
||||
and one pcm_ws_bridge.py (from docker-common) for the whole container's
|
||||
lifetime, capturing PulseAudio's single default sink. Every running game's
|
||||
audio mixes together there (normal PulseAudio behavior, no per-game routing
|
||||
needed), and every /screen/<name> page connects to that same shared
|
||||
AUDIO_PORT - simpler than per-slot audio isolation, at the cost of not being
|
||||
needed) - simpler than per-slot audio isolation, at the cost of not being
|
||||
able to tell games' audio apart when more than one is running.
|
||||
|
||||
Video and audio are deliberately on separate pages/tabs, not one wrapper
|
||||
page with an <iframe>: an iframe onto the noVNC HTTPS port hits browsers
|
||||
(Firefox in particular) refusing to load embedded content signed by a
|
||||
certificate whose warning hasn't been accepted at the top level, with no
|
||||
way to click through *inside* the iframe. So "Open Screen" is a plain
|
||||
top-level link straight to noVNC's own URL (opens in a new tab, normal
|
||||
"Accept the Risk and Continue" applies), and the one "Enable Sound" toggle
|
||||
lives on this page instead, meant to be left open in its own tab for as
|
||||
long as you're playing. Because of that, this page's auto-refresh is
|
||||
deliberately limited to only while an install is actively in progress -
|
||||
refreshing while a game is running would tear down the live AudioContext/
|
||||
WebSocket every few seconds.
|
||||
"""
|
||||
|
||||
import html
|
||||
@@ -253,7 +265,7 @@ def stop_game(name):
|
||||
running_procs.pop(name, None)
|
||||
|
||||
|
||||
def render_page():
|
||||
def render_page(host):
|
||||
games = discover_games()
|
||||
sizes = get_zip_sizes(games)
|
||||
with state_lock:
|
||||
@@ -265,7 +277,9 @@ def render_page():
|
||||
slots_used = len(novnc_ports)
|
||||
|
||||
any_pending = any(v in ("downloading", "extracting") for v in pending.values())
|
||||
refresh_tag = '<meta http-equiv="refresh" content="3">' if any_pending or slots_used else ""
|
||||
# Deliberately not refreshing just because slots_used > 0: this page hosts the one
|
||||
# persistent "Enable Sound" AudioContext/WebSocket, and a full-page refresh would kill it.
|
||||
refresh_tag = '<meta http-equiv="refresh" content="3">' if any_pending else ""
|
||||
|
||||
def button(action, name, label):
|
||||
return (
|
||||
@@ -285,7 +299,7 @@ def render_page():
|
||||
screen_html = ""
|
||||
if name in novnc_ports:
|
||||
status_html = '<span class="status busy">Running</span>'
|
||||
screen_html = f'<a href="/screen/{name}" target="_blank">Open Screen</a>'
|
||||
screen_html = f'<a href="https://{host}:{novnc_ports[name]}/" target="_blank">Open Screen</a>'
|
||||
actions = button("stop", name, "Stop")
|
||||
elif status in ("downloading", "extracting"):
|
||||
status_html = f'<span class="status busy">{status}...</span>'
|
||||
@@ -322,33 +336,17 @@ button {{ padding: 0.3em 0.8em; margin-right: 0.3em; }}
|
||||
.status.busy {{ color: #a70; font-weight: bold; }}
|
||||
.status.err {{ color: #c22; }}
|
||||
.slots {{ margin-bottom: 1.5em; }}
|
||||
#audio-btn {{ padding: 0.4em 1em; margin-bottom: 1.5em; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="audio-btn">Enable Sound</button>
|
||||
<p class="slots">{slots_used}/{MAX_CONCURRENT_GAMES} game slots in use</p>
|
||||
<h1>DOSBox games</h1>
|
||||
<table>
|
||||
<tr><th>Game</th><th>Release</th><th>Publisher</th><th>Size</th><th>Status</th><th>Actions</th><th></th></tr>
|
||||
{''.join(rows) if rows else '<tr><td colspan="7">No game manifests found on the share</td></tr>'}
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def render_screen_page(name, novnc_port, host):
|
||||
return f"""<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{html.escape(name)}</title>
|
||||
<style>
|
||||
html, body {{ margin: 0; padding: 0; height: 100%; overflow: hidden; }}
|
||||
iframe {{ border: none; width: 100%; height: 100%; }}
|
||||
#audio-btn {{ position: fixed; top: 1em; right: 1em; z-index: 10; padding: 0.5em 1em; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="audio-btn">Enable Sound</button>
|
||||
<iframe src="https://{host}:{novnc_port}/vnc.html?autoconnect=true"></iframe>
|
||||
<script>
|
||||
document.getElementById('audio-btn').addEventListener('click', async () => {{
|
||||
const btn = document.getElementById('audio-btn');
|
||||
@@ -360,7 +358,7 @@ document.getElementById('audio-btn').addEventListener('click', async () => {{
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onmessage = (event) => node.port.postMessage(event.data, [event.data]);
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Sound enabled';
|
||||
btn.textContent = 'Sound enabled - keep this tab open while playing';
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
@@ -395,18 +393,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
if path == "/":
|
||||
self._send_html(render_page())
|
||||
host = self.headers.get("Host", "localhost").rsplit(":", 1)[0]
|
||||
self._send_html(render_page(host))
|
||||
elif path == "/pcm-worklet.js":
|
||||
self._send_file(os.path.join(SCRIPTS_HOME, "pcm-worklet.js"), "application/javascript")
|
||||
elif path.startswith("/screen/"):
|
||||
name = path[len("/screen/"):]
|
||||
with state_lock:
|
||||
info = running_procs.get(name)
|
||||
if info is None:
|
||||
self._send_html(f"<p>{html.escape(name)} is not running.</p>", status=404)
|
||||
return
|
||||
host = self.headers.get("Host", "localhost").rsplit(":", 1)[0]
|
||||
self._send_html(render_screen_page(name, info["novnc_port"], host))
|
||||
else:
|
||||
self._send_html("Not found", status=404)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user