From 94627760aa5bea04120afc4dadb4ddd8bb653d74 Mon Sep 17 00:00:00 2001 From: Marco Fortina Date: Sat, 16 May 2026 16:46:41 +0000 Subject: [PATCH] libvnc: add ExtendedMouseButtons support --- CMakeLists.txt | 8 +++ include/rfb/rfb.h | 2 + include/rfb/rfbclient.h | 3 + include/rfb/rfbproto.h | 13 ++++ src/libvncclient/rfbclient.c | 24 +++++++- src/libvncserver/rfbserver.c | 84 ++++++++++++++++++++++++- test/extended_mouse_buttons_test.c | 99 ++++++++++++++++++++++++++++++ 7 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 test/extended_mouse_buttons_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d9709c809..978418d20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -712,6 +712,13 @@ if(WITH_THREADS AND (CMAKE_USE_PTHREADS_INIT OR CMAKE_USE_WIN32_THREADS_INIT) AN ) endif(WITH_THREADS AND (CMAKE_USE_PTHREADS_INIT OR CMAKE_USE_WIN32_THREADS_INIT) AND WITH_LIBVNCSERVER AND WITH_LIBVNCCLIENT) +if(UNIX AND WITH_LIBVNCCLIENT) + set(SIMPLETESTS + ${SIMPLETESTS} + extended_mouse_buttons_test + ) +endif(UNIX AND WITH_LIBVNCCLIENT) + foreach(t ${SIMPLETESTS}) add_executable(test_${t} ${TESTS_DIR}/${t}.c) set_target_properties(test_${t} PROPERTIES OUTPUT_NAME ${t}) @@ -765,6 +772,7 @@ if(UNIX) endif(WITH_LIBVNCSERVER) if(WITH_LIBVNCCLIENT) add_test(NAME includetest_client COMMAND ${TESTS_DIR}/includetest.sh ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR} ${CMAKE_MAKE_PROGRAM} "rfb/rfbclient.h") + add_test(NAME extended_mouse_buttons COMMAND test_extended_mouse_buttons_test) endif(WITH_LIBVNCCLIENT) endif(UNIX) if(WITH_JPEG AND FOUND_LIBJPEG_TURBO) diff --git a/include/rfb/rfb.h b/include/rfb/rfb.h index ecd829042..ce837a002 100644 --- a/include/rfb/rfb.h +++ b/include/rfb/rfb.h @@ -629,6 +629,8 @@ typedef struct _rfbClientRec { rfbBool enableSupportedEncodings; /**< client supports SupportedEncodings encoding */ rfbBool enableServerIdentity; /**< client supports ServerIdentity encoding */ rfbBool enableKeyboardLedState; /**< client supports KeyboardState encoding */ + rfbBool enableExtendedMouseButtons; /**< client supports ExtendedMouseButtons encoding */ + rfbBool extendedMouseButtonsPending; /**< ExtendedMouseButtons acknowledgement should be sent */ rfbBool enableLastRectEncoding; /**< client supports LastRect encoding */ rfbBool enableCursorShapeUpdates; /**< client supports cursor shape updates */ rfbBool enableCursorPosUpdates; /**< client supports cursor position updates */ diff --git a/include/rfb/rfbclient.h b/include/rfb/rfbclient.h index f72d84a49..c6f6ac49e 100644 --- a/include/rfb/rfbclient.h +++ b/include/rfb/rfbclient.h @@ -415,6 +415,9 @@ typedef struct _rfbClient { /** the QoS IP DSCP for this client */ int QoS_DSCP; + /** server acknowledged ExtendedMouseButtons pointer events */ + rfbBool extendedMouseButtonsEnabled; + /** hook to handle xvp server messages */ HandleXvpMsgProc HandleXvpMsg; diff --git a/include/rfb/rfbproto.h b/include/rfb/rfbproto.h index ebcd303cc..2f96641e6 100644 --- a/include/rfb/rfbproto.h +++ b/include/rfb/rfbproto.h @@ -516,6 +516,7 @@ typedef struct { #define rfbEncodingLastRect 0xFFFFFF20 #define rfbEncodingNewFBSize 0xFFFFFF21 #define rfbEncodingExtDesktopSize 0xFFFFFECC +#define rfbEncodingExtendedMouseButtons 0xFFFFFEC4 /* -316 */ #define rfbEncodingQualityLevel0 0xFFFFFFE0 #define rfbEncodingQualityLevel1 0xFFFFFFE1 @@ -1431,11 +1432,23 @@ typedef struct { #define rfbButton3Mask 4 #define rfbButton4Mask 8 #define rfbButton5Mask 16 +#define rfbButton6Mask 32 +#define rfbButton7Mask 64 +#define rfbButton8Mask 128 +#define rfbButton9Mask 256 +#define rfbButton10Mask 512 +#define rfbButton11Mask 1024 +#define rfbButton12Mask 2048 +#define rfbButton13Mask 4096 +#define rfbButton14Mask 8192 +#define rfbButton15Mask 16384 +#define rfbPointerEventExtendedButtonMask 128 /* RealVNC 335 method */ #define rfbWheelUpMask rfbButton4Mask #define rfbWheelDownMask rfbButton5Mask #define sz_rfbPointerEventMsg 6 +#define sz_rfbExtendedPointerEventMsg 7 diff --git a/src/libvncclient/rfbclient.c b/src/libvncclient/rfbclient.c index f6baf641c..058b48723 100644 --- a/src/libvncclient/rfbclient.c +++ b/src/libvncclient/rfbclient.c @@ -1445,6 +1445,8 @@ SetFormatAndEncodings(rfbClient* client) if (se->nEncodings < MAX_ENCODINGS) encs[se->nEncodings++] = rfbClientSwap32IfLE(rfbEncodingQemuExtendedKeyEvent); + if (se->nEncodings < MAX_ENCODINGS) + encs[se->nEncodings++] = rfbClientSwap32IfLE(rfbEncodingExtendedMouseButtons); #ifdef LIBVNCSERVER_HAVE_LIBZ /* extendedclipboard. tell server we support it if client has the callback set */ @@ -1661,17 +1663,30 @@ rfbBool SendPointerEvent(rfbClient* client,int x, int y, int buttonMask) { rfbPointerEventMsg pe; + uint8_t extendedButtonMask; + rfbBool sendExtended = client->extendedMouseButtonsEnabled && + ((buttonMask & ~0x7f) != 0); if (!SupportsClient2Server(client, rfbPointerEvent)) return TRUE; + if ((buttonMask & ~0xff) != 0 && !client->extendedMouseButtonsEnabled) return FALSE; pe.type = rfbPointerEvent; - pe.buttonMask = buttonMask; + pe.buttonMask = (uint8_t)(buttonMask & (sendExtended ? 0x7f : 0xff)); + if (sendExtended) + pe.buttonMask |= rfbPointerEventExtendedButtonMask; if (x < 0) x = 0; if (y < 0) y = 0; pe.x = rfbClientSwap16IfLE(x); pe.y = rfbClientSwap16IfLE(y); - return WriteToRFBServer(client, (char *)&pe, sz_rfbPointerEventMsg); + if (!WriteToRFBServer(client, (char *)&pe, sz_rfbPointerEventMsg)) + return FALSE; + + if (!sendExtended) + return TRUE; + + extendedButtonMask = (uint8_t)((buttonMask >> 7) & 0xff); + return WriteToRFBServer(client, (char *)&extendedButtonMask, 1); } @@ -2145,6 +2160,11 @@ HandleRFBServerMessage(rfbClient* client) continue; } + if (rect.encoding == rfbEncodingExtendedMouseButtons) { + client->extendedMouseButtonsEnabled = TRUE; + continue; + } + if (rect.encoding == rfbEncodingNewFBSize) { if(!ResizeClientBuffer(client, rect.r.w, rect.r.h)) return FALSE; diff --git a/src/libvncserver/rfbserver.c b/src/libvncserver/rfbserver.c index 22af22d7d..f20eb842d 100644 --- a/src/libvncserver/rfbserver.c +++ b/src/libvncserver/rfbserver.c @@ -500,6 +500,8 @@ rfbNewTCPOrUDPClient(rfbScreenInfoPtr rfbScreen, cl->useRichCursorEncoding = FALSE; cl->enableLastRectEncoding = FALSE; cl->enableKeyboardLedState = FALSE; + cl->enableExtendedMouseButtons = FALSE; + cl->extendedMouseButtonsPending = FALSE; cl->enableSupportedMessages = FALSE; cl->enableSupportedEncodings = FALSE; cl->enableServerIdentity = FALSE; @@ -1029,6 +1031,39 @@ rfbSendKeyboardLedState(rfbClientPtr cl) #define rfbSetBit(buffer, position) (buffer[(position & 255) / 8] |= (1 << (position % 8))) +/* + * Send rfbEncodingExtendedMouseButtons acknowledgement. + */ + +static rfbBool +rfbSendExtendedMouseButtons(rfbClientPtr cl) +{ + rfbFramebufferUpdateRectHeader rect; + + if (cl->ublen + sz_rfbFramebufferUpdateRectHeader > UPDATE_BUF_SIZE) { + if (!rfbSendUpdateBuf(cl)) + return FALSE; + } + + rect.r.x = 0; + rect.r.y = 0; + rect.r.w = 0; + rect.r.h = 0; + rect.encoding = Swap32IfLE(rfbEncodingExtendedMouseButtons); + + memcpy(&cl->updateBuf[cl->ublen], (char *)&rect, + sz_rfbFramebufferUpdateRectHeader); + cl->ublen += sz_rfbFramebufferUpdateRectHeader; + + rfbStatRecordEncodingSent(cl, rfbEncodingExtendedMouseButtons, + sz_rfbFramebufferUpdateRectHeader, sz_rfbFramebufferUpdateRectHeader); + if (!rfbSendUpdateBuf(cl)) + return FALSE; + + return TRUE; +} + + /* * Send rfbEncodingSupportedMessages. */ @@ -1131,6 +1166,7 @@ rfbSendSupportedEncodings(rfbClientPtr cl) rfbEncodingNewFBSize, rfbEncodingExtDesktopSize, rfbEncodingKeyboardLedState, + rfbEncodingExtendedMouseButtons, rfbEncodingSupportedMessages, rfbEncodingSupportedEncodings, rfbEncodingServerIdentity, @@ -2398,6 +2434,8 @@ rfbProcessClientNormalMessage(rfbClientPtr cl) cl->enableCursorShapeUpdates = FALSE; cl->enableLastRectEncoding = FALSE; cl->enableKeyboardLedState = FALSE; + cl->enableExtendedMouseButtons = FALSE; + cl->extendedMouseButtonsPending = FALSE; cl->enableSupportedMessages = FALSE; cl->enableSupportedEncodings = FALSE; cl->enableServerIdentity = FALSE; @@ -2506,7 +2544,15 @@ rfbProcessClientNormalMessage(rfbClientPtr cl) "%s\n", cl->host); cl->enableKeyboardLedState = TRUE; } - break; + break; + case rfbEncodingExtendedMouseButtons: + if (!cl->enableExtendedMouseButtons) { + rfbLog("Enabling ExtendedMouseButtons protocol extension for client " + "%s\n", cl->host); + cl->enableExtendedMouseButtons = TRUE; + cl->extendedMouseButtonsPending = TRUE; + } + break; case rfbEncodingSupportedMessages: if (!cl->enableSupportedMessages) { rfbLog("Enabling SupportedMessages protocol extension for client " @@ -2759,7 +2805,25 @@ rfbProcessClientNormalMessage(rfbClientPtr cl) return; } - rfbStatRecordMessageRcvd(cl, msg.type, sz_rfbPointerEventMsg, sz_rfbPointerEventMsg); + if (cl->enableExtendedMouseButtons && + (msg.pe.buttonMask & rfbPointerEventExtendedButtonMask)) { + uint8_t extendedButtonMask; + if ((n = rfbReadExact(cl, (char *)&extendedButtonMask, 1)) <= 0) { + if (n != 0) + rfbLogPerror("rfbProcessClientNormalMessage: read"); + rfbCloseClient(cl); + return; + } + msg.pe.buttonMask = (msg.pe.buttonMask & ~rfbPointerEventExtendedButtonMask) | + (extendedButtonMask << 7); + rfbStatRecordMessageRcvd(cl, msg.type, + sz_rfbExtendedPointerEventMsg, + sz_rfbExtendedPointerEventMsg); + } else { + rfbStatRecordMessageRcvd(cl, msg.type, + sz_rfbPointerEventMsg, + sz_rfbPointerEventMsg); + } if (cl->screen->pointerClient && cl->screen->pointerClient != cl) return; @@ -3199,6 +3263,7 @@ rfbSendFramebufferUpdate(rfbClientPtr cl, rfbBool sendCursorShape = FALSE; rfbBool sendCursorPos = FALSE; rfbBool sendKeyboardLedState = FALSE; + rfbBool sendExtendedMouseButtons = FALSE; rfbBool sendSupportedMessages = FALSE; rfbBool sendSupportedEncodings = FALSE; rfbBool sendServerIdentity = FALSE; @@ -3271,6 +3336,15 @@ rfbSendFramebufferUpdate(rfbClientPtr cl, } } + /* + * Do we plan to acknowledge ExtendedMouseButtons support? + */ + if (cl->extendedMouseButtonsPending) + { + sendExtendedMouseButtons = TRUE; + cl->extendedMouseButtonsPending = FALSE; + } + /* * Do we plan to send a rfbEncodingSupportedMessages? */ @@ -3541,6 +3615,7 @@ rfbSendFramebufferUpdate(rfbClientPtr cl, fu->nRects = Swap16IfLE((uint16_t)(sraRgnCountRects(updateCopyRegion) + nUpdateRegionRects + !!sendCursorShape + !!sendCursorPos + !!sendKeyboardLedState + + !!sendExtendedMouseButtons + !!sendSupportedMessages + !!sendSupportedEncodings + !!sendServerIdentity)); } else { fu->nRects = 0xFFFF; @@ -3564,6 +3639,11 @@ rfbSendFramebufferUpdate(rfbClientPtr cl, goto updateFailed; } + if (sendExtendedMouseButtons) { + if (!rfbSendExtendedMouseButtons(cl)) + goto updateFailed; + } + if (sendSupportedMessages) { if (!rfbSendSupportedMessages(cl)) goto updateFailed; diff --git a/test/extended_mouse_buttons_test.c b/test/extended_mouse_buttons_test.c new file mode 100644 index 000000000..1c3859608 --- /dev/null +++ b/test/extended_mouse_buttons_test.c @@ -0,0 +1,99 @@ +#include + +#include +#include +#include +#include +#include + +static void enablePointerEvent(rfbClient *client) +{ + memset(&client->supportedMessages, 0, sizeof(client->supportedMessages)); + client->supportedMessages.client2server[(rfbPointerEvent & 0xff) / 8] |= + (1 << (rfbPointerEvent % 8)); +} + +static int readExact(int fd, unsigned char *buf, size_t len) +{ + size_t off = 0; + while (off < len) { + ssize_t n = read(fd, buf + off, len - off); + if (n <= 0) + return 0; + off += (size_t)n; + } + return 1; +} + +static int expectNoByte(int fd) +{ + unsigned char ch; + ssize_t n = recv(fd, &ch, 1, MSG_DONTWAIT); + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) + return 1; + return 0; +} + +static int expectBytes(int fd, const unsigned char *expected, size_t len) +{ + unsigned char got[16]; + if (len > sizeof(got)) + return 0; + if (!readExact(fd, got, len)) + return 0; + if (memcmp(got, expected, len) != 0) { + size_t i; + fprintf(stderr, "unexpected bytes:\n"); + for (i = 0; i < len; ++i) + fprintf(stderr, " %zu: got 0x%02x expected 0x%02x\n", i, got[i], expected[i]); + return 0; + } + return 1; +} + +int main(void) +{ + int sv[2]; + rfbClient client; + unsigned char expectedNormalBack[] = { + rfbPointerEvent, rfbButton8Mask, 0x00, 0x0a, 0x00, 0x14 + }; + unsigned char expectedExtendedBackForward[] = { + rfbPointerEvent, rfbPointerEventExtendedButtonMask, + 0x00, 0x0a, 0x00, 0x14, 0x03 + }; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) + return 1; + + memset(&client, 0, sizeof(client)); + client.sock = sv[0]; + client.endianTest = 1; + enablePointerEvent(&client); + + if (!SendPointerEvent(&client, 10, 20, rfbButton8Mask)) { + fprintf(stderr, "normal back button event failed\n"); + return 1; + } + if (!expectBytes(sv[1], expectedNormalBack, sizeof(expectedNormalBack))) + return 1; + + if (SendPointerEvent(&client, 10, 20, rfbButton9Mask)) { + fprintf(stderr, "button 9 succeeded without ExtendedMouseButtons acknowledgement\n"); + return 1; + } + if (!expectNoByte(sv[1])) + return 1; + + client.extendedMouseButtonsEnabled = TRUE; + if (!SendPointerEvent(&client, 10, 20, rfbButton8Mask | rfbButton9Mask)) { + fprintf(stderr, "extended back/forward button event failed\n"); + return 1; + } + if (!expectBytes(sv[1], expectedExtendedBackForward, sizeof(expectedExtendedBackForward))) + return 1; + + close(sv[0]); + close(sv[1]); + return 0; +}