Files
javis_bot/bot/src/stream/vnc-keepalive.test.ts
javis-bot 4176a68873 fix(selfbot): smooth VNC capture via keepalive + stop ffmpeg leak on stream end
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>
2026-06-10 15:21:44 +09:00

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();
});