diff options
Diffstat (limited to 'plugins/vnc/vnc_plugin.c')
-rw-r--r-- | plugins/vnc/vnc_plugin.c | 2228 |
1 files changed, 2228 insertions, 0 deletions
diff --git a/plugins/vnc/vnc_plugin.c b/plugins/vnc/vnc_plugin.c new file mode 100644 index 0000000..633a2a9 --- /dev/null +++ b/plugins/vnc/vnc_plugin.c @@ -0,0 +1,2228 @@ +/* + * Remmina - The GTK+ Remote Desktop Client + * Copyright (C) 2010-2011 Vic Lee + * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo + * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo + * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations + * including the two. + * You must obey the GNU General Public License in all respects + * for all of the code used other than OpenSSL. * If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. * If you + * do not wish to do so, delete this exception statement from your + * version. * If you delete this exception statement from all source + * files in the program, then also delete it here. + * + */ + +#include <gmodule.h> +#include "vnc_plugin.h" +#include <rfb/rfbclient.h> + +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY 1 +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY 2 +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT 3 +#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH 4 +#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT 5 +#define REMMINA_PLUGIN_VNC_FEATURE_SCALE 6 +#define REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS 7 +#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL 8 +#define REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR 9 +#define REMMINA_PLUGIN_VNC_FEATURE_DYNRESUPDATE 10 + +#define VNC_DEFAULT_PORT 5900 + +#define GET_PLUGIN_DATA(gp) (RemminaPluginVncData *)g_object_get_data(G_OBJECT(gp), "plugin-data") + +static RemminaPluginService *remmina_plugin_service = NULL; + +static int dot_cursor_x_hot = 2; +static int dot_cursor_y_hot = 2; +static const gchar *dot_cursor_xpm[] = +{ "5 5 3 1", " c None", ". c #000000", "+ c #FFFFFF", " ... ", ".+++.", ".+ +.", ".+++.", " ... " }; + + +#define LOCK_BUFFER(t) if (t) { CANCEL_DEFER } pthread_mutex_lock(&gpdata->buffer_mutex); +#define UNLOCK_BUFFER(t) pthread_mutex_unlock(&gpdata->buffer_mutex); if (t) { CANCEL_ASYNC } + +struct onMainThread_cb_data { + enum { FUNC_UPDATE_SCALE } func; + GtkWidget * widget; + gint x, y, width, height; + RemminaProtocolWidget * gp; + gboolean scale; + + /* Mutex for thread synchronization */ + pthread_mutex_t mu; + /* Flag to catch cancellations */ + gboolean cancelled; +}; + +static gboolean onMainThread_cb(struct onMainThread_cb_data *d) +{ + TRACE_CALL(__func__); + if (!d->cancelled) { + switch (d->func) { + case FUNC_UPDATE_SCALE: + remmina_plugin_vnc_update_scale(d->gp, d->scale); + break; + } + pthread_mutex_unlock(&d->mu); + } else { + /* thread has been cancelled, so we must free d memory here */ + g_free(d); + } + return G_SOURCE_REMOVE; +} + + +static void onMainThread_cleanup_handler(gpointer data) +{ + TRACE_CALL(__func__); + struct onMainThread_cb_data *d = data; + + d->cancelled = TRUE; +} + + +static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d) +{ + TRACE_CALL(__func__); + d->cancelled = FALSE; + pthread_cleanup_push(onMainThread_cleanup_handler, d); + pthread_mutex_init(&d->mu, NULL); + pthread_mutex_lock(&d->mu); + gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer)d); + + pthread_mutex_lock(&d->mu); + + pthread_cleanup_pop(0); + pthread_mutex_unlock(&d->mu); + pthread_mutex_destroy(&d->mu); +} + +/** + * Function check_for_endianness() returns 1, if architecture + * is little endian, 0 in case of big endian. + */ +static gboolean check_for_endianness() +{ + unsigned int x = 1; + char *c = (char *)&x; + + return (int)*c; +} + +static void remmina_plugin_vnc_event_push(RemminaProtocolWidget *gp, gint event_type, gpointer p1, gpointer p2, gpointer p3) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaPluginVncEvent *event; + + event = g_new(RemminaPluginVncEvent, 1); + event->event_type = event_type; + switch (event_type) { + case REMMINA_PLUGIN_VNC_EVENT_KEY: + event->event_data.key.keyval = GPOINTER_TO_UINT(p1); + event->event_data.key.pressed = GPOINTER_TO_INT(p2); + break; + case REMMINA_PLUGIN_VNC_EVENT_POINTER: + event->event_data.pointer.x = GPOINTER_TO_INT(p1); + event->event_data.pointer.y = GPOINTER_TO_INT(p2); + event->event_data.pointer.button_mask = GPOINTER_TO_INT(p3); + break; + case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT: + case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND: + event->event_data.text.text = g_strdup((char *)p1); + break; + default: + break; + } + + pthread_mutex_lock(&gpdata->vnc_event_queue_mutex); + g_queue_push_tail(gpdata->vnc_event_queue, event); + pthread_mutex_unlock(&gpdata->vnc_event_queue_mutex); + + if (write(gpdata->vnc_event_pipe[1], "\0", 1)) { + /* Ignore */ + } +} + +static void remmina_plugin_vnc_event_free(RemminaPluginVncEvent *event) +{ + TRACE_CALL(__func__); + switch (event->event_type) { + case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT: + case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND: + g_free(event->event_data.text.text); + break; + default: + break; + } + g_free(event); +} + +static void remmina_plugin_vnc_event_free_all(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaPluginVncEvent *event; + + /* This is called from main thread after plugin thread has + * been closed, so no queue locking is necessary here */ + while ((event = g_queue_pop_head(gpdata->vnc_event_queue)) != NULL) + remmina_plugin_vnc_event_free(event); +} + +static void remmina_plugin_vnc_scale_area(RemminaProtocolWidget *gp, gint *x, gint *y, gint *w, gint *h) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GtkAllocation widget_allocation; + gint width, height; + gint sx, sy, sw, sh; + + if (gpdata->rgb_buffer == NULL) + return; + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + + gtk_widget_get_allocation(GTK_WIDGET(gp), &widget_allocation); + + if (widget_allocation.width == width && widget_allocation.height == height) + return; /* Same size, no scaling */ + + /* We have to extend the scaled region 2 scaled pixels, to avoid gaps */ + sx = MIN(MAX(0, (*x) * widget_allocation.width / width - widget_allocation.width / width - 2), widget_allocation.width - 1); + sy = MIN(MAX(0, (*y) * widget_allocation.height / height - widget_allocation.height / height - 2), widget_allocation.height - 1); + sw = MIN(widget_allocation.width - sx, (*w) * widget_allocation.width / width + widget_allocation.width / width + 4); + sh = MIN(widget_allocation.height - sy, (*h) * widget_allocation.height / height + widget_allocation.height / height + 4); + + *x = sx; + *y = sy; + *w = sw; + *h = sh; +} + +static void remmina_plugin_vnc_update_scale(RemminaProtocolWidget *gp, gboolean scale) +{ + TRACE_CALL(__func__); + /* This function can be called from a non main thread */ + + RemminaPluginVncData *gpdata; + gint width, height; + + if (!remmina_plugin_service->is_main_thread()) { + struct onMainThread_cb_data *d; + d = (struct onMainThread_cb_data *)g_malloc(sizeof(struct onMainThread_cb_data)); + d->func = FUNC_UPDATE_SCALE; + d->gp = gp; + d->scale = scale; + onMainThread_schedule_callback_and_wait(d); + g_free(d); + return; + } + + gpdata = GET_PLUGIN_DATA(gp); + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + if (scale) + /* In scaled mode, drawing_area will get its dimensions from its parent */ + gtk_widget_set_size_request(GTK_WIDGET(gpdata->drawing_area), -1, -1); + else + /* In non scaled mode, the plugins forces dimensions of drawing area */ + gtk_widget_set_size_request(GTK_WIDGET(gpdata->drawing_area), width, height); + + remmina_plugin_service->protocol_plugin_update_align(gp); +} + +gboolean remmina_plugin_vnc_setcursor(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GdkCursor *cur; + + LOCK_BUFFER(FALSE); + gpdata->queuecursor_handler = 0; + + if (gpdata->queuecursor_surface) { + cur = gdk_cursor_new_from_surface(gdk_display_get_default(), gpdata->queuecursor_surface, gpdata->queuecursor_x, + gpdata->queuecursor_y); + gdk_window_set_cursor(gtk_widget_get_window(gpdata->drawing_area), cur); + g_object_unref(cur); + cairo_surface_destroy(gpdata->queuecursor_surface); + gpdata->queuecursor_surface = NULL; + } else { + gdk_window_set_cursor(gtk_widget_get_window(gpdata->drawing_area), NULL); + } + UNLOCK_BUFFER(FALSE); + + return FALSE; +} + +static void remmina_plugin_vnc_queuecursor(RemminaProtocolWidget *gp, cairo_surface_t *surface, gint x, gint y) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + if (gpdata->queuecursor_surface) + cairo_surface_destroy(gpdata->queuecursor_surface); + gpdata->queuecursor_surface = surface; + gpdata->queuecursor_x = x; + gpdata->queuecursor_y = y; + if (!gpdata->queuecursor_handler) + gpdata->queuecursor_handler = IDLE_ADD((GSourceFunc)remmina_plugin_vnc_setcursor, gp); +} + +typedef struct _RemminaKeyVal { + guint keyval; + guint16 keycode; +} RemminaKeyVal; + +/***************************** LibVNCClient related codes *********************************/ + +static const uint32_t remmina_plugin_vnc_no_encrypt_auth_types[] = +{ rfbNoAuth, rfbVncAuth, rfbMSLogon, 0 }; + +static RemminaPluginVncEvent *remmina_plugin_vnc_event_queue_pop_head(RemminaPluginVncData *gpdata) +{ + RemminaPluginVncEvent *event; + + CANCEL_DEFER; + pthread_mutex_lock(&gpdata->vnc_event_queue_mutex); + + event = g_queue_pop_head(gpdata->vnc_event_queue); + + pthread_mutex_unlock(&gpdata->vnc_event_queue_mutex); + CANCEL_ASYNC; + + return event; +} + +static void remmina_plugin_vnc_process_vnc_event(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncEvent *event; + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + rfbClient *cl; + gchar buf[100]; + + cl = (rfbClient *)gpdata->client; + while ((event = remmina_plugin_vnc_event_queue_pop_head(gpdata)) != NULL) { + if (cl) { + switch (event->event_type) { + case REMMINA_PLUGIN_VNC_EVENT_KEY: + SendKeyEvent(cl, event->event_data.key.keyval, event->event_data.key.pressed); + break; + case REMMINA_PLUGIN_VNC_EVENT_POINTER: + SendPointerEvent(cl, event->event_data.pointer.x, event->event_data.pointer.y, + event->event_data.pointer.button_mask); + break; + case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT: + if (event->event_data.text.text) { + rfbClientLog("sending clipboard text '%s'\n", event->event_data.text.text); + SendClientCutText(cl, event->event_data.text.text, strlen(event->event_data.text.text)); + } + break; + case REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN: + TextChatOpen(cl); + break; + case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND: + TextChatSend(cl, event->event_data.text.text); + break; + case REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE: + TextChatClose(cl); + TextChatFinish(cl); + break; + default: + rfbClientLog("Ignoring VNC event: 0x%x\n", event->event_type); + break; + } + } + remmina_plugin_vnc_event_free(event); + } + if (read(gpdata->vnc_event_pipe[0], buf, sizeof(buf))) { + /* Ignore */ + } +} + +typedef struct _RemminaPluginVncCuttextParam { + RemminaProtocolWidget * gp; + gchar * text; + gint textlen; +} RemminaPluginVncCuttextParam; + +static void remmina_plugin_vnc_update_quality(rfbClient *cl, gint quality) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaFile *remminafile; + gchar *enc = NULL; + + /** + * "0", "Poor (fastest)" + * "1", "Medium" + * "2", "Good" + * "9", "Best (slowest)" + */ + switch (quality) { + case 9: + cl->appData.useBGR233 = 0; + cl->appData.encodingsString = "copyrect zlib hextile raw"; + cl->appData.compressLevel = 1; + cl->appData.qualityLevel = 9; + break; + case 2: + cl->appData.useBGR233 = 0; + cl->appData.encodingsString = "tight zrle ultra copyrect hextile zlib corre rre raw"; + cl->appData.compressLevel = 2; + cl->appData.qualityLevel = 7; + break; + case 1: + cl->appData.useBGR233 = 0; + cl->appData.encodingsString = "tight zrle ultra copyrect hextile zlib corre rre raw"; + cl->appData.compressLevel = 3; + cl->appData.qualityLevel = 5; + break; + case 0: + default: + // bpp8 and tight encoding is not supported in libvnc + cl->appData.useBGR233 = 1; + cl->appData.encodingsString = "copyrect zrle ultra zlib hextile corre rre raw"; + cl->appData.qualityLevel = 1; + break; + } + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + enc = g_strdup(remmina_plugin_service->file_get_string(remminafile, "encodings")); + if (enc) { + cl->appData.encodingsString = g_strdup(enc); + g_free (enc), enc = NULL; + } + gboolean tight = remmina_plugin_service->file_get_int (remminafile, "tightencoding", FALSE); + if (tight) { + if (!g_strrstr ( g_strdup(cl->appData.encodingsString), "tight\0")) { + cl->appData.encodingsString = g_strdup_printf("%s %s", "tight", g_strdup(cl->appData.encodingsString)); + } + } + + REMMINA_PLUGIN_DEBUG("Quality: %d", quality); + REMMINA_PLUGIN_DEBUG("Encodings: %s", cl->appData.encodingsString); +} + +static void remmina_plugin_vnc_update_colordepth(rfbClient *cl, gint colordepth) +{ + TRACE_CALL(__func__); + + cl->format.depth = colordepth; + cl->appData.requestedDepth = colordepth; + + cl->format.trueColour = 1; + cl->format.bigEndian = check_for_endianness()?FALSE:TRUE; + + switch (colordepth) { + case 8: + cl->format.depth = 8; + cl->format.bitsPerPixel = 8; + cl->format.blueMax = 3; + cl->format.blueShift = 6; + cl->format.greenMax = 7; + cl->format.greenShift = 3; + cl->format.redMax = 7; + cl->format.redShift = 0; + break; + case 16: + cl->format.depth = 15; + cl->format.bitsPerPixel = 16; + cl->format.redShift = 11; + cl->format.greenShift = 6; + cl->format.blueShift = 1; + cl->format.redMax = 31; + cl->format.greenMax = 31; + cl->format.blueMax = 31; + break; + case 32: + default: + cl->format.depth = 24; + cl->format.bitsPerPixel = 32; + cl->format.blueShift = 0; + cl->format.redShift = 16; + cl->format.greenShift = 8; + cl->format.blueMax = 0xff; + cl->format.redMax = 0xff; + cl->format.greenMax = 0xff; + break; + } + + rfbClientLog("colordepth = %d\n", colordepth); + rfbClientLog("format.depth = %d\n", cl->format.depth); + rfbClientLog("format.bitsPerPixel = %d\n", cl->format.bitsPerPixel); + rfbClientLog("format.blueShift = %d\n", cl->format.blueShift); + rfbClientLog("format.redShift = %d\n", cl->format.redShift); + rfbClientLog("format.trueColour = %d\n", cl->format.trueColour); + rfbClientLog("format.greenShift = %d\n", cl->format.greenShift); + rfbClientLog("format.blueMax = %d\n", cl->format.blueMax); + rfbClientLog("format.redMax = %d\n", cl->format.redMax); + rfbClientLog("format.greenMax = %d\n", cl->format.greenMax); + rfbClientLog("format.bigEndian = %d\n", cl->format.bigEndian); +} + +static rfbBool remmina_plugin_vnc_rfb_allocfb(rfbClient *cl) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint width, height, depth, size; + gboolean scale; + cairo_surface_t *new_surface, *old_surface; + + width = cl->width; + height = cl->height; + depth = cl->format.bitsPerPixel; + size = width * height * (depth / 8); + + new_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + if (cairo_surface_status(new_surface) != CAIRO_STATUS_SUCCESS) + return FALSE; + old_surface = gpdata->rgb_buffer; + + LOCK_BUFFER(TRUE); + + remmina_plugin_service->protocol_plugin_set_width(gp, width); + remmina_plugin_service->protocol_plugin_set_height(gp, height); + + gpdata->rgb_buffer = new_surface; + + if (gpdata->vnc_buffer) + g_free(gpdata->vnc_buffer); + gpdata->vnc_buffer = (guchar *)g_malloc(size); + cl->frameBuffer = gpdata->vnc_buffer; + + UNLOCK_BUFFER(TRUE); + + if (old_surface) + cairo_surface_destroy(old_surface); + + scale = (remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE); + remmina_plugin_vnc_update_scale(gp, scale); + + /* Notify window of change so that scroll border can be hidden or shown if needed */ + remmina_plugin_service->protocol_plugin_desktop_resize(gp); + + /* Refresh the client’s updateRect - bug in xvncclient */ + cl->updateRect.w = width; + cl->updateRect.h = height; + + return TRUE; +} + +static gint remmina_plugin_vnc_bits(gint n) +{ + TRACE_CALL(__func__); + gint b = 0; + + while (n) { + b++; + n >>= 1; + } + return b ? b : 1; +} + +static gboolean remmina_plugin_vnc_queue_draw_area_real(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint x, y, w, h; + + if (GTK_IS_WIDGET(gp) && gpdata->connected) { + LOCK_BUFFER(FALSE); + x = gpdata->queuedraw_x; + y = gpdata->queuedraw_y; + w = gpdata->queuedraw_w; + h = gpdata->queuedraw_h; + gpdata->queuedraw_handler = 0; + UNLOCK_BUFFER(FALSE); + + gtk_widget_queue_draw_area(GTK_WIDGET(gp), x, y, w, h); + } + return FALSE; +} + +static void remmina_plugin_vnc_queue_draw_area(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint nx2, ny2, ox2, oy2; + + LOCK_BUFFER(TRUE); + if (gpdata->queuedraw_handler) { + nx2 = x + w; + ny2 = y + h; + ox2 = gpdata->queuedraw_x + gpdata->queuedraw_w; + oy2 = gpdata->queuedraw_y + gpdata->queuedraw_h; + gpdata->queuedraw_x = MIN(gpdata->queuedraw_x, x); + gpdata->queuedraw_y = MIN(gpdata->queuedraw_y, y); + gpdata->queuedraw_w = MAX(ox2, nx2) - gpdata->queuedraw_x; + gpdata->queuedraw_h = MAX(oy2, ny2) - gpdata->queuedraw_y; + } else { + gpdata->queuedraw_x = x; + gpdata->queuedraw_y = y; + gpdata->queuedraw_w = w; + gpdata->queuedraw_h = h; + gpdata->queuedraw_handler = IDLE_ADD((GSourceFunc)remmina_plugin_vnc_queue_draw_area_real, gp); + } + UNLOCK_BUFFER(TRUE); +} + +static void remmina_plugin_vnc_rfb_fill_buffer(rfbClient *cl, guchar *dest, gint dest_rowstride, guchar *src, + gint src_rowstride, guchar *mask, gint w, gint h) +{ + TRACE_CALL(__func__); + guchar *srcptr; + gint bytesPerPixel; + guint32 src_pixel; + gint ix, iy; + gint i; + guchar c; + gint rs, gs, bs, rm, gm, bm, rl, gl, bl, rr, gr, br; + gint r; + guint32 *destptr; + + union { + struct { + guchar a, r, g, b; + } colors; + guint32 argb; + } dst_pixel; + + bytesPerPixel = cl->format.bitsPerPixel / 8; + switch (cl->format.bitsPerPixel) { + case 32: + /* The following codes fill in the Alpha channel swap red/green value */ + for (iy = 0; iy < h; iy++) { + destptr = (guint32 *)(dest + iy * dest_rowstride); + srcptr = src + iy * src_rowstride; + for (ix = 0; ix < w; ix++) { + if (!mask || *mask++) { + dst_pixel.colors.a = 0xff; + dst_pixel.colors.r = *(srcptr + 2); + dst_pixel.colors.g = *(srcptr + 1); + dst_pixel.colors.b = *srcptr; + *destptr++ = ntohl(dst_pixel.argb); + } else { + *destptr++ = 0; + } + srcptr += 4; + } + } + break; + default: + rm = cl->format.redMax; + gm = cl->format.greenMax; + bm = cl->format.blueMax; + rr = remmina_plugin_vnc_bits(rm); + gr = remmina_plugin_vnc_bits(gm); + br = remmina_plugin_vnc_bits(bm); + rl = 8 - rr; + gl = 8 - gr; + bl = 8 - br; + rs = cl->format.redShift; + gs = cl->format.greenShift; + bs = cl->format.blueShift; + for (iy = 0; iy < h; iy++) { + destptr = (guint32 *)(dest + iy * dest_rowstride); + srcptr = src + iy * src_rowstride; + for (ix = 0; ix < w; ix++) { + src_pixel = 0; + for (i = 0; i < bytesPerPixel; i++) + src_pixel += (*srcptr++) << (8 * i); + + if (!mask || *mask++) { + dst_pixel.colors.a = 0xff; + c = (guchar)((src_pixel >> rs) & rm) << rl; + for (r = rr; r < 8; r *= 2) + c |= c >> r; + dst_pixel.colors.r = c; + c = (guchar)((src_pixel >> gs) & gm) << gl; + for (r = gr; r < 8; r *= 2) + c |= c >> r; + dst_pixel.colors.g = c; + c = (guchar)((src_pixel >> bs) & bm) << bl; + for (r = br; r < 8; r *= 2) + c |= c >> r; + dst_pixel.colors.b = c; + *destptr++ = ntohl(dst_pixel.argb); + } else { + *destptr++ = 0; + } + } + } + break; + } +} + +static void remmina_plugin_vnc_rfb_updatefb(rfbClient *cl, int x, int y, int w, int h) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint bytesPerPixel; + gint rowstride; + gint width; + + LOCK_BUFFER(TRUE); + + if (w >= 1 || h >= 1) { + width = remmina_plugin_service->protocol_plugin_get_width(gp); + bytesPerPixel = cl->format.bitsPerPixel / 8; + rowstride = cairo_image_surface_get_stride(gpdata->rgb_buffer); + cairo_surface_flush(gpdata->rgb_buffer); + remmina_plugin_vnc_rfb_fill_buffer(cl, cairo_image_surface_get_data(gpdata->rgb_buffer) + y * rowstride + x * 4, + rowstride, gpdata->vnc_buffer + ((y * width + x) * bytesPerPixel), width * bytesPerPixel, NULL, + w, h); + cairo_surface_mark_dirty(gpdata->rgb_buffer); + } + + if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) + remmina_plugin_vnc_scale_area(gp, &x, &y, &w, &h); + + UNLOCK_BUFFER(TRUE); + + remmina_plugin_vnc_queue_draw_area(gp, x, y, w, h); +} + +static void remmina_plugin_vnc_rfb_finished(rfbClient *cl) __attribute__ ((unused)); +static void remmina_plugin_vnc_rfb_finished(rfbClient *cl) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("FinishedFrameBufferUpdate"); +} + +static void remmina_plugin_vnc_rfb_led_state(rfbClient *cl, int value, int pad) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Led state - value: %d, pad: %d", value, pad); +} + +static gboolean remmina_plugin_vnc_queue_cuttext(RemminaPluginVncCuttextParam *param) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = param->gp; + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GDateTime *t; + glong diff; + const char *cur_charset; + gchar *text; + gsize br, bw; + + if (GTK_IS_WIDGET(gp) && gpdata->connected) { + t = g_date_time_new_now_utc(); + diff = g_date_time_difference(t, gpdata->clipboard_timer) / 100000; // tenth of second + if (diff >= 10) { + g_date_time_unref(gpdata->clipboard_timer); + gpdata->clipboard_timer = t; + /* Convert text from VNC latin-1 to current GTK charset (usually UTF-8) */ + g_get_charset(&cur_charset); + text = g_convert_with_fallback(param->text, param->textlen, cur_charset, "ISO-8859-1", "?", &br, &bw, NULL); + gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), text, bw); + g_free(text); + } else { + g_date_time_unref(t); + } + } + g_free(param->text); + g_free(param); + return FALSE; +} + +static void remmina_plugin_vnc_rfb_cuttext(rfbClient *cl, const char *text, int textlen) +{ + TRACE_CALL(__func__); + RemminaPluginVncCuttextParam *param; + + param = g_new(RemminaPluginVncCuttextParam, 1); + param->gp = (RemminaProtocolWidget *)rfbClientGetClientData(cl, NULL); + param->text = g_malloc(textlen); + memcpy(param->text, text, textlen); + param->textlen = textlen; + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_queue_cuttext, param); +} + +static char * +remmina_plugin_vnc_rfb_password(rfbClient *cl) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + gchar *pwd = NULL; + + gpdata->auth_called = TRUE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (gpdata->auth_first) + pwd = g_strdup(remmina_plugin_service->file_get_string(remminafile, "password")); + if (!pwd) { + gboolean save; + gint ret; + gboolean disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE); + ret = remmina_plugin_service->protocol_plugin_init_auth(gp, + (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD), + _("Enter VNC password"), + NULL, + remmina_plugin_service->file_get_string(remminafile, "password"), + NULL, + NULL); + if (ret != GTK_RESPONSE_OK) { + gpdata->connected = FALSE; + return NULL; + } + pwd = remmina_plugin_service->protocol_plugin_init_get_password(gp); + save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + if (save) + remmina_plugin_service->file_set_string(remminafile, "password", pwd); + else + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + return pwd; +} + +static rfbCredential * +remmina_plugin_vnc_rfb_credential(rfbClient *cl, int credentialType) +{ + TRACE_CALL(__func__); + rfbCredential *cred; + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + gint ret; + gchar *s1, *s2; + gboolean disablepasswordstoring; + + gpdata->auth_called = TRUE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + cred = g_new0(rfbCredential, 1); + + switch (credentialType) { + case rfbCredentialTypeUser: + + s1 = g_strdup(remmina_plugin_service->file_get_string(remminafile, "username")); + + s2 = g_strdup(remmina_plugin_service->file_get_string(remminafile, "password")); + + if (gpdata->auth_first && s1 && s2) { + cred->userCredential.username = s1; + cred->userCredential.password = s2; + } else { + g_free(s1); + g_free(s2); + + disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE); + ret = remmina_plugin_service->protocol_plugin_init_auth(gp, + (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME, + _("Enter VNC authentication credentials"), + remmina_plugin_service->file_get_string(remminafile, "username"), + remmina_plugin_service->file_get_string(remminafile, "password"), + NULL, + NULL); + if (ret == GTK_RESPONSE_OK) { + gboolean save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp); + cred->userCredential.username = remmina_plugin_service->protocol_plugin_init_get_username(gp); + cred->userCredential.password = remmina_plugin_service->protocol_plugin_init_get_password(gp); + if (save) { + remmina_plugin_service->file_set_string(remminafile, "username", cred->userCredential.username); + remmina_plugin_service->file_set_string(remminafile, "password", cred->userCredential.password); + } else { + remmina_plugin_service->file_set_string(remminafile, "username", NULL); + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + } + } else { + g_free(cred); + cred = NULL; + gpdata->connected = FALSE; + } + } + break; + + case rfbCredentialTypeX509: + if (gpdata->auth_first && + remmina_plugin_service->file_get_string(remminafile, "cacert")) { + cred->x509Credential.x509CACertFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "cacert")); + cred->x509Credential.x509CACrlFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "cacrl")); + cred->x509Credential.x509ClientCertFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "clientcert")); + cred->x509Credential.x509ClientKeyFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "clientkey")); + } else { + ret = remmina_plugin_service->protocol_plugin_init_authx509(gp); + + if (ret == GTK_RESPONSE_OK) { + cred->x509Credential.x509CACertFile = remmina_plugin_service->protocol_plugin_init_get_cacert(gp); + cred->x509Credential.x509CACrlFile = remmina_plugin_service->protocol_plugin_init_get_cacrl(gp); + cred->x509Credential.x509ClientCertFile = remmina_plugin_service->protocol_plugin_init_get_clientcert(gp); + cred->x509Credential.x509ClientKeyFile = remmina_plugin_service->protocol_plugin_init_get_clientkey(gp); + } else { + g_free(cred); + cred = NULL; + gpdata->connected = FALSE; + } + } + break; + + default: + g_free(cred); + cred = NULL; + break; + } + return cred; +} + +static void remmina_plugin_vnc_rfb_cursor_shape(rfbClient *cl, int xhot, int yhot, int width, int height, int bytesPerPixel) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + if (!gtk_widget_get_window(GTK_WIDGET(gp))) + return; + + if (width && height) { + gint stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + guchar *data = g_malloc(stride * height); + remmina_plugin_vnc_rfb_fill_buffer(cl, data, stride, cl->rcSource, + width * cl->format.bitsPerPixel / 8, cl->rcMask, width, height); + cairo_surface_t *surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, width, height, stride); + if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + g_free(data); + return; + } + if (cairo_surface_set_user_data(surface, NULL, NULL, g_free) != CAIRO_STATUS_SUCCESS) { + g_free(data); + return; + } + + LOCK_BUFFER(TRUE); + remmina_plugin_vnc_queuecursor(gp, surface, xhot, yhot); + UNLOCK_BUFFER(TRUE); + } +} + +static void remmina_plugin_vnc_rfb_bell(rfbClient *cl) +{ + TRACE_CALL(__func__); + REMMINA_PLUGIN_DEBUG("Bell message received"); + RemminaProtocolWidget *gp; + RemminaFile *remminafile; + GdkWindow *window; + + gp = (RemminaProtocolWidget *)(rfbClientGetClientData(cl, NULL)); + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "disableserverbell", FALSE)) + return; + + window = gtk_widget_get_window(GTK_WIDGET(gp)); + + if (window) + gdk_window_beep(window); + REMMINA_PLUGIN_DEBUG("Beep emitted"); +} + +/* Translate known VNC messages. It’s for intltool only, not for gcc */ +#ifdef __DO_NOT_COMPILE_ME__ +N_("Unable to connect to VNC server") +N_("Couldn’t convert '%s' to host address") +N_("VNC connection failed: %s") +N_("Your connection has been rejected.") +#endif +/** @todo We only store the last message at this moment. */ +#define MAX_ERROR_LENGTH 1000 +static gchar vnc_error[MAX_ERROR_LENGTH + 1]; +static gboolean vnc_encryption_disable_requested; + +static void remmina_plugin_vnc_rfb_output(const char *format, ...) +{ + TRACE_CALL(__func__); + gchar *f, *p, *ff; + + if (!rfbEnableClientLogging) + return; + + va_list args; + va_start(args, format); + /* eliminate the last \n */ + f = g_strdup(format); + if (f[strlen(f) - 1] == '\n') f[strlen(f) - 1] = '\0'; + + if (g_strcmp0(f, "VNC connection failed: %s") == 0) { + p = va_arg(args, gchar *); + g_snprintf(vnc_error, MAX_ERROR_LENGTH, _(f), _(p)); + } else if (g_strcmp0(f, "The VNC server requested an unknown authentication method. %s") == 0) { + p = va_arg(args, gchar *); + if (vnc_encryption_disable_requested) { + ff = g_strconcat(_("The VNC server requested an unknown authentication method. %s"), + ". ", + _("Please retry after turning on encryption for this profile."), + NULL); + g_snprintf(vnc_error, MAX_ERROR_LENGTH, ff, p); + g_free(ff); + } else { + g_snprintf(vnc_error, MAX_ERROR_LENGTH, _(f), p); + } + } else { + g_vsnprintf(vnc_error, MAX_ERROR_LENGTH, _(f), args); + } + g_free(f); + va_end(args); + + REMMINA_PLUGIN_DEBUG("VNC returned: %s", vnc_error); +} + +static void remmina_plugin_vnc_chat_on_send(RemminaProtocolWidget *gp, const gchar *text) +{ + TRACE_CALL(__func__); + gchar *ptr; + + /* Need to add a line-feed for UltraVNC */ + ptr = g_strdup_printf("%s\n", text); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND, ptr, NULL, NULL); + g_free(ptr); +} + +static void remmina_plugin_vnc_chat_on_destroy(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE, NULL, NULL, NULL); +} + +/* Send CTRL+ALT+DEL keys keystrokes to the plugin drawing_area widget */ +static void remmina_plugin_vnc_send_ctrlaltdel(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete }; + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->drawing_area, + keys, G_N_ELEMENTS(keys), GDK_KEY_PRESS | GDK_KEY_RELEASE); +} + +static gboolean remmina_plugin_vnc_close_chat(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + remmina_plugin_service->protocol_plugin_chat_close(gp); + return FALSE; +} + +static gboolean remmina_plugin_vnc_open_chat(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + rfbClient *cl; + + cl = (rfbClient *)gpdata->client; + + remmina_plugin_service->protocol_plugin_chat_open(gp, cl->desktopName, remmina_plugin_vnc_chat_on_send, + remmina_plugin_vnc_chat_on_destroy); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN, NULL, NULL, NULL); + return FALSE; +} + +static void remmina_plugin_vnc_rfb_chat(rfbClient *cl, int value, char *text) +{ + TRACE_CALL(__func__); + RemminaProtocolWidget *gp; + + gp = (RemminaProtocolWidget *)(rfbClientGetClientData(cl, NULL)); + switch (value) { + case rfbTextChatOpen: + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_open_chat, gp); + break; + case rfbTextChatClose: + /* Do nothing… but wait for the next rfbTextChatFinished signal */ + break; + case rfbTextChatFinished: + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_close_chat, gp); + break; + default: + /* value is the text length */ + remmina_plugin_service->protocol_plugin_chat_receive(gp, text); + break; + } +} + +static gboolean remmina_plugin_vnc_incoming_connection(RemminaProtocolWidget *gp, rfbClient *cl) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + fd_set fds; + + /** + * @fixme This may fail or not working as expected with multiple network interfaces, + * change with ListenAtTcpPortAndAddress + */ + gpdata->listen_sock = ListenAtTcpPort(cl->listenPort); + if (gpdata->listen_sock < 0) + return FALSE; + + remmina_plugin_service->protocol_plugin_init_show_listen(gp, cl->listenPort); + + remmina_plugin_service->protocol_plugin_start_reverse_tunnel(gp, cl->listenPort); + + FD_ZERO(&fds); + if (gpdata->listen_sock >= 0) + FD_SET(gpdata->listen_sock, &fds); + + select(gpdata->listen_sock + 1, &fds, NULL, NULL, NULL); + + if (!FD_ISSET(gpdata->listen_sock, &fds)) { + close(gpdata->listen_sock); + gpdata->listen_sock = -1; + return FALSE; + } + + if (FD_ISSET(gpdata->listen_sock, &fds)) + cl->sock = AcceptTcpConnection(gpdata->listen_sock); + if (cl->sock >= 0) { + close(gpdata->listen_sock); + gpdata->listen_sock = -1; + } + if (cl->sock < 0 || !SetNonBlocking(cl->sock)) + return FALSE; + + return TRUE; +} + + +static gboolean remmina_plugin_vnc_main_loop(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + gint ret; + gint i; + rfbClient *cl; + fd_set fds; + struct timeval timeout; + + if (!gpdata->connected) { + gpdata->running = FALSE; + return FALSE; + } + + cl = (rfbClient *)gpdata->client; + + /* + * Do not explicitly wait while data is on the buffer, see: + * - https://jira.glyptodon.com/browse/GUAC-1056 + * - https://jira.glyptodon.com/browse/GUAC-1056?focusedCommentId=14348&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14348 + * - https://github.com/apache/guacamole-server/blob/67680bd2d51e7949453f0f7ffc7f4234a1136715/src/protocols/vnc/vnc.c#L155 + */ + if (cl->buffered) + goto handle_buffered; + + timeout.tv_sec = 10; + timeout.tv_usec = 0; + FD_ZERO(&fds); + FD_SET(cl->sock, &fds); + FD_SET(gpdata->vnc_event_pipe[0], &fds); + ret = select(MAX(cl->sock, gpdata->vnc_event_pipe[0]) + 1, &fds, NULL, NULL, &timeout); + + /* Sometimes it returns <0 when opening a modal dialog in other window. Absolutely weird */ + /* So we continue looping anyway */ + if (ret <= 0) + return TRUE; + + if (FD_ISSET(gpdata->vnc_event_pipe[0], &fds)) + remmina_plugin_vnc_process_vnc_event(gp); + if (FD_ISSET(cl->sock, &fds)) { + i = WaitForMessage(cl, 500); + if (i < 0) + return TRUE; +handle_buffered: + if (!HandleRFBServerMessage(cl)) { + gpdata->running = FALSE; + if (gpdata->connected && !remmina_plugin_service->protocol_plugin_is_closed(gp)) + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + return FALSE; + } + } + + return TRUE; +} + +static gboolean remmina_plugin_vnc_main(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + rfbClient *cl = NULL; + gchar *host; + gchar *s = NULL; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + gpdata->running = TRUE; + + rfbClientLog = rfbClientErr = remmina_plugin_vnc_rfb_output; + + gint colordepth = remmina_plugin_service->file_get_int(remminafile, "colordepth", 32); + gint quality = remmina_plugin_service->file_get_int(remminafile, "quality", 9); + + while (gpdata->connected) { + gpdata->auth_called = FALSE; + + host = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, VNC_DEFAULT_PORT, TRUE); + + if (host == NULL) { + REMMINA_PLUGIN_DEBUG("host is null"); + gpdata->connected = FALSE; + break; + } + + /* int bitsPerSample,int samplesPerPixel, int bytesPerPixel */ + switch (colordepth) { + case 8: + cl = rfbGetClient(2, 3, 1); + break; + case 15: + case 16: + cl = rfbGetClient(5, 3, 2); + break; + case 24: + cl = rfbGetClient(6, 3, 3); + break; + case 32: + default: + cl = rfbGetClient(8, 3, 4); + break; + } + REMMINA_PLUGIN_DEBUG("Color depth: %d", colordepth); + cl->MallocFrameBuffer = remmina_plugin_vnc_rfb_allocfb; + cl->canHandleNewFBSize = TRUE; + cl->GetPassword = remmina_plugin_vnc_rfb_password; + cl->GetCredential = remmina_plugin_vnc_rfb_credential; + cl->GotFrameBufferUpdate = remmina_plugin_vnc_rfb_updatefb; + /** + * @fixme we have to implement FinishedFrameBufferUpdate + * This is to know when the server has finished to send a batch of frame buffer + * updates. + * cl->FinishedFrameBufferUpdate = remmina_plugin_vnc_rfb_finished; + */ + /** + * @fixme we have to implement HandleKeyboardLedState + * cl->HandleKeyboardLedState = remmina_plugin_vnc_rfb_led_state + */ + cl->HandleKeyboardLedState = remmina_plugin_vnc_rfb_led_state; + cl->GotXCutText = ( + remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE) ? + NULL : remmina_plugin_vnc_rfb_cuttext); + cl->GotCursorShape = remmina_plugin_vnc_rfb_cursor_shape; + cl->Bell = remmina_plugin_vnc_rfb_bell; + cl->HandleTextChat = remmina_plugin_vnc_rfb_chat; + /** + * @fixme we have to implement HandleXvpMsg + * cl->HandleXvpMsg = remmina_plugin_vnc_rfb_handle_xvp; + */ + + rfbClientSetClientData(cl, NULL, gp); + + if (host[0] == '\0') { + cl->serverHost = g_strdup(host); + cl->listenSpecified = TRUE; + if (remmina_plugin_service->file_get_int(remminafile, "ssh_tunnel_enabled", FALSE)) + /* When we use reverse tunnel, the local port does not really matter. + * Hardcode a default port just in case the remote port is customized + * to a privilege port then we will have problem listening. */ + cl->listenPort = 5500; + else + cl->listenPort = remmina_plugin_service->file_get_int(remminafile, "listenport", 5500); + + remmina_plugin_vnc_incoming_connection(gp, cl); + } else { + if (strstr(host, "unix://") == host) { + cl->serverHost = g_strdup(host + strlen("unix://")); + cl->serverPort = 0; + } else { + remmina_plugin_service->get_server_port(host, VNC_DEFAULT_PORT, &s, &cl->serverPort); + cl->serverHost = g_strdup(s); + g_free(s); + /* Support short-form (:0, :1) */ + if (cl->serverPort < 100) + cl->serverPort += VNC_DEFAULT_PORT; + } + } + g_free(host); + host = NULL; + + if (cl->serverHost && strstr(cl->serverHost, "unix://") != cl->serverHost && remmina_plugin_service->file_get_string(remminafile, "proxy")) { + remmina_plugin_service->get_server_port( + remmina_plugin_service->file_get_string(remminafile, "server"), + VNC_DEFAULT_PORT, + &cl->destHost, + &cl->destPort); + remmina_plugin_service->get_server_port( + remmina_plugin_service->file_get_string(remminafile, "proxy"), + VNC_DEFAULT_PORT, + &cl->serverHost, + &cl->serverPort); + REMMINA_PLUGIN_DEBUG("cl->serverHost: %s", cl->serverHost); + REMMINA_PLUGIN_DEBUG("cl->serverPort: %d", cl->serverPort); + REMMINA_PLUGIN_DEBUG("cl->destHost: %s", cl->destHost); + REMMINA_PLUGIN_DEBUG("cl->destPort: %d", cl->destPort); + } + + cl->appData.useRemoteCursor = ( + remmina_plugin_service->file_get_int(remminafile, "showcursor", FALSE) ? FALSE : TRUE); + + remmina_plugin_vnc_update_quality(cl, quality); + remmina_plugin_vnc_update_colordepth(cl, colordepth); + if ((cl->format.depth == 8) && (quality == 9)) + cl->appData.encodingsString = "copyrect zlib hextile raw"; + else if ((cl->format.depth == 8) && (quality == 2)) + cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw"; + else if ((cl->format.depth == 8) && (quality == 1)) + cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw"; + else if ((cl->format.depth == 8) && (quality == 0)) + cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw"; + SetFormatAndEncodings(cl); + + if (remmina_plugin_service->file_get_int(remminafile, "disableencryption", FALSE)) { + vnc_encryption_disable_requested = TRUE; + SetClientAuthSchemes(cl, remmina_plugin_vnc_no_encrypt_auth_types, -1); + } else { + vnc_encryption_disable_requested = FALSE; + } + + if (rfbInitClient(cl, NULL, NULL)) { + REMMINA_PLUGIN_DEBUG("Client initialization successfull"); + break; + } else { + REMMINA_PLUGIN_DEBUG("Client initialization failed"); + } + + /* If the authentication is not called, it has to be a fatal error and must quit */ + if (!gpdata->auth_called) { + REMMINA_PLUGIN_DEBUG("Client not authenticated"); + gpdata->connected = FALSE; + break; + } + + /* vnc4server reports "already in use" after authentication. Workaround here */ + if (strstr(vnc_error, "The server is already in use")) { + gpdata->connected = FALSE; + gpdata->auth_called = FALSE; + break; + } + /* Don't assume authentication failed for known network-related errors in + libvncclient/sockets.c. */ + if (strstr(vnc_error, "read (") || strstr(vnc_error, "select\n") || + strstr(vnc_error, "write\n") || strstr(vnc_error, "Connection timed out")) { + gpdata->connected = FALSE; + gpdata->auth_called = FALSE; + break; + } + + /* Otherwise, it’s a password error. Try to clear saved password if any */ + remmina_plugin_service->file_set_string(remminafile, "password", NULL); + + if (!gpdata->connected) + break; + + remmina_plugin_service->protocol_plugin_init_show_retry(gp); + + /* It’s safer to sleep a while before reconnect */ + sleep(2); + + gpdata->auth_first = FALSE; + } + + if (!gpdata->connected) { + REMMINA_PLUGIN_DEBUG("Client not connected with error: %s", vnc_error); + if (cl && !gpdata->auth_called && !(remmina_plugin_service->protocol_plugin_has_error(gp))) + remmina_plugin_service->protocol_plugin_set_error(gp, "%s", vnc_error); + gpdata->running = FALSE; + + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + + return FALSE; + } + + REMMINA_PLUGIN_DEBUG("Client connected"); + remmina_plugin_service->protocol_plugin_init_save_cred(gp); + + gpdata->client = cl; + + remmina_plugin_service->protocol_plugin_signal_connection_opened(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "disableserverinput", FALSE)) + PermitServerInput(cl, 1); + + if (gpdata->thread) { + while (remmina_plugin_vnc_main_loop(gp)) { + } + gpdata->running = FALSE; + } else { + IDLE_ADD((GSourceFunc)remmina_plugin_vnc_main_loop, gp); + } + + return FALSE; +} + + +static gpointer +remmina_plugin_vnc_main_thread(gpointer data) +{ + TRACE_CALL(__func__); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + + CANCEL_ASYNC + remmina_plugin_vnc_main((RemminaProtocolWidget *)data); + return NULL; +} + + +static RemminaPluginVncCoordinates remmina_plugin_vnc_scale_coordinates(GtkWidget *widget, RemminaProtocolWidget *gp, gint x, gint y) +{ + GtkAllocation widget_allocation; + RemminaPluginVncCoordinates result; + + if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) { + gtk_widget_get_allocation(widget, &widget_allocation); + result.x = x * remmina_plugin_service->protocol_plugin_get_width(gp) / widget_allocation.width; + result.y = y * remmina_plugin_service->protocol_plugin_get_height(gp) / widget_allocation.height; + } else { + result.x = x; + result.y = y; + } + + return result; +} + +static gboolean remmina_plugin_vnc_on_motion(GtkWidget *widget, GdkEventMotion *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaPluginVncCoordinates coordinates; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(gpdata->button_mask)); + return TRUE; +} + +static gboolean remmina_plugin_vnc_on_button(GtkWidget *widget, GdkEventButton *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaPluginVncCoordinates coordinates; + gint mask; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + /* We only accept 3 buttons */ + if (event->button < 1 || event->button > 3) + return FALSE; + /* We bypass 2button-press and 3button-press events */ + if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) + return TRUE; + + mask = (1 << (event->button - 1)); + gpdata->button_mask = (event->type == GDK_BUTTON_PRESS ? (gpdata->button_mask | mask) : + (gpdata->button_mask & (0xff - mask))); + + coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(gpdata->button_mask)); + return TRUE; +} + +static gint delta_to_mask(float delta, float *accum, gint mask_plus, gint mask_minus) +{ + *accum += delta; + if (*accum >= 1.0) { + *accum = 0.0; + return mask_plus; + } else if (*accum <= -1.0) { + *accum = 0.0; + return mask_minus; + } + return 0; +} + +static gboolean remmina_plugin_vnc_on_scroll(GtkWidget *widget, GdkEventScroll *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaPluginVncCoordinates coordinates; + gint mask; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + switch (event->direction) { + case GDK_SCROLL_UP: + mask = (1 << 3); + gpdata->scroll_y_accumulator = 0; + break; + case GDK_SCROLL_DOWN: + mask = (1 << 4); + gpdata->scroll_y_accumulator = 0; + break; + case GDK_SCROLL_LEFT: + mask = (1 << 5); + gpdata->scroll_x_accumulator = 0; + break; + case GDK_SCROLL_RIGHT: + mask = (1 << 6); + gpdata->scroll_x_accumulator = 0; + break; +#if GTK_CHECK_VERSION(3, 4, 0) + case GDK_SCROLL_SMOOTH: + /* RFB does not seems to support SMOOTH scroll, so we accumulate GTK delta requested + * up to 1.0 and then send a normal RFB wheel scroll when the accumulator reaches 1.0 */ + mask = delta_to_mask(event->delta_y, &(gpdata->scroll_y_accumulator), (1 << 4), (1 << 3)); + mask |= delta_to_mask(event->delta_x, &(gpdata->scroll_x_accumulator), (1 << 6), (1 << 5)); + if (!mask) + return FALSE; + break; +#endif + default: + return FALSE; + } + + coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(mask | gpdata->button_mask)); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y), + GINT_TO_POINTER(gpdata->button_mask)); + + return TRUE; +} + +static void remmina_plugin_vnc_release_key(RemminaProtocolWidget *gp, guint16 keycode) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaKeyVal *k; + gint i; + + if (!gpdata) + return; + + if (keycode == 0) { + /* Send all release key events for previously pressed keys */ + for (i = 0; i < gpdata->pressed_keys->len; i++) { + k = g_ptr_array_index(gpdata->pressed_keys, i); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_KEY, GUINT_TO_POINTER(k->keyval), + GINT_TO_POINTER(FALSE), NULL); + g_free(k); + } + g_ptr_array_set_size(gpdata->pressed_keys, 0); + } else { + /* Unregister the keycode only */ + for (i = 0; i < gpdata->pressed_keys->len; i++) { + k = g_ptr_array_index(gpdata->pressed_keys, i); + if (k->keycode == keycode) { + g_free(k); + g_ptr_array_remove_index_fast(gpdata->pressed_keys, i); + break; + } + } + } +} + +static gboolean remmina_plugin_vnc_on_key(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + RemminaKeyVal *k; + guint event_keyval; + guint keyval; + + if (!gpdata->connected || !gpdata->client) + return FALSE; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return FALSE; + + gpdata->scroll_x_accumulator = 0; + gpdata->scroll_y_accumulator = 0; + + /* When sending key release, try first to find out a previously sent keyval + * to workaround bugs like https://bugs.freedesktop.org/show_bug.cgi?id=7430 */ + + event_keyval = event->keyval; + if (event->type == GDK_KEY_RELEASE) { + for (int i = 0; i < gpdata->pressed_keys->len; i++) { + k = g_ptr_array_index(gpdata->pressed_keys, i); + if (k->keycode == event->hardware_keycode) { + event_keyval = k->keyval; + break; + } + } + } + + keyval = remmina_plugin_service->pref_keymap_get_keyval(remmina_plugin_service->file_get_string(remminafile, "keymap"), + event_keyval); + + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_KEY, GUINT_TO_POINTER(keyval), + GINT_TO_POINTER(event->type == GDK_KEY_PRESS ? TRUE : FALSE), NULL); + + /* Register/unregister the pressed key */ + if (event->type == GDK_KEY_PRESS) { + k = g_new(RemminaKeyVal, 1); + k->keyval = keyval; + k->keycode = event->hardware_keycode; + g_ptr_array_add(gpdata->pressed_keys, k); + } else { + remmina_plugin_vnc_release_key(gp, event->hardware_keycode); + } + return TRUE; +} + +static void remmina_plugin_vnc_on_cuttext_request(GtkClipboard *clipboard, const gchar *text, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + GDateTime *t; + glong diff; + gsize br, bw; + gchar *latin1_text; + const char *cur_charset; + + if (text) { + /* A timer (1 second) to avoid clipboard "loopback": text cut out from VNC won’t paste back into VNC */ + t = g_date_time_new_now_utc(); + diff = g_date_time_difference(t, gpdata->clipboard_timer) / 100000; // tenth of second + if (diff < 10) + return; + g_date_time_unref(gpdata->clipboard_timer); + gpdata->clipboard_timer = t; + /* Convert text from current charset to latin-1 before sending to remote server. + * See RFC6143 7.5.6 */ + g_get_charset(&cur_charset); + latin1_text = g_convert_with_fallback(text, -1, "ISO-8859-1", cur_charset, "?", &br, &bw, NULL); + remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CUTTEXT, (gpointer)latin1_text, NULL, NULL); + g_free(latin1_text); + } +} + +static void remmina_plugin_vnc_on_cuttext(GtkClipboard *clipboard, GdkEvent *event, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + + if (!gpdata->connected || !gpdata->client) + return; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE)) + return; + + gtk_clipboard_request_text(clipboard, (GtkClipboardTextReceivedFunc)remmina_plugin_vnc_on_cuttext_request, gp); +} + +static void remmina_plugin_vnc_on_realize(RemminaProtocolWidget *gp, gpointer data) +{ + TRACE_CALL(__func__); + RemminaFile *remminafile; + GdkCursor *cursor; + GdkPixbuf *pixbuf; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + if (remmina_plugin_service->file_get_int(remminafile, "showcursor", FALSE)) { + /* Hide local cursor (show a small dot instead) */ + pixbuf = gdk_pixbuf_new_from_xpm_data(dot_cursor_xpm); + cursor = gdk_cursor_new_from_pixbuf(gdk_display_get_default(), pixbuf, dot_cursor_x_hot, dot_cursor_y_hot); + g_object_unref(pixbuf); + gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(gp)), cursor); + g_object_unref(cursor); + } +} + +/******************************************************************************************/ + +static gboolean remmina_plugin_vnc_open_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + gpdata->connected = TRUE; + gchar *server; + gint port; + const gchar* raw_server; + + remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->drawing_area); + + g_signal_connect(G_OBJECT(gp), "realize", G_CALLBACK(remmina_plugin_vnc_on_realize), NULL); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "motion-notify-event", G_CALLBACK(remmina_plugin_vnc_on_motion), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "button-press-event", G_CALLBACK(remmina_plugin_vnc_on_button), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "button-release-event", G_CALLBACK(remmina_plugin_vnc_on_button), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "scroll-event", G_CALLBACK(remmina_plugin_vnc_on_scroll), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "key-press-event", G_CALLBACK(remmina_plugin_vnc_on_key), gp); + g_signal_connect(G_OBJECT(gpdata->drawing_area), "key-release-event", G_CALLBACK(remmina_plugin_vnc_on_key), gp); + + if (!remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE)) + gpdata->clipboard_handler = g_signal_connect(G_OBJECT(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)), + "owner-change", G_CALLBACK(remmina_plugin_vnc_on_cuttext), gp); + + + if (pthread_create(&gpdata->thread, NULL, remmina_plugin_vnc_main_thread, gp)) { + /* I don’t think this will ever happen… */ + g_print("Could not initialize pthread. Falling back to non-thread mode…\n"); + g_timeout_add(0, (GSourceFunc)remmina_plugin_vnc_main, gp); + gpdata->thread = 0; + } + + raw_server = remmina_plugin_service->file_get_string(remminafile, "server"); + + if (raw_server && strstr(raw_server, "unix://") == raw_server) { + REMMINA_PLUGIN_AUDIT(_("Connected to %s via VNC"), raw_server); + } else { + remmina_plugin_service->get_server_port(raw_server, + VNC_DEFAULT_PORT, + &server, + &port); + + REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via VNC"), server, port); + g_free(server), server = NULL; + } +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) + remmina_plugin_service->protocol_plugin_unlock_dynres(gp); +#endif + return TRUE; +} + +static gboolean remmina_plugin_vnc_close_connection_timeout(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + gchar *server; + gint port; + + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + remmina_plugin_service->get_server_port(remmina_plugin_service->file_get_string(remminafile, "server"), + VNC_DEFAULT_PORT, + &server, + &port); + + REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via VNC"), server, port); + g_free(server), server = NULL; + + /* wait until the running attribute is set to false by the VNC thread */ + if (gpdata->running) + return TRUE; + + /* unregister the clipboard monitor */ + if (gpdata->clipboard_handler) { + g_signal_handler_disconnect(G_OBJECT(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)), gpdata->clipboard_handler); + gpdata->clipboard_handler = 0; + } + + if (gpdata->queuecursor_handler) { + g_source_remove(gpdata->queuecursor_handler); + gpdata->queuecursor_handler = 0; + } + if (gpdata->queuecursor_surface) { + cairo_surface_destroy(gpdata->queuecursor_surface); + gpdata->queuecursor_surface = NULL; + } + + if (gpdata->queuedraw_handler) { + g_source_remove(gpdata->queuedraw_handler); + gpdata->queuedraw_handler = 0; + } + if (gpdata->listen_sock >= 0) + close(gpdata->listen_sock); + if (gpdata->client) { + rfbClientCleanup((rfbClient *)gpdata->client); + gpdata->client = NULL; + } + if (gpdata->rgb_buffer) { + cairo_surface_destroy(gpdata->rgb_buffer); + gpdata->rgb_buffer = NULL; + } + if (gpdata->vnc_buffer) { + g_free(gpdata->vnc_buffer); + gpdata->vnc_buffer = NULL; + } + g_ptr_array_free(gpdata->pressed_keys, TRUE); + g_date_time_unref(gpdata->clipboard_timer); + remmina_plugin_vnc_event_free_all(gp); + g_queue_free(gpdata->vnc_event_queue); + pthread_mutex_destroy(&gpdata->vnc_event_queue_mutex); + close(gpdata->vnc_event_pipe[0]); + close(gpdata->vnc_event_pipe[1]); + + + pthread_mutex_destroy(&gpdata->buffer_mutex); + remmina_plugin_service->protocol_plugin_signal_connection_closed(gp); + + return FALSE; +} + +static gboolean remmina_plugin_vnc_close_connection(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + gpdata->connected = FALSE; + + if (gpdata->thread) { + pthread_cancel(gpdata->thread); + if (gpdata->thread) pthread_join(gpdata->thread, NULL); + gpdata->running = FALSE; + remmina_plugin_vnc_close_connection_timeout(gp); + } else { + g_timeout_add(200, (GSourceFunc)remmina_plugin_vnc_close_connection_timeout, gp); + } + + return FALSE; +} + +static gboolean remmina_plugin_vnc_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + switch (feature->id) { + case REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT: + return SupportsClient2Server((rfbClient *)(gpdata->client), rfbSetServerInput) ? TRUE : FALSE; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT: + return SupportsClient2Server((rfbClient *)(gpdata->client), rfbTextChat) ? TRUE : FALSE; + default: + return TRUE; + } +} + +static void remmina_plugin_vnc_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + RemminaFile *remminafile; + rfbClient* client; + uint8_t previous_bpp; + remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + switch (feature->id) { + case REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY: + remmina_plugin_vnc_update_quality((rfbClient *)(gpdata->client), + remmina_plugin_service->file_get_int(remminafile, "quality", 9)); + remmina_plugin_vnc_update_colordepth((rfbClient *)(gpdata->client), + remmina_plugin_service->file_get_int(remminafile, "colordepth", 32)); + SetFormatAndEncodings((rfbClient *)(gpdata->client)); + break; + case REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR: + client = (rfbClient *)(gpdata->client); + previous_bpp = client->format.bitsPerPixel; + remmina_plugin_vnc_update_colordepth(client, + remmina_plugin_service->file_get_int(remminafile, "colordepth", 32)); + SetFormatAndEncodings(client); + //Need to clear away old and reallocate if we're increasing bpp + if (client->format.bitsPerPixel > previous_bpp){ + remmina_plugin_vnc_rfb_allocfb((rfbClient *)(gpdata->client)); + SendFramebufferUpdateRequest((rfbClient *)(gpdata->client), 0, 0, + remmina_plugin_service->protocol_plugin_get_width(gp), + remmina_plugin_service->protocol_plugin_get_height(gp), FALSE); + } + break; + case REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY: + break; + case REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT: + PermitServerInput((rfbClient *)(gpdata->client), + remmina_plugin_service->file_get_int(remminafile, "disableserverinput", FALSE) ? 1 : 0); + break; + case REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS: + remmina_plugin_vnc_release_key(gp, 0); + break; + case REMMINA_PLUGIN_VNC_FEATURE_SCALE: + remmina_plugin_vnc_update_scale(gp, remmina_plugin_service->file_get_int(remminafile, "scale", FALSE)); + break; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH: + SendFramebufferUpdateRequest((rfbClient *)(gpdata->client), 0, 0, + remmina_plugin_service->protocol_plugin_get_width(gp), + remmina_plugin_service->protocol_plugin_get_height(gp), FALSE); + break; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT: + remmina_plugin_vnc_open_chat(gp); + break; + case REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL: + remmina_plugin_vnc_send_ctrlaltdel(gp); + break; + default: + break; + } +} + +/* Send a keystroke to the plugin window */ +static void remmina_plugin_vnc_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->drawing_area, + keystrokes, keylen, GDK_KEY_PRESS | GDK_KEY_RELEASE); + return; +} + +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) +static gboolean remmina_plugin_vnc_on_size_allocate(GtkWidget *widget, GtkAllocation *alloc, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaScaleMode scale_mode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + + if (scale_mode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES){ + char str[1024]; + sprintf(str, "DEBUG: %d x %d", alloc->width, alloc->height); + TRACE_CALL(str); + if (gpdata->client){ + SendExtDesktopSize(gpdata->client, alloc->width, alloc->height); + } + } + return TRUE; +} +#endif + +static gboolean remmina_plugin_vnc_on_draw(GtkWidget *widget, cairo_t *context, RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp); + cairo_surface_t *surface; + gint width, height; + GtkAllocation widget_allocation; + + LOCK_BUFFER(FALSE); + + surface = gpdata->rgb_buffer; + if (!surface) { + UNLOCK_BUFFER(FALSE); + return FALSE; + } + + width = remmina_plugin_service->protocol_plugin_get_width(gp); + height = remmina_plugin_service->protocol_plugin_get_height(gp); + + if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) { + gtk_widget_get_allocation(widget, &widget_allocation); + cairo_scale(context, + (double)widget_allocation.width / width, + (double)widget_allocation.height / height); + } + + cairo_rectangle(context, 0, 0, width, height); + cairo_set_source_surface(context, surface, 0, 0); + cairo_fill(context); + + UNLOCK_BUFFER(FALSE); + return TRUE; +} + +static void remmina_plugin_vnc_init(RemminaProtocolWidget *gp) +{ + TRACE_CALL(__func__); + RemminaPluginVncData *gpdata; + gint flags; + gdouble aspect_ratio; + + gpdata = g_new0(RemminaPluginVncData, 1); + g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free); + + gboolean disable_smooth_scrolling = FALSE; + RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp); + + disable_smooth_scrolling = remmina_plugin_service->file_get_int(remminafile, "disablesmoothscrolling", FALSE); + REMMINA_PLUGIN_DEBUG("Disable smooth scrolling is set to %d", disable_smooth_scrolling); + + gpdata->drawing_area = gtk_drawing_area_new(); + gtk_widget_show(gpdata->drawing_area); + + aspect_ratio = remmina_plugin_service->file_get_double(remminafile, "aspect_ratio", 0); + if (aspect_ratio > 0){ + GtkWidget* aspectframe = gtk_aspect_frame_new(NULL, 0, 0, aspect_ratio, FALSE); + + gtk_frame_set_shadow_type(GTK_FRAME(aspectframe), GTK_SHADOW_NONE); + gtk_widget_show(aspectframe); + gtk_container_add(GTK_CONTAINER(aspectframe), gpdata->drawing_area); + gtk_container_add(GTK_CONTAINER(gp), aspectframe); + } + else{ + gtk_container_add(GTK_CONTAINER(gp), gpdata->drawing_area); + } + + gtk_widget_add_events( + gpdata->drawing_area, + GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK + | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK + | GDK_KEY_RELEASE_MASK + | GDK_SCROLL_MASK); + gtk_widget_set_can_focus(gpdata->drawing_area, TRUE); + + if (!disable_smooth_scrolling) { + REMMINA_PLUGIN_DEBUG("Adding GDK_SMOOTH_SCROLL_MASK"); + gtk_widget_add_events(gpdata->drawing_area, GDK_SMOOTH_SCROLL_MASK); + } + + + g_signal_connect(G_OBJECT(gpdata->drawing_area), "draw", G_CALLBACK(remmina_plugin_vnc_on_draw), gp); +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) + g_signal_connect(G_OBJECT(gpdata->drawing_area), "size-allocate", G_CALLBACK(remmina_plugin_vnc_on_size_allocate), gp); +#endif + gpdata->auth_first = TRUE; + gpdata->clipboard_timer = g_date_time_new_now_utc(); + gpdata->listen_sock = -1; + gpdata->pressed_keys = g_ptr_array_new(); + gpdata->vnc_event_queue = g_queue_new(); + pthread_mutex_init(&gpdata->vnc_event_queue_mutex, NULL); + if (pipe(gpdata->vnc_event_pipe)) { + g_print("Error creating pipes.\n"); + gpdata->vnc_event_pipe[0] = 0; + gpdata->vnc_event_pipe[1] = 0; + } + flags = fcntl(gpdata->vnc_event_pipe[0], F_GETFL, 0); + fcntl(gpdata->vnc_event_pipe[0], F_SETFL, flags | O_NONBLOCK); + + pthread_mutex_init(&gpdata->buffer_mutex, NULL); +} + +/* Array of key/value pairs for color depths */ +static gpointer colordepth_list[] = +{ + "32", N_("True colour (32 bpp)"), + "16", N_("High colour (16 bpp)"), + "8", N_("256 colours (8 bpp)"), + NULL +}; + +/* Array of key/value pairs for quality selection */ +static gpointer quality_list[] = +{ + "2", N_("Good"), + "9", N_("Best (slowest)"), + "1", N_("Medium"), + "0", N_("Poor (fastest)"), + NULL +}; + +static gchar repeater_tooltip[] = + N_("Connect to VNC using a repeater:\n" + " • The server field must contain the repeater ID, e.g. ID:123456789\n" + " • The repeater field have to be set to the repeater IP and port, like:\n" + " 10.10.10.12:5901\n" + " • From the remote VNC server, you will connect to\n" + " the repeater, e.g. with x11vnc:\n" + " x11vnc -connect repeater=ID:123456789+10.10.10.12:5500"); + +static gchar vnciport_tooltip[] = + N_("Listening for remote VNC connection:\n" + " • The “Listen on port” field is the port Remmina will listen to,\n" + " e.g. 8888\n" + " • From the remote VNC server, you will connect to\n" + " Remmina, e.g. with x11vnc:\n" + " x11vnc -display :0 -connect 192.168.1.36:8888"); + +static gchar aspect_ratio_tooltip[] = + N_("Lock the aspect ratio when dynamic resolution is enabled:\n" + "\n" + " • The aspect ratio should be entered as a decimal number, e.g. 1.777\n" + " • 16:9 corresponds roughly to 1.7777, 4:3 corresponds roughly to 1.333\n" + " • The default value of 0 does not enforce any aspect ratio"); + +static gchar vncencodings_tooltip[] = + N_("Overriding the pre-set VNC encoding quality:\n" + "\n" + " • “Poor (fastest)” sets encoding to “copyrect zlib hextile raw”\n" + " • “Medium” sets encoding to “tight zrle ultra copyrect hextile zlib corre rre raw”\n" + " • “Good” sets encoding to “tight zrle ultra copyrect hextile zlib corre rre raw”\n" + " • “Best (slowest)” sets encoding to “copyrect zrle ultra zlib hextile corre rre raw”"); + +/* Array of RemminaProtocolSetting for basic settings. + * Each item is composed by: + * a) RemminaProtocolSettingType for setting type + * b) Setting name + * c) Setting description + * d) Compact disposition + * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO + * f) Setting tooltip + * g) Validation data pointer, will be passed to the validation callback method. + * h) Validation callback method (Can be NULL. Every entry will be valid then.) + * use following prototype: + * gboolean mysetting_validator_method(gpointer key, gpointer value, + * gpointer validator_data); + * gpointer key is a gchar* containing the setting's name, + * gpointer value contains the value which should be validated, + * gpointer validator_data contains your passed data. + */ +static const RemminaProtocolSetting remmina_plugin_vnc_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, "_rfb._tcp", NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "proxy", N_("Repeater"), FALSE, NULL, repeater_tooltip, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, FALSE, NULL, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL } +}; + +// Same as above. +static const RemminaProtocolSetting remmina_plugin_vnci_basic_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "listenport", N_("Listen on port"), FALSE, NULL, vnciport_tooltip, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, FALSE, NULL, NULL, NULL, NULL}, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL} +}; + +/* Array of RemminaProtocolSetting for advanced settings. + * Each item is composed by: + * a) RemminaProtocolSettingType for setting type + * b) Setting name + * c) Setting description + * d) Compact disposition + * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO + * f) Setting Tooltip + */ +static const RemminaProtocolSetting remmina_plugin_vnc_advanced_settings[] = +{ + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "encodings", N_("Override pre-set VNC encodings"), FALSE, NULL, vncencodings_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "aspect_ratio", N_("Dynamic resolution enforced aspec ratio"), FALSE, NULL, aspect_ratio_tooltip }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "tightencoding", N_("Force tight encoding"), TRUE, NULL, N_("Enabling this may help when the remote desktop looks scrambled") }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablesmoothscrolling", N_("Disable smooth scrolling"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverbell", N_("Ignore remote bell messages"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverinput", N_("Prevent local interaction on the server"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "showcursor", N_("Show remote cursor"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("Turn off clipboard sync"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableencryption", N_("Turn off encryption"), FALSE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL }, + { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL } +}; + +/* Array for available features. + * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */ +static const RemminaProtocolFeature remmina_plugin_vnc_features[] = +{ + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_RADIO), "quality", + quality_list }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_RADIO), "colordepth", + colordepth_list }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", + N_("View only") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableserverinput",N_("Prevent local interaction on the server") }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH, N_("Refresh"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT, N_("Open Chat…"), "face-smile", NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_PLUGIN_VNC_FEATURE_SCALE, NULL, NULL, NULL }, + { REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS, NULL, NULL, NULL }, +#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14) + { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_PLUGIN_VNC_FEATURE_DYNRESUPDATE, NULL, NULL, NULL }, +#endif + { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL } +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin_vnc = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + VNC_PLUGIN_NAME, // Name + VNC_PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + VNC_PLUGIN_VERSION, // Version number + VNC_PLUGIN_APPICON, // Icon for normal connection + VNC_PLUGIN_SSH_APPICON, // Icon for SSH connection + remmina_plugin_vnc_basic_settings, // Array for basic settings + remmina_plugin_vnc_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type + remmina_plugin_vnc_features, // Array for available features + remmina_plugin_vnc_init, // Plugin initialization + remmina_plugin_vnc_open_connection, // Plugin open connection + remmina_plugin_vnc_close_connection, // Plugin close connection + remmina_plugin_vnc_query_feature, // Query for available features + remmina_plugin_vnc_call_feature, // Call a feature + remmina_plugin_vnc_keystroke // Send a keystroke +}; + +/* Protocol plugin definition and features */ +static RemminaProtocolPlugin remmina_plugin_vnci = +{ + REMMINA_PLUGIN_TYPE_PROTOCOL, // Type + VNCI_PLUGIN_NAME, // Name + VNCI_PLUGIN_DESCRIPTION, // Description + GETTEXT_PACKAGE, // Translation domain + VERSION, // Version number + VNCI_PLUGIN_APPICON, // Icon for normal connection + VNCI_PLUGIN_SSH_APPICON, // Icon for SSH connection + remmina_plugin_vnci_basic_settings, // Array for basic settings + remmina_plugin_vnc_advanced_settings, // Array for advanced settings + REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL, // SSH settings type + remmina_plugin_vnc_features, // Array for available features + remmina_plugin_vnc_init, // Plugin initialization + remmina_plugin_vnc_open_connection, // Plugin open connection + remmina_plugin_vnc_close_connection, // Plugin close connection + remmina_plugin_vnc_query_feature, // Query for available features + remmina_plugin_vnc_call_feature, // Call a feature + remmina_plugin_vnc_keystroke, // Send a keystroke + NULL, // No screenshot support available + NULL, // RCW map event + NULL // RCW unmap event +}; + +G_MODULE_EXPORT gboolean +remmina_plugin_entry(RemminaPluginService *service) +{ + TRACE_CALL(__func__); + remmina_plugin_service = service; + + bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + + if (!service->register_plugin((RemminaPlugin *)&remmina_plugin_vnc)) + return FALSE; + + if (!service->register_plugin((RemminaPlugin *)&remmina_plugin_vnci)) + return FALSE; + + return TRUE; +} |