Commit a3681bde for guacamole.apache.org

commit a3681bde975a1c4e9644a8ed0aad4f3093b5241c
Author: Bradley Bennett <bbennett@keepersecurity.com>
Date:   Wed Apr 29 16:04:49 2026 -0400

    GUACAMOLE-2268: VNC: add UTF-8 (Unicode) support.

diff --git a/src/protocols/vnc/clipboard.c b/src/protocols/vnc/clipboard.c
index 319ca95f..3d4a400d 100644
--- a/src/protocols/vnc/clipboard.c
+++ b/src/protocols/vnc/clipboard.c
@@ -29,6 +29,7 @@
 #include <guacamole/stream.h>
 #include <guacamole/user.h>
 #include <rfb/rfbclient.h>
+#include <rfb/rfbconfig.h>
 #include <rfb/rfbproto.h>

 int guac_vnc_set_clipboard_encoding(guac_client* client,
@@ -103,6 +104,35 @@ int guac_vnc_clipboard_end_handler(guac_user* user, guac_stream* stream) {
     guac_vnc_client* vnc_client = (guac_vnc_client*) user->client->data;
     rfbClient* rfb_client = vnc_client->rfb_client;

+    /* Send via VNC only if finished connecting */
+    if (rfb_client == NULL)
+        return 0;
+
+    /*
+     * Guacamole stores clipboard text as UTF-8. The clipboard-encoding
+     * setting only applies to the classic VNC clipboard path, where text
+     * must be converted from UTF-8 to the configured wire encoding.
+     *
+     * If clipboard-encoding is UTF-8, try the Extended Clipboard  path first
+     * since it can send UTF-8 directly. Otherwise, or if that fails, fall
+     * back to classic clipboard conversion.
+     *
+     * Text coming back from the VNC server follows the same idea in reverse:
+     * classic clipboard text is decoded using clipboard-encoding, while
+     * Extended Clipboard text is already UTF-8.
+     */
+
+    const char* clipboard_encoding = vnc_client->settings->clipboard_encoding;
+    int use_utf8_clipboard = clipboard_encoding != NULL &&
+        strcmp(clipboard_encoding, "UTF-8") == 0;
+
+    if (use_utf8_clipboard) {
+        if (SendClientCutTextUTF8(rfb_client, vnc_client->clipboard->buffer,
+                    vnc_client->clipboard->length))
+            return 0;
+    }
+
+    /* Fall back to classic clipboard with encoding conversion */
     char output_data[GUAC_COMMON_CLIPBOARD_MAX_LENGTH];

     const char* input = vnc_client->clipboard->buffer;
@@ -113,9 +143,7 @@ int guac_vnc_clipboard_end_handler(guac_user* user, guac_stream* stream) {
     guac_iconv(GUAC_READ_UTF8, &input, vnc_client->clipboard->length,
                writer, &output, sizeof(output_data));

-    /* Send via VNC only if finished connecting */
-    if (rfb_client != NULL)
-        SendClientCutText(rfb_client, output_data, output - output_data);
+    SendClientCutText(rfb_client, output_data, output - output_data);

     return 0;
 }
@@ -146,3 +174,29 @@ void guac_vnc_cut_text(rfbClient* client, const char* text, int textlen) {

 }

+void guac_vnc_cut_text_utf8(rfbClient* client, const char* text, int textlen) {
+
+    guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY);
+    guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data;
+
+    /* Ignore received text if outbound clipboard transfer is disabled */
+    if (vnc_client->settings->disable_copy)
+        return;
+
+    char received_data[GUAC_COMMON_CLIPBOARD_MAX_LENGTH];
+
+    const char* input = text;
+    char* output = received_data;
+
+    /* Extended clipboard always delivers UTF-8; iconv() here enforces
+     * GUAC_COMMON_CLIPBOARD_MAX_LENGTH and replaces invalid lead bytes
+     * with the Unicode replacement character (U+FFFD, ?) */
+    guac_iconv(GUAC_READ_UTF8, &input, textlen,
+               GUAC_WRITE_UTF8, &output, sizeof(received_data));
+
+    /* Send converted data */
+    guac_common_clipboard_reset(vnc_client->clipboard, "text/plain");
+    guac_common_clipboard_append(vnc_client->clipboard, received_data, output - received_data);
+    guac_common_clipboard_send(vnc_client->clipboard, gc);
+
+}
diff --git a/src/protocols/vnc/clipboard.h b/src/protocols/vnc/clipboard.h
index fbdd5888..48e08ab5 100644
--- a/src/protocols/vnc/clipboard.h
+++ b/src/protocols/vnc/clipboard.h
@@ -74,5 +74,21 @@ guac_user_end_handler guac_vnc_clipboard_end_handler;
  */
 void guac_vnc_cut_text(rfbClient* client, const char* text, int textlen);

+/**
+ * Handler for UTF-8 clipboard data received via the Extended Clipboard
+ * pseudo-encoding, invoked by libVNCServer when the server sends clipboard
+ * text using the extended clipboard protocol.
+ *
+ * @param client
+ *     The VNC client associated with the session.
+ *
+ * @param text
+ *     The UTF-8 clipboard text received from the server.
+ *
+ * @param textlen
+ *     The number of bytes in the clipboard text.
+ */
+void guac_vnc_cut_text_utf8(rfbClient* client, const char* text, int textlen);
+
 #endif

diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c
index 630ce986..cce4593f 100644
--- a/src/protocols/vnc/vnc.c
+++ b/src/protocols/vnc/vnc.c
@@ -68,6 +68,105 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;

 char* GUAC_VNC_CLIENT_KEY = "GUAC_VNC";

+/**
+ * Returns a human-readable name for the given negotiated VNC security type.
+ *
+ * Security scheme values come from libvncclient/libvncserver:
+ *   - rfbproto.h
+ *   - rfbclient.h
+ *
+ * @param auth_scheme
+ *     The negotiated security type.
+ *
+ * @return
+ *     The human-readable name of the given security type, or "UNKNOWN" if no
+ *     known name exists.
+ */
+static const char* guac_vnc_auth_scheme_name(uint32_t auth_scheme) {
+
+    switch (auth_scheme) {
+        case rfbNoAuth:
+            return "None";
+
+        case rfbVncAuth:
+            return "VNC";
+
+        case rfbTLS:
+            return "TLS";
+
+        case rfbVeNCrypt:
+            return "VeNCrypt";
+
+        case rfbVeNCryptPlain:
+            return "VeNCrypt/Plain";
+
+        case rfbVeNCryptTLSNone:
+            return "VeNCrypt/TLSNone";
+
+        case rfbVeNCryptTLSVNC:
+            return "VeNCrypt/TLSVNC";
+
+        case rfbVeNCryptTLSPlain:
+            return "VeNCrypt/TLSPlain";
+
+        case rfbVeNCryptX509None:
+            return "VeNCrypt/X509None";
+
+        case rfbVeNCryptX509VNC:
+            return "VeNCrypt/X509VNC";
+
+        case rfbVeNCryptX509Plain:
+            return "VeNCrypt/X509Plain";
+
+        case rfbVeNCryptX509SASL:
+            return "VeNCrypt/X509SASL";
+
+        case rfbVeNCryptTLSSASL:
+            return "VeNCrypt/TLSSASL";
+    }
+
+    return "UNKNOWN";
+
+}
+
+/**
+ * Logs the negotiated VNC protocol version and security scheme selected for
+ * the given connection.
+ *
+ * @param client
+ *     The Guacamole client associated with the connection.
+ *
+ * @param rfb_client
+ *     The libvncclient connection state containing the negotiated protocol
+ *     and security information.
+ */
+static void guac_vnc_log_connection_security(guac_client* client,
+        rfbClient* rfb_client) {
+
+    const char* auth_name =
+        guac_vnc_auth_scheme_name(rfb_client->authScheme);
+
+    /* Some security protocols store the selected sub-authentication scheme
+     * separately. */
+    if (rfb_client->subAuthScheme != 0) {
+        const char* subauth_name =
+            guac_vnc_auth_scheme_name(rfb_client->subAuthScheme);
+
+        guac_client_log(client, GUAC_LOG_INFO,
+                "Connected using RFB %d.%d, security: %s (%u), sub-security: %s (%u).",
+                rfb_client->major, rfb_client->minor,
+                auth_name, rfb_client->authScheme,
+                subauth_name, rfb_client->subAuthScheme);
+    }
+    else {
+        guac_client_log(client, GUAC_LOG_INFO,
+                "Connected using RFB %d.%d, security: %s (%u).",
+                rfb_client->major, rfb_client->minor,
+                auth_name, rfb_client->authScheme);
+    }
+
+}
+
 #ifdef ENABLE_VNC_TLS_LOCKING
 /**
  * A callback function that is called by the VNC library prior to writing
@@ -174,6 +273,7 @@ rfbClient* guac_vnc_get_client(guac_client* client) {

         /* Clipboard */
         rfb_client->GotXCutText = guac_vnc_cut_text;
+        rfb_client->GotXCutTextUTF8 = guac_vnc_cut_text_utf8;

         /* Set remote cursor */
         if (vnc_settings->remote_cursor) {
@@ -433,6 +533,9 @@ void* guac_vnc_client_thread(void* data) {
         return NULL;
     }

+    /* Log negotiated protocol version and security scheme to aid debugging */
+    guac_vnc_log_connection_security(client, rfb_client);
+
 #ifdef ENABLE_PULSE
     /* If audio is enabled, start streaming via PulseAudio */
     if (settings->audio_enabled)