diff --git a/README.md b/README.md index b95d15e65..305108588 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ profits such as: RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman, UltraVNC's MSLogonII * Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG, - ZRLE, JPEG + ZRLE, JPEG, Zlib * Supports scaling, clipping and resizing the desktop * Local cursor rendering * Clipboard copy/paste with full Unicode support diff --git a/core/decoders/zlib.js b/core/decoders/zlib.js new file mode 100644 index 000000000..d1e5d5c98 --- /dev/null +++ b/core/decoders/zlib.js @@ -0,0 +1,51 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2024 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import Inflator from "../inflator.js"; + +export default class ZlibDecoder { + constructor() { + this._zlib = new Inflator(); + this._length = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if ((width === 0) || (height === 0)) { + return true; + } + + if (this._length === 0) { + if (sock.rQwait("ZLIB", 4)) { + return false; + } + + this._length = sock.rQshift32(); + } + + if (sock.rQwait("ZLIB", this._length)) { + return false; + } + + let data = new Uint8Array(sock.rQshiftBytes(this._length, false)); + this._length = 0; + + this._zlib.setInput(data); + data = this._zlib.inflate(width * height * 4); + this._zlib.setInput(null); + + // Max sure the image is fully opaque + for (let i = 0; i < width * height; i++) { + data[i * 4 + 3] = 255; + } + + display.blitImage(x, y, width, height, data, 0); + + return true; + } +} diff --git a/core/encodings.js b/core/encodings.js index 1a79989d1..d80536191 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -11,6 +11,7 @@ export const encodings = { encodingCopyRect: 1, encodingRRE: 2, encodingHextile: 5, + encodingZlib: 6, encodingTight: 7, encodingZRLE: 16, encodingTightPNG: -260, @@ -40,6 +41,7 @@ export function encodingName(num) { case encodings.encodingCopyRect: return "CopyRect"; case encodings.encodingRRE: return "RRE"; case encodings.encodingHextile: return "Hextile"; + case encodings.encodingZlib: return "Zlib"; case encodings.encodingTight: return "Tight"; case encodings.encodingZRLE: return "ZRLE"; case encodings.encodingTightPNG: return "TightPNG"; diff --git a/core/rfb.js b/core/rfb.js index f2deb0e7b..0bd2b07e9 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -31,6 +31,7 @@ import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; import RREDecoder from "./decoders/rre.js"; import HextileDecoder from "./decoders/hextile.js"; +import ZlibDecoder from './decoders/zlib.js'; import TightDecoder from "./decoders/tight.js"; import TightPNGDecoder from "./decoders/tightpng.js"; import ZRLEDecoder from "./decoders/zrle.js"; @@ -244,6 +245,7 @@ export default class RFB extends EventTargetMixin { this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); this._decoders[encodings.encodingRRE] = new RREDecoder(); this._decoders[encodings.encodingHextile] = new HextileDecoder(); + this._decoders[encodings.encodingZlib] = new ZlibDecoder(); this._decoders[encodings.encodingTight] = new TightDecoder(); this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); this._decoders[encodings.encodingZRLE] = new ZRLEDecoder(); @@ -2121,6 +2123,7 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.encodingJPEG); encs.push(encodings.encodingHextile); encs.push(encodings.encodingRRE); + encs.push(encodings.encodingZlib); } encs.push(encodings.encodingRaw); diff --git a/tests/test.zlib.js b/tests/test.zlib.js new file mode 100644 index 000000000..bc72137e5 --- /dev/null +++ b/tests/test.zlib.js @@ -0,0 +1,84 @@ +import Websock from '../core/websock.js'; +import Display from '../core/display.js'; + +import ZlibDecoder from '../core/decoders/zlib.js'; + +import FakeWebSocket from './fake.websocket.js'; + +function testDecodeRect(decoder, x, y, width, height, data, display, depth) { + let sock; + let done = false; + + sock = new Websock; + sock.open("ws://example.com"); + + sock.on('message', () => { + done = decoder.decodeRect(x, y, width, height, sock, display, depth); + }); + + // Empty messages are filtered at multiple layers, so we need to + // do a direct call + if (data.length === 0) { + done = decoder.decodeRect(x, y, width, height, sock, display, depth); + } else { + sock._websocket._receiveData(new Uint8Array(data)); + } + + display.flip(); + + return done; +} + +describe('Zlib Decoder', function () { + let decoder; + let display; + + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + beforeEach(function () { + decoder = new ZlibDecoder(); + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + }); + + it('should handle the Zlib encoding', function () { + let done; + + let zlibData = new Uint8Array([ + 0x00, 0x00, 0x00, 0x23, /* length */ + 0x78, 0x01, 0xfa, 0xcf, 0x00, 0x04, 0xff, 0x61, 0x04, 0x90, 0x01, 0x41, 0x50, 0xc1, 0xff, 0x0c, + 0xef, 0x40, 0x02, 0xef, 0xfe, 0x33, 0xac, 0x02, 0xe2, 0xd5, 0x40, 0x8c, 0xce, 0x07, 0x00, 0x00, + 0x00, 0xff, 0xff, + ]); + done = testDecodeRect(decoder, 0, 0, 4, 4, zlibData, display, 24); + expect(done).to.be.true; + + let targetData = new Uint8ClampedArray([ + 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + ]); + + expect(display).to.have.displayed(targetData); + }); + + it('should handle empty rects', function () { + display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); + display.fillRect(2, 0, 2, 2, [0x00, 0xff, 0x00]); + display.fillRect(0, 2, 2, 2, [0x00, 0xff, 0x00]); + + let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); + + expect(done).to.be.true; + expect(display).to.have.displayed(targetData); + }); +});