From e9cf5dde561349577d19f9dc1e9402845fdbe45b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 15:33:50 -0700 Subject: [PATCH] fix(gateway): keep view-only desktop sessions connected [AI-assisted] (#121256) * fix(gateway): keep view-only desktop sessions connected [AI-assisted] * fix(gateway): satisfy RFB filter type checks [AI-assisted] --- .../rfb-view-only-filter.test.ts | 57 +++++++++++++++++++ .../rfb-view-only-filter.ts | 13 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/gateway/worker-environments/rfb-view-only-filter.test.ts b/src/gateway/worker-environments/rfb-view-only-filter.test.ts index 567c5b401690..85cd3a502a8a 100644 --- a/src/gateway/worker-environments/rfb-view-only-filter.test.ts +++ b/src/gateway/worker-environments/rfb-view-only-filter.test.ts @@ -89,6 +89,63 @@ describe("RFB view-only client message filter", () => { }); }); + it("forwards the fragmented noVNC 1.7 display and flow-control sequence", () => { + const filter = enterMessagePhase(); + const setPixelFormat = Buffer.alloc(20); + const setEncodings = Buffer.alloc(100); + setEncodings[0] = 2; + setEncodings.writeUInt16BE(24, 2); + const framebufferUpdateRequest = Buffer.from([3, 1, 0, 0, 0, 0, 0, 64, 0, 64]); + const clientFence = Buffer.from([248, 0, 0, 0, 0, 0, 0, 0, 1, 0]); + const enableContinuousUpdates = Buffer.from([150, 1, 0, 0, 0, 0, 0, 64, 0, 64]); + const cutText = Buffer.concat([Buffer.from([6, 0, 0, 0, 0, 0, 0, 8]), Buffer.alloc(8)]); + const capturedSequence = Buffer.concat([ + setPixelFormat, + setEncodings, + framebufferUpdateRequest, + clientFence, + enableContinuousUpdates, + cutText, + enableContinuousUpdates, + ]); + const forwarded: Buffer[] = []; + + for (const byte of capturedSequence) { + const result = filter.filter(Buffer.of(byte)); + if ("error" in result) { + throw new Error(result.error); + } + forwarded.push(result.forward); + } + + expect(Buffer.concat(forwarded)).toEqual( + Buffer.concat([ + setPixelFormat, + setEncodings, + framebufferUpdateRequest, + clientFence, + enableContinuousUpdates, + enableContinuousUpdates, + ]), + ); + }); + + it("uses the ClientFence payload length to preserve message boundaries", () => { + const filter = enterMessagePhase(); + const clientFence = Buffer.concat([ + Buffer.from([248, 0, 0, 0, 0, 0, 0, 0, 3]), + Buffer.from("abc"), + ]); + const framebufferUpdateRequest = Buffer.from([3, 1, 0, 0, 0, 0, 0, 64, 0, 64]); + + expect(filter.filter(clientFence.subarray(0, 10))).toEqual({ forward: Buffer.alloc(0) }); + expect( + filter.filter(Buffer.concat([clientFence.subarray(10), framebufferUpdateRequest])), + ).toEqual({ + forward: Buffer.concat([clientFence, framebufferUpdateRequest]), + }); + }); + it("reassembles a message split across three chunks", () => { const filter = enterMessagePhase(); const completePrefix = Buffer.from([3, 1, 0, 0, 0, 0, 0, 64, 0, 64]); diff --git a/src/gateway/worker-environments/rfb-view-only-filter.ts b/src/gateway/worker-environments/rfb-view-only-filter.ts index 556b058ae90a..025f2ea9f630 100644 --- a/src/gateway/worker-environments/rfb-view-only-filter.ts +++ b/src/gateway/worker-environments/rfb-view-only-filter.ts @@ -45,6 +45,11 @@ export function createRfbClientMessageFilter() { return 6; case 6: return pending.length < 8 ? 8 : 8 + pending.readUInt32BE(4); + case 150: + return 10; + case 248: + // ClientFence's payload length byte follows its 8-byte fixed header. + return pending.length < 9 ? 9 : 9 + pending.readUInt8(8); default: return `unsupported RFB client message type ${pending[0]}`; } @@ -75,7 +80,13 @@ export function createRfbClientMessageFilter() { pending[0] = 1; forwarded.push(pending); phase = "messages"; - } else if (pending[0] === 0 || pending[0] === 2 || pending[0] === 3) { + } else if ( + pending[0] === 0 || + pending[0] === 2 || + pending[0] === 3 || + pending[0] === 150 || + pending[0] === 248 + ) { forwarded.push(pending); } pending = Buffer.alloc(0);