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)