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.connected { color: var(--green); }
|
||||
#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 {
|
||||
display: flex; justify-content: center; align-items: flex-start;
|
||||
width: 100%; height: calc(100% - 36px); overflow: auto;
|
||||
|
|
@ -52,10 +64,15 @@ canvas { display: block; cursor: default; }
|
|||
<div id="toolbar">
|
||||
<strong>🖥 screen</strong>
|
||||
<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>
|
||||
</div>
|
||||
<div id="canvas-wrap"><canvas id="c"></canvas></div>
|
||||
<div id="msg"></div>
|
||||
<div id="debug-log"></div>
|
||||
|
||||
<script>
|
||||
// Minimal RFB-over-WebSocket renderer.
|
||||
|
|
@ -73,10 +90,34 @@ canvas { display: block; cursor: default; }
|
|||
const ctx = canvas.getContext('2d');
|
||||
const status = document.getElementById('status');
|
||||
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) {
|
||||
status.textContent = text;
|
||||
status.className = cls || '';
|
||||
if (cls === 'error') dbg('ERROR: ' + text, 'err');
|
||||
}
|
||||
|
||||
function flash(text) {
|
||||
|
|
@ -90,7 +131,7 @@ canvas { display: block; cursor: default; }
|
|||
const ws = new WebSocket(`${proto}://${location.host}/screen/ws`);
|
||||
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.onclose = (e) => {
|
||||
setStatus(`disconnected (${e.code})`, 'error');
|
||||
|
|
@ -138,7 +179,11 @@ canvas { display: block; cursor: default; }
|
|||
}
|
||||
|
||||
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; }
|
||||
|
|
@ -155,6 +200,7 @@ canvas { display: block; cursor: default; }
|
|||
case 'version': {
|
||||
const b = drainTo(12);
|
||||
if (!b) return false;
|
||||
dbg('← server version: ' + new TextDecoder().decode(b).replace('\n','\\n'));
|
||||
// Send back same version (RFB 003.008)
|
||||
send(new TextEncoder().encode('RFB 003.008\n'));
|
||||
state = 'security-types';
|
||||
|
|
@ -164,15 +210,19 @@ canvas { display: block; cursor: default; }
|
|||
const b = drainTo(1);
|
||||
if (!b) return false;
|
||||
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);
|
||||
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
|
||||
// even with --disable-transport-layer-security), else first offered.
|
||||
let prefer;
|
||||
if (types.indexOf(1) !== -1) prefer = 1; // plain None
|
||||
else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt
|
||||
else prefer = types[0];
|
||||
dbg('→ choosing security type ' + prefer +
|
||||
(prefer === 1 ? ' (None)' : prefer === 19 ? ' (VeNCrypt)' : prefer === 2 ? ' (VncAuth)' : ''));
|
||||
send(new Uint8Array([prefer]));
|
||||
if (prefer === 1) state = 'security-result';
|
||||
else if (prefer === 19) state = 'vencrypt-version';
|
||||
|
|
@ -185,6 +235,7 @@ canvas { display: block; cursor: default; }
|
|||
// weston VNC which uses None via VeNCrypt.
|
||||
const b = drainTo(16);
|
||||
if (!b) return false;
|
||||
dbg('← vnc-challenge (16 bytes): ' + hex(b));
|
||||
send(new Uint8Array(16));
|
||||
state = 'security-result';
|
||||
return true;
|
||||
|
|
@ -196,6 +247,7 @@ canvas { display: block; cursor: default; }
|
|||
// Server sends: major (u8), minor (u8) — e.g. 0, 2
|
||||
const b = drainTo(2);
|
||||
if (!b) return false;
|
||||
dbg('← VeNCrypt version: ' + b[0] + '.' + b[1]);
|
||||
// Echo same version back
|
||||
send(new Uint8Array([b[0], b[1]]));
|
||||
state = 'vencrypt-subtypes';
|
||||
|
|
@ -206,14 +258,17 @@ canvas { display: block; cursor: default; }
|
|||
const nb = drainTo(1);
|
||||
if (!nb) return false;
|
||||
const nSub = nb[0];
|
||||
dbg('← VeNCrypt nSubtypes=' + nSub);
|
||||
const raw = drainTo(nSub * 4);
|
||||
if (!raw) { chunks.unshift(nb); totalBytes += 1; return false; }
|
||||
// Build sub-type array from big-endian u32s
|
||||
const subs = [];
|
||||
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.
|
||||
// Fall back to first offered.
|
||||
const sub = subs.includes(1) ? 1 : subs[0];
|
||||
dbg('→ choosing VeNCrypt sub-type ' + sub);
|
||||
// Send chosen sub-type as big-endian u32
|
||||
send(new Uint8Array([sub>>>24, (sub>>>16)&0xff, (sub>>>8)&0xff, sub&0xff]));
|
||||
state = 'vencrypt-accept';
|
||||
|
|
@ -223,6 +278,7 @@ canvas { display: block; cursor: default; }
|
|||
// Server sends 1 byte: 1=accepted, 0=refused
|
||||
const b = drainTo(1);
|
||||
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; }
|
||||
// Sub-type 1 (None): proceed to SecurityResult
|
||||
state = 'security-result';
|
||||
|
|
@ -232,7 +288,9 @@ canvas { display: block; cursor: default; }
|
|||
case 'security-result': {
|
||||
const b = drainTo(4);
|
||||
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
|
||||
send(new Uint8Array([1]));
|
||||
state = 'server-init';
|
||||
|
|
@ -253,6 +311,7 @@ canvas { display: block; cursor: default; }
|
|||
const nameLen = u32be(b, 20);
|
||||
const nameBytes = drainTo(nameLen);
|
||||
if (!nameBytes) { chunks.unshift(b); totalBytes += 24; return false; }
|
||||
dbg('← server-init: ' + fbW + 'x' + fbH + ' bpp=' + pixelFormat.bpp, 'ok');
|
||||
canvas.width = fbW;
|
||||
canvas.height = fbH;
|
||||
setStatus('connected', 'connected');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue