The Go-Live broadcast looked badly choppy: video and scrolling stuttered while the cursor stayed smooth. Root cause is TigerVNC: it only refreshes its framebuffer while a VNC client is attached, but the broadcast reads that framebuffer with x11grab (not as a VNC client). With no viewer attached the captured screen idled at ~1.5 fps (measured 3/30 distinct frames); the cursor looked smooth only because x11grab overlays the live cursor on every frame. - Add a headless RFB keepalive (vnc-keepalive.ts) that stays connected for the life of the stream and requests incremental framebuffer updates at the stream framerate. SelfbotStreamer starts it on broadcast start and tears it down on stop/self-end. Measured 3/30 -> 57/60 distinct frames at 60 fps. Fail-open; authenticates with VNC_PASSWORD or the ~/.config/tigervnc/passwd file. - Fix a resource leak: when the Go-Live ended on its own, only the active flag was cleared, leaving the x11grab->nvenc ffmpeg running forever (pinning a CPU core while no media was transmitted, with only the gateway TCP left and no UDP media). The self-end path now tears down capture, keepalive and voice like stop() does. - Tests for both paths (self-end teardown; keepalive DES auth, port mapping, password resolution). Add @types/bun so bun:test typechecks; document the keepalive and recommended Chrome flags in README and .env.example. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
54 lines
2.0 KiB
TypeScript
54 lines
2.0 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import crypto from "node:crypto";
|
|
import {
|
|
decodeVncPassword,
|
|
vncChallengeResponse,
|
|
vncPortForDisplay,
|
|
resolveVncPassword,
|
|
} from "./vnc-keepalive.ts";
|
|
|
|
// Independent reference for VNC's bit-reversed-key DES, to cross-check the module.
|
|
const rev = (b: number) => {
|
|
let r = 0;
|
|
for (let i = 0; i < 8; i++) r = (r << 1) | ((b >> i) & 1);
|
|
return r & 0xff;
|
|
};
|
|
const vncKey = (buf: Buffer) => Buffer.from([...buf.subarray(0, 8)].map(rev));
|
|
const desEnc = (key: Buffer, data: Buffer) => {
|
|
const c = crypto.createCipheriv("des-ecb", key, null);
|
|
c.setAutoPadding(false);
|
|
return Buffer.concat([c.update(data), c.final()]);
|
|
};
|
|
const FIXED_KEY = Buffer.from([23, 82, 107, 6, 35, 78, 88, 7]);
|
|
|
|
test("decodeVncPassword inverts the fixed-key obfuscation", () => {
|
|
const pw = Buffer.from("s3cr3t\0\0", "binary"); // 8 bytes, trailing nulls
|
|
const obf = desEnc(vncKey(FIXED_KEY), pw); // how vncpasswd stores it
|
|
expect(decodeVncPassword(obf).toString()).toBe("s3cr3t");
|
|
});
|
|
|
|
test("vncChallengeResponse encrypts both challenge blocks with the bit-reversed password key", () => {
|
|
const pw = Buffer.from("hunter12");
|
|
const challenge = crypto.randomBytes(16);
|
|
const expected = desEnc(vncKey(pw), challenge);
|
|
const got = vncChallengeResponse(pw, challenge);
|
|
expect(got.length).toBe(16);
|
|
expect(got.equals(expected)).toBe(true);
|
|
});
|
|
|
|
test("vncPortForDisplay maps an X display to its RFB port", () => {
|
|
expect(vncPortForDisplay(":1")).toBe(5901);
|
|
expect(vncPortForDisplay(":0")).toBe(5900);
|
|
expect(vncPortForDisplay(":5")).toBe(5905);
|
|
});
|
|
|
|
test("resolveVncPassword prefers the VNC_PASSWORD env var", () => {
|
|
const pw = resolveVncPassword({ VNC_PASSWORD: "letmein9" } as NodeJS.ProcessEnv);
|
|
expect(pw?.toString()).toBe("letmein9");
|
|
});
|
|
|
|
test("resolveVncPassword returns null when nothing is available", () => {
|
|
const pw = resolveVncPassword({ VNC_PASSWD_FILE: "/nonexistent/path/xyz" } as NodeJS.ProcessEnv);
|
|
expect(pw).toBeNull();
|
|
});
|