screen: add RFB debug log panel for handshake diagnosis
This commit is contained in:
parent
1e325c84f2
commit
86e4f41203
1 changed files with 67 additions and 8 deletions
|
|
@ -33,6 +33,18 @@ html, body { height: 100%; background: var(--base); color: var(--text);
|
||||||
#status { margin-left: auto; font-size: 0.75rem; color: var(--subtext0); }
|
#status { margin-left: auto; font-size: 0.75rem; color: var(--subtext0); }
|
||||||
#status.connected { color: var(--green); }
|
#status.connected { color: var(--green); }
|
||||||
#status.error { color: var(--red); }
|
#status.error { color: var(--red); }
|
||||||
|
#debug-log {
|
||||||
|
position: fixed; bottom: 0; left: 0; right: 0; max-height: 40vh;
|
||||||
|
overflow-y: auto; background: rgba(17,17,27,0.95);
|
||||||
|
border-top: 1px solid var(--surface1);
|
||||||
|
font-size: 0.72rem; font-family: ui-monospace, monospace;
|
||||||
|
padding: 0.4rem 0.6rem; z-index: 100;
|
||||||
|
display: none; /* hidden by default; toggled by toolbar button */
|
||||||
|
}
|
||||||
|
#debug-log .dbg-line { color: var(--subtext0); margin: 1px 0; white-space: pre; }
|
||||||
|
#debug-log .dbg-line.err { color: var(--red); }
|
||||||
|
#debug-log .dbg-line.ok { color: var(--green); }
|
||||||
|
#debug-log .dbg-line.send { color: var(--blue); }
|
||||||
#canvas-wrap {
|
#canvas-wrap {
|
||||||
display: flex; justify-content: center; align-items: flex-start;
|
display: flex; justify-content: center; align-items: flex-start;
|
||||||
width: 100%; height: calc(100% - 36px); overflow: auto;
|
width: 100%; height: calc(100% - 36px); overflow: auto;
|
||||||
|
|
@ -52,10 +64,15 @@ canvas { display: block; cursor: default; }
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<strong>🖥 screen</strong>
|
<strong>🖥 screen</strong>
|
||||||
<a href="/" title="back to agent page">← agent</a>
|
<a href="/" title="back to agent page">← agent</a>
|
||||||
|
<button id="debug-toggle" title="Toggle RFB debug log"
|
||||||
|
style="margin-left:0.5rem;padding:0.15rem 0.5rem;font-size:0.72rem;
|
||||||
|
background:var(--surface0);color:var(--subtext0);border:1px solid var(--surface1);
|
||||||
|
border-radius:4px;cursor:pointer;">debug</button>
|
||||||
<span id="status">connecting…</span>
|
<span id="status">connecting…</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="canvas-wrap"><canvas id="c"></canvas></div>
|
<div id="canvas-wrap"><canvas id="c"></canvas></div>
|
||||||
<div id="msg"></div>
|
<div id="msg"></div>
|
||||||
|
<div id="debug-log"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Minimal RFB-over-WebSocket renderer.
|
// Minimal RFB-over-WebSocket renderer.
|
||||||
|
|
@ -73,10 +90,34 @@ canvas { display: block; cursor: default; }
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const msg = document.getElementById('msg');
|
const msg = document.getElementById('msg');
|
||||||
|
const debugLog = document.getElementById('debug-log');
|
||||||
|
const debugBtn = document.getElementById('debug-toggle');
|
||||||
|
|
||||||
|
// --- Debug log ---
|
||||||
|
let debugVisible = false;
|
||||||
|
debugBtn.addEventListener('click', () => {
|
||||||
|
debugVisible = !debugVisible;
|
||||||
|
debugLog.style.display = debugVisible ? 'block' : 'none';
|
||||||
|
debugBtn.style.color = debugVisible ? 'var(--green)' : 'var(--subtext0)';
|
||||||
|
});
|
||||||
|
|
||||||
|
function hex(bytes) {
|
||||||
|
return Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbg(text, cls) {
|
||||||
|
console.log('[rfb]', text);
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'dbg-line' + (cls ? ' ' + cls : '');
|
||||||
|
line.textContent = text;
|
||||||
|
debugLog.appendChild(line);
|
||||||
|
debugLog.scrollTop = debugLog.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
function setStatus(text, cls) {
|
function setStatus(text, cls) {
|
||||||
status.textContent = text;
|
status.textContent = text;
|
||||||
status.className = cls || '';
|
status.className = cls || '';
|
||||||
|
if (cls === 'error') dbg('ERROR: ' + text, 'err');
|
||||||
}
|
}
|
||||||
|
|
||||||
function flash(text) {
|
function flash(text) {
|
||||||
|
|
@ -90,7 +131,7 @@ canvas { display: block; cursor: default; }
|
||||||
const ws = new WebSocket(`${proto}://${location.host}/screen/ws`);
|
const ws = new WebSocket(`${proto}://${location.host}/screen/ws`);
|
||||||
ws.binaryType = 'arraybuffer';
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
ws.onopen = () => setStatus('connected', 'connected');
|
ws.onopen = () => { dbg('WebSocket open — starting RFB handshake', 'ok'); setStatus('handshaking…'); };
|
||||||
ws.onerror = () => setStatus('connection error', 'error');
|
ws.onerror = () => setStatus('connection error', 'error');
|
||||||
ws.onclose = (e) => {
|
ws.onclose = (e) => {
|
||||||
setStatus(`disconnected (${e.code})`, 'error');
|
setStatus(`disconnected (${e.code})`, 'error');
|
||||||
|
|
@ -138,7 +179,11 @@ canvas { display: block; cursor: default; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(data) {
|
function send(data) {
|
||||||
if (ws.readyState === WebSocket.OPEN) ws.send(data);
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
const arr = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||||
|
dbg('→ send [' + arr.length + 'b]: ' + hex(arr.slice(0, 32)) + (arr.length > 32 ? '…' : ''), 'send');
|
||||||
|
ws.send(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function u32be(b, o) { return ((b[o]<<24)|(b[o+1]<<16)|(b[o+2]<<8)|b[o+3])>>>0; }
|
function u32be(b, o) { return ((b[o]<<24)|(b[o+1]<<16)|(b[o+2]<<8)|b[o+3])>>>0; }
|
||||||
|
|
@ -155,6 +200,7 @@ canvas { display: block; cursor: default; }
|
||||||
case 'version': {
|
case 'version': {
|
||||||
const b = drainTo(12);
|
const b = drainTo(12);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
|
dbg('← server version: ' + new TextDecoder().decode(b).replace('\n','\\n'));
|
||||||
// Send back same version (RFB 003.008)
|
// Send back same version (RFB 003.008)
|
||||||
send(new TextEncoder().encode('RFB 003.008\n'));
|
send(new TextEncoder().encode('RFB 003.008\n'));
|
||||||
state = 'security-types';
|
state = 'security-types';
|
||||||
|
|
@ -164,15 +210,19 @@ canvas { display: block; cursor: default; }
|
||||||
const b = drainTo(1);
|
const b = drainTo(1);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
const n = b[0];
|
const n = b[0];
|
||||||
if (n === 0) { state = 'error'; return false; }
|
dbg('← security-types: count=' + n + (n === 0 ? ' (server error!)' : ''));
|
||||||
|
if (n === 0) { setStatus('server sent 0 security types', 'error'); return false; }
|
||||||
const types = drainTo(n);
|
const types = drainTo(n);
|
||||||
if (!types) { chunks.unshift(b); totalBytes += 1; return false; }
|
if (!types) { chunks.unshift(b); totalBytes += 1; return false; }
|
||||||
|
dbg('← security-types offered: [' + Array.from(types).join(', ') + ']');
|
||||||
// Prefer type 1 (None), then type 19 (VeNCrypt — used by neatvnc/weston
|
// Prefer type 1 (None), then type 19 (VeNCrypt — used by neatvnc/weston
|
||||||
// even with --disable-transport-layer-security), else first offered.
|
// even with --disable-transport-layer-security), else first offered.
|
||||||
let prefer;
|
let prefer;
|
||||||
if (types.indexOf(1) !== -1) prefer = 1; // plain None
|
if (types.indexOf(1) !== -1) prefer = 1; // plain None
|
||||||
else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt
|
else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt
|
||||||
else prefer = types[0];
|
else prefer = types[0];
|
||||||
|
dbg('→ choosing security type ' + prefer +
|
||||||
|
(prefer === 1 ? ' (None)' : prefer === 19 ? ' (VeNCrypt)' : prefer === 2 ? ' (VncAuth)' : ''));
|
||||||
send(new Uint8Array([prefer]));
|
send(new Uint8Array([prefer]));
|
||||||
if (prefer === 1) state = 'security-result';
|
if (prefer === 1) state = 'security-result';
|
||||||
else if (prefer === 19) state = 'vencrypt-version';
|
else if (prefer === 19) state = 'vencrypt-version';
|
||||||
|
|
@ -185,6 +235,7 @@ canvas { display: block; cursor: default; }
|
||||||
// weston VNC which uses None via VeNCrypt.
|
// weston VNC which uses None via VeNCrypt.
|
||||||
const b = drainTo(16);
|
const b = drainTo(16);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
|
dbg('← vnc-challenge (16 bytes): ' + hex(b));
|
||||||
send(new Uint8Array(16));
|
send(new Uint8Array(16));
|
||||||
state = 'security-result';
|
state = 'security-result';
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -196,6 +247,7 @@ canvas { display: block; cursor: default; }
|
||||||
// Server sends: major (u8), minor (u8) — e.g. 0, 2
|
// Server sends: major (u8), minor (u8) — e.g. 0, 2
|
||||||
const b = drainTo(2);
|
const b = drainTo(2);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
|
dbg('← VeNCrypt version: ' + b[0] + '.' + b[1]);
|
||||||
// Echo same version back
|
// Echo same version back
|
||||||
send(new Uint8Array([b[0], b[1]]));
|
send(new Uint8Array([b[0], b[1]]));
|
||||||
state = 'vencrypt-subtypes';
|
state = 'vencrypt-subtypes';
|
||||||
|
|
@ -206,14 +258,17 @@ canvas { display: block; cursor: default; }
|
||||||
const nb = drainTo(1);
|
const nb = drainTo(1);
|
||||||
if (!nb) return false;
|
if (!nb) return false;
|
||||||
const nSub = nb[0];
|
const nSub = nb[0];
|
||||||
|
dbg('← VeNCrypt nSubtypes=' + nSub);
|
||||||
const raw = drainTo(nSub * 4);
|
const raw = drainTo(nSub * 4);
|
||||||
if (!raw) { chunks.unshift(nb); totalBytes += 1; return false; }
|
if (!raw) { chunks.unshift(nb); totalBytes += 1; return false; }
|
||||||
// Build sub-type array from big-endian u32s
|
// Build sub-type array from big-endian u32s
|
||||||
const subs = [];
|
const subs = [];
|
||||||
for (let i = 0; i < nSub; i++) subs.push(u32be(raw, i * 4));
|
for (let i = 0; i < nSub; i++) subs.push(u32be(raw, i * 4));
|
||||||
|
dbg('← VeNCrypt sub-types: [' + subs.join(', ') + ']');
|
||||||
// Prefer sub-type 1 (VeNCrypt None) — no TLS, no password.
|
// Prefer sub-type 1 (VeNCrypt None) — no TLS, no password.
|
||||||
// Fall back to first offered.
|
// Fall back to first offered.
|
||||||
const sub = subs.includes(1) ? 1 : subs[0];
|
const sub = subs.includes(1) ? 1 : subs[0];
|
||||||
|
dbg('→ choosing VeNCrypt sub-type ' + sub);
|
||||||
// Send chosen sub-type as big-endian u32
|
// Send chosen sub-type as big-endian u32
|
||||||
send(new Uint8Array([sub>>>24, (sub>>>16)&0xff, (sub>>>8)&0xff, sub&0xff]));
|
send(new Uint8Array([sub>>>24, (sub>>>16)&0xff, (sub>>>8)&0xff, sub&0xff]));
|
||||||
state = 'vencrypt-accept';
|
state = 'vencrypt-accept';
|
||||||
|
|
@ -223,6 +278,7 @@ canvas { display: block; cursor: default; }
|
||||||
// Server sends 1 byte: 1=accepted, 0=refused
|
// Server sends 1 byte: 1=accepted, 0=refused
|
||||||
const b = drainTo(1);
|
const b = drainTo(1);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
|
dbg('← VeNCrypt accept byte: ' + b[0] + (b[0] === 1 ? ' (ok)' : ' (REFUSED)'));
|
||||||
if (b[0] !== 1) { setStatus('VeNCrypt sub-type refused', 'error'); return false; }
|
if (b[0] !== 1) { setStatus('VeNCrypt sub-type refused', 'error'); return false; }
|
||||||
// Sub-type 1 (None): proceed to SecurityResult
|
// Sub-type 1 (None): proceed to SecurityResult
|
||||||
state = 'security-result';
|
state = 'security-result';
|
||||||
|
|
@ -232,7 +288,9 @@ canvas { display: block; cursor: default; }
|
||||||
case 'security-result': {
|
case 'security-result': {
|
||||||
const b = drainTo(4);
|
const b = drainTo(4);
|
||||||
if (!b) return false;
|
if (!b) return false;
|
||||||
if (u32be(b, 0) !== 0) { setStatus('auth failed', 'error'); return false; }
|
const code = u32be(b, 0);
|
||||||
|
dbg('← security-result: ' + code + ' (bytes: ' + hex(b) + ')' + (code === 0 ? ' ✓' : ' FAIL'), code === 0 ? 'ok' : 'err');
|
||||||
|
if (code !== 0) { setStatus('auth failed (code ' + code + ')', 'error'); return false; }
|
||||||
// ClientInit: shared flag = 1
|
// ClientInit: shared flag = 1
|
||||||
send(new Uint8Array([1]));
|
send(new Uint8Array([1]));
|
||||||
state = 'server-init';
|
state = 'server-init';
|
||||||
|
|
@ -253,6 +311,7 @@ canvas { display: block; cursor: default; }
|
||||||
const nameLen = u32be(b, 20);
|
const nameLen = u32be(b, 20);
|
||||||
const nameBytes = drainTo(nameLen);
|
const nameBytes = drainTo(nameLen);
|
||||||
if (!nameBytes) { chunks.unshift(b); totalBytes += 24; return false; }
|
if (!nameBytes) { chunks.unshift(b); totalBytes += 24; return false; }
|
||||||
|
dbg('← server-init: ' + fbW + 'x' + fbH + ' bpp=' + pixelFormat.bpp, 'ok');
|
||||||
canvas.width = fbW;
|
canvas.width = fbW;
|
||||||
canvas.height = fbH;
|
canvas.height = fbH;
|
||||||
setStatus('connected', 'connected');
|
setStatus('connected', 'connected');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue