Commit e081d14 for novnc.com

commit e081d1415ac0f3620d5ff3f81080711ef20002de
Author: Adam Halim <adaha@cendio.se>
Date:   Thu Jan 9 09:19:21 2025 +0100

    Add support for forward and back mouse buttons

    This commit implements the extendedMouseButtons pseudo-encoding, which
    makes it possible to use the forward and back mouse buttons.

diff --git a/core/encodings.js b/core/encodings.js
index bf25ac9..7afcb17 100644
--- a/core/encodings.js
+++ b/core/encodings.js
@@ -30,6 +30,7 @@ export const encodings = {
     pseudoEncodingXvp: -309,
     pseudoEncodingFence: -312,
     pseudoEncodingContinuousUpdates: -313,
+    pseudoEncodingExtendedMouseButtons: -316,
     pseudoEncodingCompressLevel9: -247,
     pseudoEncodingCompressLevel0: -256,
     pseudoEncodingVMwareCursor: 0x574d5664,
diff --git a/core/rfb.js b/core/rfb.js
index 89e9197..57f0258 100644
--- a/core/rfb.js
+++ b/core/rfb.js
@@ -152,6 +152,8 @@ export default class RFB extends EventTargetMixin {

         this._qemuExtKeyEventSupported = false;

+        this._extendedPointerEventSupported = false;
+
         this._clipboardText = null;
         this._clipboardServerCapabilitiesActions = {};
         this._clipboardServerCapabilitiesFormats = {};
@@ -1051,10 +1053,11 @@ export default class RFB extends EventTargetMixin {
             1: 1 << 2, // Right
             2: 1 << 1, // Middle
             3: 1 << 7, // Back
+            4: 1 << 8, // Forward
         };

         let bmask = 0;
-        for (let i = 0; i < 4; i++) {
+        for (let i = 0; i < 5; i++) {
             if (buttons & (1 << i)) {
                 bmask |= buttonMaskMap[i];
             }
@@ -1189,8 +1192,20 @@ export default class RFB extends EventTargetMixin {
         if (this._rfbConnectionState !== 'connected') { return; }
         if (this._viewOnly) { return; } // View only, skip mouse events

-        RFB.messages.pointerEvent(this._sock, this._display.absX(x),
-                                  this._display.absY(y), mask);
+        // Highest bit in mask is never sent to the server
+        if (mask & 0x8000) {
+            throw new Error("Illegal mouse button mask (mask: " + mask + ")");
+        }
+
+        let extendedMouseButtons = mask & 0x7f80;
+
+        if (this._extendedPointerEventSupported && extendedMouseButtons) {
+            RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),
+                                              this._display.absY(y), mask);
+        } else {
+            RFB.messages.pointerEvent(this._sock, this._display.absX(x),
+                                      this._display.absY(y), mask);
+        }
     }

     _handleWheel(ev) {
@@ -2229,6 +2244,7 @@ export default class RFB extends EventTargetMixin {
         encs.push(encodings.pseudoEncodingContinuousUpdates);
         encs.push(encodings.pseudoEncodingDesktopName);
         encs.push(encodings.pseudoEncodingExtendedClipboard);
+        encs.push(encodings.pseudoEncodingExtendedMouseButtons);

         if (this._fbDepth == 24) {
             encs.push(encodings.pseudoEncodingVMwareCursor);
@@ -2658,6 +2674,10 @@ export default class RFB extends EventTargetMixin {
             case encodings.pseudoEncodingExtendedDesktopSize:
                 return this._handleExtendedDesktopSize();

+            case encodings.pseudoEncodingExtendedMouseButtons:
+                this._extendedPointerEventSupported = true;
+                return true;
+
             case encodings.pseudoEncodingQEMULedEvent:
                 return this._handleLedEvent();

@@ -3067,6 +3087,10 @@ RFB.messages = {
     pointerEvent(sock, x, y, mask) {
         sock.sQpush8(5); // msg-type

+        // Marker bit must be set to 0, otherwise the server might
+        // confuse the marker bit with the highest bit in a normal
+        // PointerEvent message.
+        mask = mask & 0x7f;
         sock.sQpush8(mask);

         sock.sQpush16(x);
@@ -3075,6 +3099,27 @@ RFB.messages = {
         sock.flush();
     },

+    extendedPointerEvent(sock, x, y, mask) {
+        sock.sQpush8(5); // msg-type
+
+        let higherBits = (mask >> 7) & 0xff;
+
+        // Bits 2-7 are reserved
+        if (higherBits & 0xfc) {
+            throw new Error("Invalid mouse button mask: " + mask);
+        }
+
+        let lowerBits = mask & 0x7f;
+        lowerBits |= 0x80; // Set marker bit to 1
+
+        sock.sQpush8(lowerBits);
+        sock.sQpush16(x);
+        sock.sQpush16(y);
+        sock.sQpush8(higherBits);
+
+        sock.flush();
+    },
+
     // Used to build Notify and Request data.
     _buildExtendedClipboardFlags(actions, formats) {
         let data = new Uint8Array(4);
diff --git a/tests/test.rfb.js b/tests/test.rfb.js
index cb2945a..8cdd2e3 100644
--- a/tests/test.rfb.js
+++ b/tests/test.rfb.js
@@ -3265,6 +3265,7 @@ describe('Remote Frame Buffer protocol client', function () {
                     expect(spy).to.have.been.calledOnce;
                     expect(spy.args[0][0].detail.name).to.equal('som€ nam€');
                 });
+
             });

             describe('Caps Lock and Num Lock remote fixup', function () {
@@ -3757,6 +3758,7 @@ describe('Remote Frame Buffer protocol client', function () {
     describe('Asynchronous events', function () {
         let client;
         let pointerEvent;
+        let extendedPointerEvent;
         let keyEvent;
         let qemuKeyEvent;

@@ -3770,12 +3772,14 @@ describe('Remote Frame Buffer protocol client', function () {
             client.focusOnClick = false;

             pointerEvent = sinon.spy(RFB.messages, 'pointerEvent');
+            extendedPointerEvent = sinon.spy(RFB.messages, 'extendedPointerEvent');
             keyEvent = sinon.spy(RFB.messages, 'keyEvent');
             qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent');
         });

         afterEach(function () {
             pointerEvent.restore();
+            extendedPointerEvent.restore();
             keyEvent.restore();
             qemuKeyEvent.restore();
         });
@@ -3884,6 +3888,23 @@ describe('Remote Frame Buffer protocol client', function () {
                                                                  50, 70, 0x0);
             });

+            it('should send extended pointer event when server supports extended pointer events', function () {
+                // Enable extended pointer events
+                sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -316 }], [[]], client);
+
+                sendMouseButtonEvent(50, 70, true, 0x10, client);
+
+                expect(extendedPointerEvent).to.have.been.calledOnceWith(client._sock,
+                                                                         50, 70, 0x100);
+            });
+
+            it('should send normal pointer event when server does not support extended pointer events', function () {
+                sendMouseButtonEvent(50, 70, true, 0x10, client);
+
+                expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+                                                                 50, 70, 0x100);
+            });
+
             describe('Event aggregation', function () {
                 it('should send a single pointer event on mouse movement', function () {
                     sendMouseMoveEvent(50, 70, 0x0, client);
@@ -5135,11 +5156,36 @@ describe('RFB messages', function () {
         });

         it('should send correct data for pointer events', function () {
+            RFB.messages.pointerEvent(sock, 12345, 54321, 0x2b);
+            let expected =
+                [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];
+            expect(sock).to.have.sent(new Uint8Array(expected));
+        });
+
+        it('should send correct data for pointer events with marker bit set', function () {
             RFB.messages.pointerEvent(sock, 12345, 54321, 0xab);
             let expected =
-                [ 5, 0xab, 0x30, 0x39, 0xd4, 0x31];
+                [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];
+            expect(sock).to.have.sent(new Uint8Array(expected));
+        });
+
+        it('should send correct data for pointer events with extended button bits set', function () {
+            RFB.messages.pointerEvent(sock, 12345, 54321, 0x3ab);
+            let expected =
+                [ 5, 0x2b, 0x30, 0x39, 0xd4, 0x31];
             expect(sock).to.have.sent(new Uint8Array(expected));
         });
+
+        it('should send correct data for extended pointer events', function () {
+            RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0xab);
+            let expected =
+                [ 5, 0xab, 0x30, 0x39, 0xd4, 0x31, 0x1];
+            expect(sock).to.have.sent(new Uint8Array(expected));
+        });
+
+        it('should not send invalid data for extended pointer events', function () {
+            expect(() => RFB.messages.extendedPointerEvent(sock, 12345, 54321, 0x3ab)).to.throw(Error);
+        });
     });

     describe('Clipboard events', function () {