From 297ad403d6e4416c9fc969cce316832947dd8fd4 Mon Sep 17 00:00:00 2001 From: Hideyuki Nagase Date: Tue, 10 May 2022 13:16:55 -0500 Subject: [PATCH] rdp: Add clipboard redirection support Allow clipboard pasting in and out of an RDP session. Co-authored-by: Steve Pronovost Co-authored-by: Brenton DeGeer Signed-off-by: Hideyuki Nagase Signed-off-by: Steve Pronovost Signed-off-by: Brenton DeGeer --- libweston/backend-rdp/meson.build | 1 + libweston/backend-rdp/rdp.c | 44 + libweston/backend-rdp/rdp.h | 30 + libweston/backend-rdp/rdpclip.c | 1744 +++++++++++++++++++++++++++++ libweston/backend-rdp/rdputil.c | 36 + 5 files changed, 1855 insertions(+) create mode 100644 libweston/backend-rdp/rdpclip.c diff --git a/libweston/backend-rdp/meson.build b/libweston/backend-rdp/meson.build index 7da92469..d43eb651 100644 --- a/libweston/backend-rdp/meson.build +++ b/libweston/backend-rdp/meson.build @@ -31,6 +31,7 @@ deps_rdp = [ ] srcs_rdp = [ 'rdp.c', + 'rdpclip.c', 'rdputil.c', ] diff --git a/libweston/backend-rdp/rdp.c b/libweston/backend-rdp/rdp.c index 3f104d25..24e98c98 100644 --- a/libweston/backend-rdp/rdp.c +++ b/libweston/backend-rdp/rdp.c @@ -555,6 +555,16 @@ rdp_destroy(struct weston_compositor *ec) if (b->listener_events[i]) wl_event_source_remove(b->listener_events[i]); + if (b->clipboard_debug) { + weston_log_scope_destroy(b->clipboard_debug); + b->clipboard_debug = NULL; + } + + if (b->clipboard_verbose) { + weston_log_scope_destroy(b->clipboard_verbose); + b->clipboard_verbose = NULL; + } + if (b->debug) { weston_log_scope_destroy(b->debug); b->debug = NULL; @@ -669,6 +679,8 @@ rdp_peer_context_free(freerdp_peer* client, RdpPeerContext* context) wl_event_source_remove(context->events[i]); } + rdp_clipboard_destroy(context); + if (context->vcm) WTSCloseServer(context->vcm); @@ -924,6 +936,13 @@ xf_peer_activate(freerdp_peer* client) settings->CompressionEnabled = FALSE; } + if (settings->RedirectClipboard) { + if (!peerCtx->vcm) { + weston_log("Virtual channel is required for clipboard\n"); + goto error_exit; + } + } + if (output->base.width != (int)settings->DesktopWidth || output->base.height != (int)settings->DesktopHeight) { @@ -995,6 +1014,11 @@ xf_peer_activate(freerdp_peer* client) xkb_keymap_unref(keymap); weston_seat_init_pointer(peersItem->seat); + /* Initialize RDP clipboard after seat is initialized */ + if (settings->RedirectClipboard) + if (rdp_clipboard_init(client) != 0) + goto error_exit; + peersItem->flags |= RDP_PEER_ACTIVATED; /* disable pointer on the client side */ @@ -1014,6 +1038,12 @@ xf_peer_activate(freerdp_peer* client) pixman_region32_fini(&damage); return TRUE; + +error_exit: + + rdp_clipboard_destroy(peerCtx); + + return FALSE; } static BOOL @@ -1450,6 +1480,7 @@ rdp_peer_init(freerdp_peer *client, struct rdp_backend *b) settings->NSCodec = TRUE; settings->FrameMarkerCommandEnabled = TRUE; settings->SurfaceFrameMarkerEnabled = TRUE; + settings->RedirectClipboard = TRUE; settings->HasExtendedMouseEvent = TRUE; settings->HasHorizontalWheel = TRUE; @@ -1569,6 +1600,15 @@ rdp_backend_create(struct weston_compositor *compositor, b->rdp_monitor_refresh_rate = config->refresh_rate * 1000; rdp_debug(b, "RDP backend: WESTON_RDP_MONITOR_REFRESH_RATE: %d\n", b->rdp_monitor_refresh_rate); + b->clipboard_debug = weston_log_ctx_add_log_scope(b->compositor->weston_log_ctx, + "rdp-backend-clipboard", + "Debug messages from RDP backend clipboard\n", + NULL, NULL, NULL); + b->clipboard_verbose = weston_log_ctx_add_log_scope(b->compositor->weston_log_ctx, + "rdp-backend-clipboard-verbose", + "Debug messages from RDP backend clipboard\n", + NULL, NULL, NULL); + compositor->backend = &b->base; if (config->server_cert && config->server_key) { @@ -1660,6 +1700,10 @@ err_compositor: weston_compositor_shutdown(compositor); err_free_strings: + if (b->clipboard_debug) + weston_log_scope_destroy(b->clipboard_debug); + if (b->clipboard_verbose) + weston_log_scope_destroy(b->clipboard_verbose); if (b->debug) weston_log_scope_destroy(b->debug); if (b->verbose) diff --git a/libweston/backend-rdp/rdp.h b/libweston/backend-rdp/rdp.h index ea28919f..e2fc6abf 100644 --- a/libweston/backend-rdp/rdp.h +++ b/libweston/backend-rdp/rdp.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -78,6 +79,9 @@ struct rdp_backend { struct weston_log_scope *debug; struct weston_log_scope *verbose; + struct weston_log_scope *clipboard_debug; + struct weston_log_scope *clipboard_verbose; + char *server_cert; char *server_key; char *rdp_key; @@ -142,6 +146,13 @@ struct rdp_peer_context { pthread_mutex_t loop_task_list_mutex; struct wl_list loop_task_list; /* struct rdp_loop_task::link */ + /* Clipboard support */ + CliprdrServerContext *clipboard_server_context; + + struct rdp_clipboard_data_source *clipboard_client_data_source; + struct rdp_clipboard_data_source *clipboard_inflight_client_data_source; + + struct wl_listener clipboard_selection_listener; }; typedef struct rdp_peer_context RdpPeerContext; @@ -171,10 +182,22 @@ struct rdp_loop_task { #define rdp_debug_continue(b, ...) \ rdp_debug_print(b->debug, true, __VA_ARGS__) +#define rdp_debug_clipboard_verbose(b, ...) \ + rdp_debug_print(b->clipboard_verbose, false, __VA_ARGS__) +#define rdp_debug_clipboard_verbose_continue(b, ...) \ + rdp_debug_print(b->clipboard_verbose, true, __VA_ARGS__) +#define rdp_debug_clipboard(b, ...) \ + rdp_debug_print(b->clipboard_debug, false, __VA_ARGS__) +#define rdp_debug_clipboard_continue(b, ...) \ + rdp_debug_print(b->clipboard_debug, true, __VA_ARGS__) + /* rdputil.c */ void rdp_debug_print(struct weston_log_scope *log_scope, bool cont, char *fmt, ...); +int +rdp_wl_array_read_fd(struct wl_array *array, int fd); + void convert_rdp_keyboard_to_xkb_rule_names(UINT32 KeyboardType, UINT32 KeyboardSubType, UINT32 KeyboardLayout, struct xkb_rule_names *xkbRuleNames); @@ -204,6 +227,13 @@ rdp_initialize_dispatch_task_event_source(RdpPeerContext *peerCtx); void rdp_destroy_dispatch_task_event_source(RdpPeerContext *peerCtx); +/* rdpclip.c */ +int +rdp_clipboard_init(freerdp_peer *client); + +void +rdp_clipboard_destroy(RdpPeerContext *peerCtx); + static inline struct rdp_head * to_rdp_head(struct weston_head *base) { diff --git a/libweston/backend-rdp/rdpclip.c b/libweston/backend-rdp/rdpclip.c new file mode 100644 index 00000000..c8955e8c --- /dev/null +++ b/libweston/backend-rdp/rdpclip.c @@ -0,0 +1,1744 @@ +/* + * Copyright © 2020 Microsoft + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rdp.h" + +#include "libweston-internal.h" + +/* From MSDN, RegisterClipboardFormat API. + Registered clipboard formats are identified by values in the range 0xC000 through 0xFFFF. */ +#define CF_PRIVATE_RTF 49309 /* fake format ID for "Rich Text Format". */ +#define CF_PRIVATE_HTML 49405 /* fake format ID for "HTML Format".*/ + + /* 1 2 3 4 5 6 7 8 */ + /*01234567890 1 2345678901234 5 67890123456 7 89012345678901234567890 1 234567890123456789012 3 4*/ +static const char rdp_clipboard_html_header[] = "Version:0.9\r\nStartHTML:-1\r\nEndHTML:-1\r\nStartFragment:00000000\r\nEndFragment:00000000\r\n"; +#define RDP_CLIPBOARD_FRAGMENT_START_OFFSET (53) //---------------------------------------------------------+ | +#define RDP_CLIPBOARD_FRAGMENT_END_OFFSET (75) //-----------------------------------------------------------------------------------+ + +/* + * https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format + * + * The fragment should be preceded and followed by the HTML comments and + * (no space allowed between the !-- and the text) to conveniently + * indicate where the fragment starts and ends. + */ +static const char rdp_clipboard_html_fragment_start[] = "\r\n"; +static const char rdp_clipboard_html_fragment_end[] = "\r\n"; + +struct rdp_clipboard_data_source; + +typedef bool (*pfn_process_data)(struct rdp_clipboard_data_source *source, bool is_send); + +struct rdp_clipboard_supported_format { + uint32_t format_id; + char *format_name; + char *mime_type; + pfn_process_data pfn; +}; + +static bool +clipboard_process_text_utf8(struct rdp_clipboard_data_source *source, bool is_send); + +static bool +clipboard_process_text_raw(struct rdp_clipboard_data_source *source, bool is_send); + +static bool +clipboard_process_bmp(struct rdp_clipboard_data_source *source , bool is_send); + +static bool +clipboard_process_html(struct rdp_clipboard_data_source *source, bool is_send); + +/* TODO: need to support to 1:n or m:n format conversion. + * For example, CF_UNICODETEXT to "UTF8_STRING" as well as "text/plain;charset=utf-8". + */ +struct rdp_clipboard_supported_format clipboard_supported_formats[] = { + { CF_UNICODETEXT, NULL, "text/plain;charset=utf-8", clipboard_process_text_utf8 }, + { CF_TEXT, NULL, "STRING", clipboard_process_text_raw }, + { CF_DIB, NULL, "image/bmp", clipboard_process_bmp }, + { CF_PRIVATE_RTF, "Rich Text Format", "text/rtf", clipboard_process_text_raw }, + { CF_PRIVATE_HTML, "HTML Format", "text/html", clipboard_process_html }, +}; +#define RDP_NUM_CLIPBOARD_FORMATS ARRAY_LENGTH(clipboard_supported_formats) + +enum rdp_clipboard_data_source_state { + RDP_CLIPBOARD_SOURCE_ALLOCATED = 0, + RDP_CLIPBOARD_SOURCE_FORMATLIST_READY, /* format list obtained from provider */ + RDP_CLIPBOARD_SOURCE_PUBLISHED, /* availablity of some or no clipboard data notified to consumer */ + RDP_CLIPBOARD_SOURCE_REQUEST_DATA, /* data request sent to provider */ + RDP_CLIPBOARD_SOURCE_RECEIVED_DATA, /* data was received from provider, waiting data to be dispatched to consumer */ + RDP_CLIPBOARD_SOURCE_TRANSFERING, /* transfering data to consumer */ + RDP_CLIPBOARD_SOURCE_TRANSFERRED, /* completed transfering data to consumer */ + RDP_CLIPBOARD_SOURCE_CANCEL_PENDING, /* data transfer cancel requested */ + RDP_CLIPBOARD_SOURCE_CANCELED, /* data transfer canceled */ + RDP_CLIPBOARD_SOURCE_RETRY, /* retry later */ + RDP_CLIPBOARD_SOURCE_FAILED, /* failure occured */ +}; + +struct rdp_clipboard_data_source { + struct weston_data_source base; + struct rdp_loop_task task_base; + struct wl_event_source *transfer_event_source; /* used for read/write with pipe */ + struct wl_array data_contents; + void *context; + int refcount; + int data_source_fd; + int format_index; + enum rdp_clipboard_data_source_state state; + uint32_t data_response_fail_count; + uint32_t inflight_write_count; + void *inflight_data_to_write; + size_t inflight_data_size; + bool is_data_processed; + void *processed_data_start; + uint32_t processed_data_size; + bool processed_data_is_send; + bool is_canceled; + uint32_t client_format_id_table[RDP_NUM_CLIPBOARD_FORMATS]; +}; + +struct rdp_clipboard_data_request { + struct rdp_loop_task task_base; + RdpPeerContext *ctx; + uint32_t requested_format_index; +}; + +static char * +clipboard_data_source_state_to_string(struct rdp_clipboard_data_source *source) +{ + if (!source) + return "null"; + + switch (source->state) { + case RDP_CLIPBOARD_SOURCE_ALLOCATED: + return "allocated"; + case RDP_CLIPBOARD_SOURCE_FORMATLIST_READY: + return "format list ready"; + case RDP_CLIPBOARD_SOURCE_PUBLISHED: + return "published"; + case RDP_CLIPBOARD_SOURCE_REQUEST_DATA: + return "request data"; + case RDP_CLIPBOARD_SOURCE_RECEIVED_DATA: + return "received data"; + case RDP_CLIPBOARD_SOURCE_TRANSFERING: + return "transferring"; + case RDP_CLIPBOARD_SOURCE_TRANSFERRED: + return "transferred"; + case RDP_CLIPBOARD_SOURCE_CANCEL_PENDING: + return "cancel pending"; + case RDP_CLIPBOARD_SOURCE_CANCELED: + return "canceled"; + case RDP_CLIPBOARD_SOURCE_RETRY: + return "retry"; + case RDP_CLIPBOARD_SOURCE_FAILED: + return "failed"; + } + assert(false); + return "unknown"; +} + +static bool +clipboard_process_text_utf8(struct rdp_clipboard_data_source *source, bool is_send) +{ + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct wl_array data_contents; + + wl_array_init(&data_contents); + + assert(!source->is_data_processed); + + if (is_send) { + char *data = source->data_contents.data; + size_t data_size, data_size_in_char; + + /* Linux to Windows (convert utf-8 to UNICODE) */ + /* Include terminating NULL in size */ + assert((source->data_contents.size + 1) <= source->data_contents.alloc); + data[source->data_contents.size] = '\0'; + source->data_contents.size++; + + /* obtain size in UNICODE */ + data_size = MultiByteToWideChar(CP_UTF8, 0, + data, + source->data_contents.size, + NULL, 0); + if (data_size < 1) + goto error_return; + + data_size *= 2; // convert to size in bytes. + if (!wl_array_add(&data_contents, data_size)) + goto error_return; + + /* convert to UNICODE */ + data_size_in_char = MultiByteToWideChar(CP_UTF8, 0, + data, + source->data_contents.size, + data_contents.data, + data_size); + assert(data_contents.size == (data_size_in_char * 2)); + } else { + /* Windows to Linux (UNICODE to utf-8) */ + size_t data_size; + LPWSTR data = source->data_contents.data; + size_t data_size_in_char = source->data_contents.size / 2; + + /* Windows's data has trailing chars, which Linux doesn't expect. */ + while (data_size_in_char && + ((data[data_size_in_char-1] == L'\0') || (data[data_size_in_char-1] == L'\n'))) + data_size_in_char -= 1; + if (!data_size_in_char) + goto error_return; + + /* obtain size in utf-8 */ + data_size = WideCharToMultiByte(CP_UTF8, 0, + source->data_contents.data, + data_size_in_char, + NULL, 0, + NULL, NULL); + if (data_size < 1) + goto error_return; + + if (!wl_array_add(&data_contents, data_size)) + goto error_return; + + /* convert to utf-8 */ + data_size = WideCharToMultiByte(CP_UTF8, 0, + source->data_contents.data, + data_size_in_char, + data_contents.data, + data_size, + NULL, NULL); + assert(data_contents.size == data_size); + } + + /* swap the data_contents with new one */ + wl_array_release(&source->data_contents); + source->data_contents = data_contents; + source->is_data_processed = true; + source->processed_data_start = source->data_contents.data; + source->processed_data_size = source->data_contents.size; + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s): %s (%u bytes)\n", + __func__, source, clipboard_data_source_state_to_string(source), + is_send ? "send" : "receive", (uint32_t)source->data_contents.size); + + return true; + +error_return: + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s FAILED (%p:%s): %s (%u bytes)\n", + __func__, source, clipboard_data_source_state_to_string(source), + is_send ? "send" : "receive", (uint32_t)source->data_contents.size); + + wl_array_release(&data_contents); + + return false; +} + +static bool +clipboard_process_text_raw(struct rdp_clipboard_data_source *source, bool is_send) +{ + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + char *data = source->data_contents.data; + size_t data_size = source->data_contents.size; + + assert(!source->is_data_processed); + + if (is_send) { + /* Linux to Windows */ + /* Include terminating NULL in size */ + assert(data_size + 1 <= source->data_contents.alloc); + data[data_size] = '\0'; + source->data_contents.size++; + } else { + /* Windows to Linux */ + /* Windows's data has trailing chars, which Linux doesn't expect. */ + while (data_size && ((data[data_size-1] == '\0') || (data[data_size-1] == '\n'))) + data_size -= 1; + source->data_contents.size = data_size; + } + source->is_data_processed = true; + source->processed_data_start = source->data_contents.data; + source->processed_data_size = source->data_contents.size; + rdp_debug_clipboard_verbose(b, "RDP %s (%p): %s (%u bytes)\n", + __func__, source, is_send ? "send" : "receive", (uint32_t)source->data_contents.size); + + return true; +} + +/* based off sample code at https://docs.microsoft.com/en-us/troubleshoot/cpp/add-html-code-clipboard + But this missing a lot of corner cases, it must be rewritten with use of proper HTML parser */ +/* TODO: This doesn't work for converting HTML from Firefox in Wayland mode to Windows in certain cases, + because Firefox sends "...", thus + this needs to property strip meta header and convert to the Windows clipboard style HTML. */ +static bool +clipboard_process_html(struct rdp_clipboard_data_source *source, bool is_send) +{ + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct wl_array data_contents; + char *cur = source->data_contents.data; + + assert(!source->is_data_processed); + + /* We're tresting the contents as a string for now, so null + * terminate it so strstr can't run off the end. However, we + * don't increase data_contents.size because we don't want + * to affect the content. */ + assert(source->data_contents.size + 1 <= source->data_contents.alloc); + ((char *)(source->data_contents.data))[source->data_contents.size] = '\0'; + + wl_array_init(&data_contents); + cur = strstr(cur, "data_contents.size - + (cur - (char *)source->data_contents.data); + + /* Windows's data has trailing chars, which Linux doesn't expect. */ + while (data_size && ((cur[data_size-1] == '\0') || (cur[data_size-1] == '\n'))) + data_size -= 1; + + if (!data_size) + goto error_return; + + if (!wl_array_add(&data_contents, data_size+1)) /* +1 for null */ + goto error_return; + + memcpy(data_contents.data, cur, data_size); + ((char *)(data_contents.data))[data_size] = '\0'; + data_contents.size = data_size; + } else { + /* Linux to Windows */ + char *last, *buf; + uint32_t fragment_start, fragment_end; + + if (!wl_array_add(&data_contents, source->data_contents.size+200)) + goto error_return; + + buf = data_contents.data; + strcpy(buf, rdp_clipboard_html_header); + last = cur; + cur = strstr(cur, "' */ + strncat(buf, last, cur-last); + last = cur; + fragment_start = strlen(buf); + strcat(buf, rdp_clipboard_html_fragment_start); + cur = strstr(cur, "data_contents); + source->data_contents = data_contents; + source->is_data_processed = true; + source->processed_data_start = source->data_contents.data; + source->processed_data_size = source->data_contents.size; + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s): %s (%u bytes)\n", + __func__, source, clipboard_data_source_state_to_string(source), + is_send ? "send" : "receive", (uint32_t)source->data_contents.size); + + return true; + +error_return: + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s FAILED (%p:%s): %s (%u bytes)\n", + __func__, source, clipboard_data_source_state_to_string(source), + is_send ? "send" : "receive", (uint32_t)source->data_contents.size); + + wl_array_release(&data_contents); + + return false; +} + +#define DIB_HEADER_MARKER ((WORD) ('M' << 8) | 'B') +#define DIB_WIDTH_BYTES(bits) ((((bits) + 31) & ~31) >> 3) + +static bool +clipboard_process_bmp(struct rdp_clipboard_data_source *source, bool is_send) +{ + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + BITMAPFILEHEADER *bmfh = NULL; + BITMAPINFOHEADER *bmih = NULL; + uint32_t color_table_size = 0; + struct wl_array data_contents; + + assert(!source->is_data_processed); + + wl_array_init(&data_contents); + + if (is_send) { + /* Linux to Windows (remove BITMAPFILEHEADER) */ + if (source->data_contents.size <= sizeof(*bmfh)) + goto error_return; + + bmfh = source->data_contents.data; + bmih = (BITMAPINFOHEADER *)(bmfh + 1); + + source->is_data_processed = true; + source->processed_data_start = bmih; + source->processed_data_size = source->data_contents.size - sizeof(*bmfh); + } else { + /* Windows to Linux (insert BITMAPFILEHEADER) */ + BITMAPFILEHEADER _bmfh = {}; + + if (source->data_contents.size <= sizeof(*bmih)) + goto error_return; + + bmih = source->data_contents.data; + bmfh = &_bmfh; + if (bmih->biCompression == BI_BITFIELDS) + color_table_size = sizeof(RGBQUAD) * 3; + else + color_table_size = sizeof(RGBQUAD) * bmih->biClrUsed; + + bmfh->bfType = DIB_HEADER_MARKER; + bmfh->bfOffBits = sizeof(*bmfh) + bmih->biSize + color_table_size; + if (bmih->biSizeImage) + bmfh->bfSize = bmfh->bfOffBits + bmih->biSizeImage; + else if (bmih->biCompression == BI_BITFIELDS || bmih->biCompression == BI_RGB) + bmfh->bfSize = bmfh->bfOffBits + + (DIB_WIDTH_BYTES(bmih->biWidth * bmih->biBitCount) * abs(bmih->biHeight)); + else + goto error_return; + + /* source data must have enough size as described in its own bitmap header */ + if (source->data_contents.size < (bmfh->bfSize - sizeof(*bmfh))) + goto error_return; + + if (!wl_array_add(&data_contents, bmfh->bfSize)) + goto error_return; + assert(data_contents.size == bmfh->bfSize); + + /* copy generated BITMAPFILEHEADER */ + memcpy(data_contents.data, bmfh, sizeof(*bmfh)); + /* copy rest of bitmap data from source */ + memcpy((char *)data_contents.data + sizeof(*bmfh), + source->data_contents.data, bmfh->bfSize - sizeof(*bmfh)); + + /* swap the data_contents with new one */ + wl_array_release(&source->data_contents); + source->data_contents = data_contents; + source->is_data_processed = true; + source->processed_data_start = source->data_contents.data; + source->processed_data_size = source->data_contents.size; + } + + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s): %s (%d bytes)\n", + __func__, source, clipboard_data_source_state_to_string(source), + is_send ? "send" : "receive", + (UINT32)source->data_contents.size); + + return true; + +error_return: + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s FAILED (%p:%s): %s (%d bytes)\n", + __func__, source, clipboard_data_source_state_to_string(source), + is_send ? "send" : "receive", (UINT32)source->data_contents.size); + + wl_array_release(&data_contents); + + return false; +} + +static char * +clipboard_format_id_to_string(UINT32 formatId, bool is_server_format_id) +{ + switch (formatId) { + case CF_RAW: + return "CF_RAW"; + case CF_TEXT: + return "CF_TEXT"; + case CF_BITMAP: + return "CF_BITMAP"; + case CF_METAFILEPICT: + return "CF_METAFILEPICT"; + case CF_SYLK: + return "CF_SYLK"; + case CF_DIF: + return "CF_DIF"; + case CF_TIFF: + return "CF_TIFF"; + case CF_OEMTEXT: + return "CF_OEMTEXT"; + case CF_DIB: + return "CF_DIB"; + case CF_PALETTE: + return "CF_PALETTE"; + case CF_PENDATA: + return "CF_PENDATA"; + case CF_RIFF: + return "CF_RIFF"; + case CF_WAVE: + return "CF_WAVE"; + case CF_UNICODETEXT: + return "CF_UNICODETEXT"; + case CF_ENHMETAFILE: + return "CF_ENHMETAFILE"; + case CF_HDROP: + return "CF_HDROP"; + case CF_LOCALE: + return "CF_LOCALE"; + case CF_DIBV5: + return "CF_DIBV5"; + + case CF_OWNERDISPLAY: + return "CF_OWNERDISPLAY"; + case CF_DSPTEXT: + return "CF_DSPTEXT"; + case CF_DSPBITMAP: + return "CF_DSPBITMAP"; + case CF_DSPMETAFILEPICT: + return "CF_DSPMETAFILEPICT"; + case CF_DSPENHMETAFILE: + return "CF_DSPENHMETAFILE"; + } + + if (formatId >= CF_PRIVATEFIRST && formatId <= CF_PRIVATELAST) + return "CF_PRIVATE"; + + if (formatId >= CF_GDIOBJFIRST && formatId <= CF_GDIOBJLAST) + return "CF_GDIOBJ"; + + if (is_server_format_id) { + if (formatId == CF_PRIVATE_HTML) + return "CF_PRIVATE_HTML"; + + if (formatId == CF_PRIVATE_RTF) + return "CF_PRIVATE_RTF"; + } else { + /* From MSDN, RegisterClipboardFormat API. + Registered clipboard formats are identified by values in the range 0xC000 through 0xFFFF. */ + if (formatId >= 0xC000 && formatId <= 0xFFFF) + return "Client side Registered Clipboard Format"; + } + + return "Unknown format"; +} + +/* find supported index in supported format table by format id from client */ +static int +clipboard_find_supported_format_by_format_id(UINT32 format_id) +{ + unsigned int i; + + for (i = 0; i < RDP_NUM_CLIPBOARD_FORMATS; i++) { + struct rdp_clipboard_supported_format *format = &clipboard_supported_formats[i]; + + if (format_id == format->format_id) + return i; + } + return -1; +} + +/* find supported index in supported format table by format id and name from client */ +static int +clipboard_find_supported_format_by_format_id_and_name(UINT32 format_id, const char *format_name) +{ + unsigned int i; + + for (i = 0; i < RDP_NUM_CLIPBOARD_FORMATS; i++) { + struct rdp_clipboard_supported_format *format = &clipboard_supported_formats[i]; + + /* when our supported format table has format name, only format name must match, + format id provided from client is ignored (but it may be saved by caller for future use. + When our supported format table doesn't have format name, only format id must match, + format name (if provided from client) is ignored */ + if ((format->format_name == NULL && format_id == format->format_id) || + (format->format_name && format_name && strcmp(format_name, format->format_name) == 0)) + return i; + } + return -1; +} + +/* find supported index in supported format table by mime */ +static int +clipboard_find_supported_format_by_mime_type(const char *mime_type) +{ + unsigned int i; + + for (i = 0; i < RDP_NUM_CLIPBOARD_FORMATS; i++) { + struct rdp_clipboard_supported_format *format = &clipboard_supported_formats[i]; + + if (strcmp(mime_type, format->mime_type) == 0) + return i; + } + return -1; +} + +static bool +clipboard_process_source(struct rdp_clipboard_data_source *source, bool is_send) +{ + if (source->is_data_processed) { + assert(source->processed_data_is_send == is_send); + return true; + } + + source->processed_data_start = NULL; + source->processed_data_size = 0; + + if (clipboard_supported_formats[source->format_index].pfn) + return clipboard_supported_formats[source->format_index].pfn(source, is_send); + + /* No processor, so just set up pointer and length for raw data */ + source->is_data_processed = true; + source->processed_data_start = source->data_contents.data; + source->processed_data_size = source->data_contents.size; + source->processed_data_is_send = is_send; + return true; +} + +static void +clipboard_data_source_unref(struct rdp_clipboard_data_source *source) +{ + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + char **p; + + ASSERT_COMPOSITOR_THREAD(b); + + assert(source->refcount); + source->refcount--; + + rdp_debug_clipboard(b, "RDP %s (%p:%s): refcount:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->refcount); + + if (source->refcount > 0) + return; + + if (source->transfer_event_source) + wl_event_source_remove(source->transfer_event_source); + + if (source->data_source_fd != -1) + close(source->data_source_fd); + + if (!wl_list_empty(&source->base.destroy_signal.listener_list)) + wl_signal_emit(&source->base.destroy_signal, + &source->base); + + wl_array_release(&source->data_contents); + + wl_array_for_each(p, &source->base.mime_types) + free(*p); + + wl_array_release(&source->base.mime_types); + + free(source); +} + +/******************************************\ + * FreeRDP format data response functions * +\******************************************/ + +/* Inform client data request is succeeded with data */ +static void +clipboard_client_send_format_data_response(RdpPeerContext *ctx, struct rdp_clipboard_data_source *source) +{ + struct rdp_backend *b = ctx->rdpBackend; + CLIPRDR_FORMAT_DATA_RESPONSE formatDataResponse = {}; + + assert(source->is_data_processed); + rdp_debug_clipboard(b, "Client: %s (%p:%s) format_index:%d %s (%d bytes)\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->format_index, + clipboard_supported_formats[source->format_index].mime_type, + source->processed_data_size); + + formatDataResponse.msgType = CB_FORMAT_DATA_RESPONSE; + formatDataResponse.msgFlags = CB_RESPONSE_OK; + formatDataResponse.dataLen = source->processed_data_size; + formatDataResponse.requestedFormatData = source->processed_data_start; + ctx->clipboard_server_context->ServerFormatDataResponse(ctx->clipboard_server_context, &formatDataResponse); + /* if here failed to send response, what can we do ? */ +} + +/* Inform client data request has failed */ +static void +clipboard_client_send_format_data_response_fail(RdpPeerContext *ctx, struct rdp_clipboard_data_source *source) +{ + struct rdp_backend *b = ctx->rdpBackend; + CLIPRDR_FORMAT_DATA_RESPONSE formatDataResponse = {}; + + rdp_debug_clipboard(b, "Client: %s (%p:%s)\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + + if (source) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + source->data_response_fail_count++; + } + + formatDataResponse.msgType = CB_FORMAT_DATA_RESPONSE; + formatDataResponse.msgFlags = CB_RESPONSE_FAIL; + formatDataResponse.dataLen = 0; + formatDataResponse.requestedFormatData = NULL; + ctx->clipboard_server_context->ServerFormatDataResponse(ctx->clipboard_server_context, &formatDataResponse); + /* if here failed to send response, what can we do ? */ +} + +/***************************************\ + * Compositor file descritor callbacks * +\***************************************/ + +/* Send server clipboard data to client when server side application sent them via pipe. */ +static int +clipboard_data_source_read(int fd, uint32_t mask, void *arg) +{ + struct rdp_clipboard_data_source *source = (struct rdp_clipboard_data_source *)arg; + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + int len; + bool failed = true; + + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) fd:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), fd); + + ASSERT_COMPOSITOR_THREAD(b); + + assert(source->data_source_fd == fd); + assert(source->refcount == 1); + + /* event source is not removed here, but it will be removed when read is completed, + until it's completed this function will be called whenever next chunk of data is + available for read in pipe. */ + assert(source->transfer_event_source); + + source->state = RDP_CLIPBOARD_SOURCE_TRANSFERING; + + len = rdp_wl_array_read_fd(&source->data_contents, fd); + if (len < 0) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s (%p:%s) read failed (%s)\n", + __func__, source, + clipboard_data_source_state_to_string(source), + strerror(errno)); + goto error_exit; + } + + if (len > 0) { + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) read (%zu bytes)\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->data_contents.size); + /* continue to read next batch */ + return 0; + } + + /* len == 0, all data from source is read, so completed. */ + source->state = RDP_CLIPBOARD_SOURCE_TRANSFERRED; + rdp_debug_clipboard(b, "RDP %s (%p:%s): read completed (%ld bytes)\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->data_contents.size); + if (!source->data_contents.size) + goto error_exit; + /* process data before sending to client */ + if (!clipboard_process_source(source, true)) + goto error_exit; + + clipboard_client_send_format_data_response(ctx, source); + failed = false; + +error_exit: + if (failed) + clipboard_client_send_format_data_response_fail(ctx, source); + + /* make sure this is the last reference, so event source is removed at unref */ + assert(source->refcount == 1); + clipboard_data_source_unref(source); + return 0; +} + +/* client's reply with error for data request, clean up */ +static int +clipboard_data_source_fail(int fd, uint32_t mask, void *arg) +{ + struct rdp_clipboard_data_source *source = (struct rdp_clipboard_data_source *)arg; + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) fd:%d\n", __func__, + source, clipboard_data_source_state_to_string(source), fd); + + ASSERT_COMPOSITOR_THREAD(b); + + assert(source->data_source_fd == fd); + /* this data source must be tracked as inflight */ + assert(source == ctx->clipboard_inflight_client_data_source); + + wl_event_source_remove(source->transfer_event_source); + source->transfer_event_source = NULL; + + /* if data was received, but failed for another reason then keep data + * and format index for future request, otherwise data is purged at + * last reference release. */ + if (!source->data_contents.size) { + /* data has been never received, thus must be empty. */ + assert(source->data_contents.size == 0); + assert(source->data_contents.alloc == 0); + assert(source->data_contents.data == NULL); + /* clear previous requested format so it can be requested later again. */ + source->format_index = -1; + } + + /* data has never been sent to write(), thus must be no inflight write. */ + assert(source->inflight_write_count == 0); + assert(source->inflight_data_to_write == NULL); + assert(source->inflight_data_size == 0); + /* data never has been sent to write(), so must not be processed. */ + assert(source->is_data_processed == FALSE); + /* close fd to server clipboard stop pulling data. */ + close(source->data_source_fd); + source->data_source_fd = -1; + /* clear inflight data source from client to server. */ + ctx->clipboard_inflight_client_data_source = NULL; + clipboard_data_source_unref(source); + + return 0; +} + +/* Send client's clipboard data to the requesting application at server side */ +static int +clipboard_data_source_write(int fd, uint32_t mask, void *arg) +{ + struct rdp_clipboard_data_source *source = (struct rdp_clipboard_data_source *)arg; + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + void *data_to_write; + size_t data_size; + ssize_t size; + + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) fd:%d\n", __func__, + source, + clipboard_data_source_state_to_string(source), + fd); + + ASSERT_COMPOSITOR_THREAD(b); + + assert(source->data_source_fd == fd); + /* this data source must be tracked as inflight */ + assert(source == ctx->clipboard_inflight_client_data_source); + + if (source->is_canceled) { + /* if source is being canceled, this must be the last reference */ + assert(source->refcount == 1); + source->state = RDP_CLIPBOARD_SOURCE_CANCELED; + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) canceled\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + goto fail; + } + + if (!source->data_contents.data || !source->data_contents.size) { + assert(source->refcount > 1); + weston_log("RDP %s (%p:%s) no data received from client\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + goto fail; + } + + assert(source->refcount > 1); + if (source->inflight_data_to_write) { + assert(source->inflight_data_size); + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) transfer in chunck, count:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->inflight_write_count); + data_to_write = source->inflight_data_to_write; + data_size = source->inflight_data_size; + } else { + fcntl(source->data_source_fd, F_SETFL, O_WRONLY | O_NONBLOCK); + clipboard_process_source(source, false); + data_to_write = source->processed_data_start; + data_size = source->processed_data_size; + } + while (data_to_write && data_size) { + source->state = RDP_CLIPBOARD_SOURCE_TRANSFERING; + do { + size = write(source->data_source_fd, data_to_write, data_size); + } while (size == -1 && errno == EINTR); + + if (size <= 0) { + if (errno != EAGAIN) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s (%p:%s) write failed %s\n", + __func__, source, + clipboard_data_source_state_to_string(source), strerror(errno)); + break; + } + /* buffer is full, wait until data_source_fd is writable again */ + source->inflight_data_to_write = data_to_write; + source->inflight_data_size = data_size; + source->inflight_write_count++; + return 0; + } else { + assert(data_size >= (size_t)size); + data_size -= size; + data_to_write = (char *)data_to_write + size; + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) wrote %ld bytes, remaining %ld bytes\n", + __func__, source, + clipboard_data_source_state_to_string(source), + size, data_size); + if (!data_size) { + source->state = RDP_CLIPBOARD_SOURCE_TRANSFERRED; + rdp_debug_clipboard(b, "RDP %s (%p:%s) write completed (%ld bytes)\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->data_contents.size); + } + } + } + +fail: + /* Here write is either completed, canceled or failed, so close the pipe. */ + close(source->data_source_fd); + source->data_source_fd = -1; + /* and remove the event source */ + wl_event_source_remove(source->transfer_event_source); + source->transfer_event_source = NULL; + source->inflight_write_count = 0; + source->inflight_data_to_write = NULL; + source->inflight_data_size = 0; + ctx->clipboard_inflight_client_data_source = NULL; + clipboard_data_source_unref(source); + + return 0; +} + +/***********************************\ + * Clipboard data-device callbacks * +\***********************************/ + +/* data-device informs the given data format is accepted */ +static void +clipboard_data_source_accept(struct weston_data_source *base, + uint32_t time, const char *mime_type) +{ + struct rdp_clipboard_data_source *source = (struct rdp_clipboard_data_source *)base; + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + + rdp_debug_clipboard(b, "RDP %s (%p:%s) mime-type:\"%s\"\n", + __func__, source, + clipboard_data_source_state_to_string(source), + mime_type); +} + +/* data-device informs the application requested the specified format data + * in given data_source (= client's clipboard) */ +static void +clipboard_data_source_send(struct weston_data_source *base, + const char *mime_type, int32_t fd) +{ + struct rdp_clipboard_data_source *source = (struct rdp_clipboard_data_source *)base; + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct weston_seat *seat = ctx->item.seat; + struct wl_event_loop *loop = wl_display_get_event_loop(seat->compositor->wl_display); + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest = {}; + int index; + + rdp_debug_clipboard(b, "RDP %s (%p:%s) fd:%d, mime-type:\"%s\"\n", + __func__, source, + clipboard_data_source_state_to_string(source), + fd, mime_type); + + ASSERT_COMPOSITOR_THREAD(b); + + if (ctx->clipboard_inflight_client_data_source) { + /* Here means server side (Linux application) request clipboard data, + but server hasn't completed with previous request yet. + If this happens, punt to idle loop and reattempt. */ + weston_log("\n\n\nRDP %s new (%p:%s:fd %d) vs prev (%p:%s:fd %d): outstanding RDP data request (client to server)\n\n\n", + __func__, source, clipboard_data_source_state_to_string(source), fd, + ctx->clipboard_inflight_client_data_source, + clipboard_data_source_state_to_string(ctx->clipboard_inflight_client_data_source), + ctx->clipboard_inflight_client_data_source->data_source_fd); + if (source == ctx->clipboard_inflight_client_data_source) { + /* when new source and previous source is same, update fd with new one and retry */ + source->state = RDP_CLIPBOARD_SOURCE_RETRY; + ctx->clipboard_inflight_client_data_source->data_source_fd = fd; + return; + } else { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + goto error_return_close_fd; + } + } + + if (source->base.mime_types.size == 0) { + source->state = RDP_CLIPBOARD_SOURCE_TRANSFERRED; + rdp_debug_clipboard(b, "RDP %s (%p:%s) source has no data\n", + __func__, source, clipboard_data_source_state_to_string(source)); + goto error_return_close_fd; + } + + index = clipboard_find_supported_format_by_mime_type(mime_type); + if (index >= 0 && /* check supported by this RDP bridge */ + source->client_format_id_table[index]) { /* check supported by current data source from client */ + ctx->clipboard_inflight_client_data_source = source; + source->refcount++; // reference while request inflight. + source->data_source_fd = fd; + assert(source->inflight_write_count == 0); + assert(source->inflight_data_to_write == NULL); + assert(source->inflight_data_size == 0); + if (index == source->format_index) { + bool ret; + + /* data is already in data_contents, no need to pull from client */ + assert(source->transfer_event_source == NULL); + source->state = RDP_CLIPBOARD_SOURCE_RECEIVED_DATA; + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) data in cache \"%s\" index:%d formatId:%d %s\n", + __func__, source, + clipboard_data_source_state_to_string(source), + mime_type, index, + source->client_format_id_table[index], + clipboard_format_id_to_string(source->client_format_id_table[index], + false)); + + ret = rdp_event_loop_add_fd(loop, source->data_source_fd, WL_EVENT_WRITABLE, + clipboard_data_source_write, source, + &source->transfer_event_source); + if (!ret) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s (%p:%s) wl_event_loop_add_fd failed\n", + __func__, source, clipboard_data_source_state_to_string(source)); + goto error_return_unref_source; + } + } else { + /* purge cached data */ + wl_array_release(&source->data_contents); + wl_array_init(&source->data_contents); + source->is_data_processed = false; + /* update requesting format property */ + source->format_index = index; + /* request clipboard data from client */ + formatDataRequest.msgType = CB_FORMAT_DATA_REQUEST; + formatDataRequest.dataLen = 4; + formatDataRequest.requestedFormatId = source->client_format_id_table[index]; + source->state = RDP_CLIPBOARD_SOURCE_REQUEST_DATA; + rdp_debug_clipboard(b, "RDP %s (%p:%s) request data \"%s\" index:%d formatId:%d %s\n", + __func__, source, clipboard_data_source_state_to_string(source), mime_type, index, + formatDataRequest.requestedFormatId, + clipboard_format_id_to_string(formatDataRequest.requestedFormatId, false)); + if (ctx->clipboard_server_context->ServerFormatDataRequest(ctx->clipboard_server_context, &formatDataRequest) != 0) + goto error_return_unref_source; + } + } else { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s (%p:%s) specified format \"%s\" index:%d formatId:%d is not supported by client\n", + __func__, source, clipboard_data_source_state_to_string(source), + mime_type, index, source->client_format_id_table[index]); + goto error_return_close_fd; + } + + return; + +error_return_unref_source: + source->data_source_fd = -1; + assert(source->inflight_write_count == 0); + assert(source->inflight_data_to_write == NULL); + assert(source->inflight_data_size == 0); + assert(ctx->clipboard_inflight_client_data_source == source); + ctx->clipboard_inflight_client_data_source = NULL; + clipboard_data_source_unref(source); + +error_return_close_fd: + close(fd); +} + +/* data-device informs the given data source is not longer referenced by compositor */ +static void +clipboard_data_source_cancel(struct weston_data_source *base) +{ + struct rdp_clipboard_data_source *source = (struct rdp_clipboard_data_source *)base; + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + + rdp_debug_clipboard(b, "RDP %s (%p:%s)\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + + ASSERT_COMPOSITOR_THREAD(b); + + if (source == ctx->clipboard_inflight_client_data_source) { + source->is_canceled = true; + source->state = RDP_CLIPBOARD_SOURCE_CANCEL_PENDING; + rdp_debug_clipboard(b, "RDP %s (%p:%s): still inflight - refcount:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->refcount); + assert(source->refcount > 1); + return; + } + /* everything outside of the base has to be cleaned up */ + source->state = RDP_CLIPBOARD_SOURCE_CANCELED; + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) - refcount:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->refcount); + assert(source->refcount == 1); + assert(source->transfer_event_source == NULL); + wl_array_release(&source->data_contents); + wl_array_init(&source->data_contents); + source->is_data_processed = false; + source->format_index = -1; + memset(source->client_format_id_table, 0, sizeof(source->client_format_id_table)); + source->inflight_write_count = 0; + source->inflight_data_to_write = NULL; + source->inflight_data_size = 0; + if (source->data_source_fd != -1) { + close(source->data_source_fd); + source->data_source_fd = -1; + } +} + +/**********************************\ + * Compositor idle loop callbacks * +\**********************************/ + +/* Publish client's available clipboard formats to compositor (make them visible to applications in server) */ +static void +clipboard_data_source_publish(bool freeOnly, void *arg) +{ + struct rdp_clipboard_data_source *source = wl_container_of(arg, source, task_base); + freerdp_peer *client = (freerdp_peer *)source->context; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct rdp_clipboard_data_source *source_prev; + + rdp_debug_clipboard(b, "RDP %s (%p:%s)\n", + __func__, source, clipboard_data_source_state_to_string(source)); + + ASSERT_COMPOSITOR_THREAD(b); + + /* here is going to publish new data, if previous data from client is still referenced, + unref it after selection */ + source_prev = ctx->clipboard_client_data_source; + if (!freeOnly) { + ctx->clipboard_client_data_source = source; + source->transfer_event_source = NULL; + source->base.accept = clipboard_data_source_accept; + source->base.send = clipboard_data_source_send; + source->base.cancel = clipboard_data_source_cancel; + source->state = RDP_CLIPBOARD_SOURCE_PUBLISHED; + weston_seat_set_selection(ctx->item.seat, &source->base, + wl_display_next_serial(b->compositor->wl_display)); + } else { + ctx->clipboard_client_data_source = NULL; + clipboard_data_source_unref(source); + } + + if (source_prev) + clipboard_data_source_unref(source_prev); +} + +/* Request the specified clipboard data from data-device at server side */ +static void +clipboard_data_source_request(bool freeOnly, void *arg) +{ + struct rdp_clipboard_data_request *request = wl_container_of(arg, request, task_base); + RdpPeerContext *ctx = request->ctx; + struct rdp_backend *b = ctx->rdpBackend; + struct weston_seat *seat = ctx->item.seat; + struct weston_data_source *selection_data_source = seat->selection_data_source; + struct wl_event_loop *loop = wl_display_get_event_loop(seat->compositor->wl_display); + struct rdp_clipboard_data_source *source = NULL; + int p[2] = {}; + const char *requested_mime_type, **mime_type; + int index; + bool found_requested_format; + bool ret; + + ASSERT_COMPOSITOR_THREAD(b); + + if (freeOnly) + goto error_exit_free_request; + + index = request->requested_format_index; + assert(index >= 0 && index < (int)RDP_NUM_CLIPBOARD_FORMATS); + requested_mime_type = clipboard_supported_formats[index].mime_type; + rdp_debug_clipboard(b, "RDP %s (base:%p) requested mime type:\"%s\"\n", + __func__, selection_data_source, requested_mime_type); + + found_requested_format = FALSE; + wl_array_for_each(mime_type, &selection_data_source->mime_types) { + rdp_debug_clipboard(b, "RDP %s (base:%p) available formats: %s\n", + __func__, selection_data_source, *mime_type); + if (strcmp(requested_mime_type, *mime_type) == 0) { + found_requested_format = true; + break; + } + } + if (!found_requested_format) { + rdp_debug_clipboard(b, "RDP %s (base:%p) requested format not found format:\"%s\"\n", + __func__, selection_data_source, requested_mime_type); + goto error_exit_response_fail; + } + + source = zalloc(sizeof *source); + if (!source) + goto error_exit_response_fail; + + /* By now, the server side data availablity is already notified + to client by clipboard_set_selection(). */ + source->state = RDP_CLIPBOARD_SOURCE_PUBLISHED; + rdp_debug_clipboard(b, "RDP %s (%p:%s) for (base:%p)\n", + __func__, source, + clipboard_data_source_state_to_string(source), + selection_data_source); + wl_signal_init(&source->base.destroy_signal); + wl_array_init(&source->base.mime_types); + wl_array_init(&source->data_contents); + source->is_data_processed = false; + source->context = ctx->item.peer; + source->refcount = 1; // decremented when data sent to client. + source->data_source_fd = -1; + source->format_index = index; + + if (pipe2(p, O_CLOEXEC) == -1) + goto error_exit_free_source; + + source->data_source_fd = p[0]; + + rdp_debug_clipboard_verbose(b, "RDP %s (%p:%s) pipe write:%d -> read:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + p[1], p[0]); + + /* Request data from data source */ + source->state = RDP_CLIPBOARD_SOURCE_REQUEST_DATA; + selection_data_source->send(selection_data_source, requested_mime_type, p[1]); + /* p[1] should be closed by data source */ + + ret = rdp_event_loop_add_fd(loop, p[0], WL_EVENT_READABLE, + clipboard_data_source_read, source, + &source->transfer_event_source); + if (!ret) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("RDP %s (%p:%s) wl_event_loop_add_fd failed.\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + goto error_exit_free_source; + } + + free(request); + + return; + +error_exit_free_source: + assert(source->refcount == 1); + clipboard_data_source_unref(source); +error_exit_response_fail: + clipboard_client_send_format_data_response_fail(ctx, NULL); +error_exit_free_request: + free(request); +} + +/*************************************\ + * Compositor notification callbacks * +\*************************************/ + +/* Compositor notify new clipboard data is going to be copied to clipboard, and its supported formats */ +static void +clipboard_set_selection(struct wl_listener *listener, void *data) +{ + RdpPeerContext *ctx = + container_of(listener, RdpPeerContext, clipboard_selection_listener); + struct rdp_backend *b = ctx->rdpBackend; + struct weston_seat *seat = data; + struct weston_data_source *selection_data_source = seat->selection_data_source; + struct rdp_clipboard_data_source *data_source; + CLIPRDR_FORMAT_LIST formatList = {}; + CLIPRDR_FORMAT format[RDP_NUM_CLIPBOARD_FORMATS] = {}; + const char **mime_type; + int index, num_supported_format = 0, num_avail_format = 0; + + rdp_debug_clipboard(b, "RDP %s (base:%p)\n", __func__, selection_data_source); + + ASSERT_COMPOSITOR_THREAD(b); + + if (selection_data_source == NULL) { + return; + } + + if (selection_data_source->accept == clipboard_data_source_accept) { + /* Callback for our data source. */ + return; + } + + /* another data source (from server side) gets selected, + no longer need previous data from client. */ + if (ctx->clipboard_client_data_source) { + data_source = ctx->clipboard_client_data_source; + ctx->clipboard_client_data_source = NULL; + clipboard_data_source_unref(data_source); + } + + wl_array_for_each(mime_type, &selection_data_source->mime_types) { + rdp_debug_clipboard(b, "RDP %s (base:%p) available formats[%d]: %s\n", + __func__, selection_data_source, num_avail_format, *mime_type); + num_avail_format++; + } + + /* check supported clipboard formats */ + wl_array_for_each(mime_type, &selection_data_source->mime_types) { + index = clipboard_find_supported_format_by_mime_type(*mime_type); + if (index >= 0) { + CLIPRDR_FORMAT *f = &format[num_supported_format]; + + f->formatId = clipboard_supported_formats[index].format_id; + f->formatName = clipboard_supported_formats[index].format_name; + rdp_debug_clipboard(b, "RDP %s (base:%p) supported formats[%d]: %d: %s\n", + __func__, + selection_data_source, + num_supported_format, + f->formatId, + f->formatName ? + f->formatName : + clipboard_format_id_to_string(f->formatId, true)); + num_supported_format++; + } + } + + if (num_supported_format) { + /* let client knows formats are available in server clipboard */ + formatList.msgType = CB_FORMAT_LIST; + formatList.numFormats = num_supported_format; + formatList.formats = &format[0]; + ctx->clipboard_server_context->ServerFormatList(ctx->clipboard_server_context, &formatList); + } else { + rdp_debug_clipboard(b, "RDP %s (base:%p) no supported formats\n", __func__, selection_data_source); + } + + return; +} + +/*********************\ + * FreeRDP callbacks * +\*********************/ + +/* client reports the path of temp folder */ +static UINT +clipboard_client_temp_directory(CliprdrServerContext *context, const CLIPRDR_TEMP_DIRECTORY *tempDirectory) +{ + freerdp_peer *client = (freerdp_peer *)context->custom; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + + rdp_debug_clipboard(b, "Client: %s %s\n", __func__, tempDirectory->szTempDir); + return 0; +} + +/* client reports thier clipboard capabilities */ +static UINT +clipboard_client_capabilities(CliprdrServerContext *context, const CLIPRDR_CAPABILITIES *capabilities) +{ + freerdp_peer *client = (freerdp_peer *)context->custom; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + + rdp_debug_clipboard(b, "Client: clipboard capabilities: cCapabilitiesSet:%d\n", capabilities->cCapabilitiesSets); + for (uint32_t i = 0; i < capabilities->cCapabilitiesSets; i++) { + CLIPRDR_CAPABILITY_SET *capabilitySets = &capabilities->capabilitySets[i]; + CLIPRDR_GENERAL_CAPABILITY_SET *generalCapabilitySet = (CLIPRDR_GENERAL_CAPABILITY_SET *)capabilitySets; + + switch (capabilitySets->capabilitySetType) { + case CB_CAPSTYPE_GENERAL: + rdp_debug_clipboard(b, "Client: clipboard capabilities[%d]: General\n", i); + rdp_debug_clipboard(b, " Version:%d\n", generalCapabilitySet->version); + rdp_debug_clipboard(b, " GeneralFlags:0x%x\n", generalCapabilitySet->generalFlags); + if (generalCapabilitySet->generalFlags & CB_USE_LONG_FORMAT_NAMES) + rdp_debug_clipboard(b, " CB_USE_LONG_FORMAT_NAMES\n"); + if (generalCapabilitySet->generalFlags & CB_STREAM_FILECLIP_ENABLED) + rdp_debug_clipboard(b, " CB_STREAM_FILECLIP_ENABLED\n"); + if (generalCapabilitySet->generalFlags & CB_FILECLIP_NO_FILE_PATHS) + rdp_debug_clipboard(b, " CB_FILECLIP_NO_FILE_PATHS\n"); + if (generalCapabilitySet->generalFlags & CB_CAN_LOCK_CLIPDATA) + rdp_debug_clipboard(b, " CB_CAN_LOCK_CLIPDATA\n"); + break; + default: + return -1; + } + } + return 0; +} + +/* client reports the supported format list in client's clipboard */ +static UINT +clipboard_client_format_list(CliprdrServerContext *context, const CLIPRDR_FORMAT_LIST *formatList) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse = {}; + freerdp_peer *client = (freerdp_peer *)context->custom; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct rdp_clipboard_data_source *source = NULL; + char **p, *s; + + ASSERT_NOT_COMPOSITOR_THREAD(b); + + rdp_debug_clipboard(b, "Client: %s clipboard format list: numFormats:%d\n", __func__, formatList->numFormats); + for (uint32_t i = 0; i < formatList->numFormats; i++) { + CLIPRDR_FORMAT *format = &formatList->formats[i]; + + rdp_debug_clipboard(b, "Client: %s clipboard formats[%d]: formatId:%d, formatName:%s\n", + __func__, i, format->formatId, + format->formatName ? format->formatName : clipboard_format_id_to_string(format->formatId, false)); + } + + source = zalloc(sizeof *source); + if (!source) + goto fail; + + source->state = RDP_CLIPBOARD_SOURCE_ALLOCATED; + rdp_debug_clipboard(b, "Client: %s (%p:%s) allocated\n", + __func__, source, clipboard_data_source_state_to_string(source)); + wl_signal_init(&source->base.destroy_signal); + wl_array_init(&source->base.mime_types); + wl_array_init(&source->data_contents); + source->context = client; + source->refcount = 1; // decremented when another source is selected. + source->data_source_fd = -1; + source->format_index = -1; + + for (uint32_t i = 0; i < formatList->numFormats; i++) { + CLIPRDR_FORMAT *format = &formatList->formats[i]; + int index = clipboard_find_supported_format_by_format_id_and_name(format->formatId, format->formatName); + + if (index >= 0) { + /* save format id given from client, client can handle its own format id for private format. */ + source->client_format_id_table[index] = format->formatId; + s = strdup(clipboard_supported_formats[index].mime_type); + if (s) { + p = wl_array_add(&source->base.mime_types, sizeof *p); + if (p) { + rdp_debug_clipboard(b, "Client: %s (%p:%s) mine_type:\"%s\" index:%d formatId:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + s, index, format->formatId); + *p = s; + } else { + rdp_debug_clipboard(b, "Client: %s (%p:%s) wl_array_add failed\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + free(s); + } + } else { + rdp_debug_clipboard(b, "Client: %s (%p:%s) strdup failed\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + } + } + } + + if (formatList->numFormats != 0 && + source->base.mime_types.size == 0) { + rdp_debug_clipboard(b, "Client: %s (%p:%s) no formats are supported\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + } + + source->state = RDP_CLIPBOARD_SOURCE_FORMATLIST_READY; + rdp_dispatch_task_to_display_loop(ctx, clipboard_data_source_publish, &source->task_base); + +fail: + formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.msgFlags = source ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + formatListResponse.dataLen = 0; + if (ctx->clipboard_server_context->ServerFormatListResponse(ctx->clipboard_server_context, &formatListResponse) != 0) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("Client: %s (%p:%s) ServerFormatListResponse failed\n", + __func__, source, + clipboard_data_source_state_to_string(source)); + return -1; + } + return 0; +} + +/* client responded with clipboard data asked by server */ +static UINT +clipboard_client_format_data_response(CliprdrServerContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse) +{ + freerdp_peer *client = (freerdp_peer *)context->custom; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct wl_event_loop *loop = wl_display_get_event_loop(b->compositor->wl_display); + struct rdp_clipboard_data_source *source = ctx->clipboard_inflight_client_data_source; + bool success = false; + bool ret; + + rdp_debug_clipboard(b, "Client: %s (%p:%s) flags:%d dataLen:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + formatDataResponse->msgFlags, + formatDataResponse->dataLen); + + ASSERT_NOT_COMPOSITOR_THREAD(b); + + if (!source) { + rdp_debug_clipboard(b, "Client: %s client send data without server asking. protocol error", __func__); + return -1; + } + + if (source->transfer_event_source || (source->inflight_write_count != 0)) { + /* here means client responded more than once for single data request */ + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("Client: %s (%p:%s) middle of write loop:%p, %d\n", + __func__, source, clipboard_data_source_state_to_string(source), + source->transfer_event_source, source->inflight_write_count); + return -1; + } + + if (formatDataResponse->msgFlags == CB_RESPONSE_OK) { + /* Recieved data from client, cache to data source */ + if (wl_array_add(&source->data_contents, formatDataResponse->dataLen+1)) { + memcpy(source->data_contents.data, + formatDataResponse->requestedFormatData, + formatDataResponse->dataLen); + source->data_contents.size = formatDataResponse->dataLen; + /* regardless data type, make sure it ends with NULL */ + ((char *)source->data_contents.data)[source->data_contents.size] = '\0'; + /* data is ready, waiting to be written to destination */ + source->state = RDP_CLIPBOARD_SOURCE_RECEIVED_DATA; + success = true; + } else { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + } + } else { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + source->data_response_fail_count++; + } + rdp_debug_clipboard_verbose(b, "Client: %s (%p:%s) fail count:%d\n", + __func__, source, + clipboard_data_source_state_to_string(source), + source->data_response_fail_count); + + assert(source->transfer_event_source == NULL); + ret = rdp_event_loop_add_fd(loop, source->data_source_fd, WL_EVENT_WRITABLE, + success ? clipboard_data_source_write : clipboard_data_source_fail, + source, &source->transfer_event_source); + if (!ret) { + source->state = RDP_CLIPBOARD_SOURCE_FAILED; + weston_log("Client: %s (%p:%s) rdp_event_loop_add_fd failed\n", + __func__, source, clipboard_data_source_state_to_string(source)); + return -1; + } + + return 0; +} + +/* client responded on the format list sent by server */ +static UINT +clipboard_client_format_list_response(CliprdrServerContext *context, + const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse) +{ + freerdp_peer *client = (freerdp_peer *)context->custom; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + + rdp_debug_clipboard(b, "Client: %s msgFlags:0x%x\n", __func__, formatListResponse->msgFlags); + ASSERT_NOT_COMPOSITOR_THREAD(b); + return 0; +} + +/* client requested the data of specificed format in server clipboard */ +static UINT +clipboard_client_format_data_request(CliprdrServerContext *context, + const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest) +{ + freerdp_peer *client = (freerdp_peer *)context->custom; + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct rdp_clipboard_data_request *request; + int index; + + rdp_debug_clipboard(b, "Client: %s requestedFormatId:%d - %s\n", + __func__, formatDataRequest->requestedFormatId, + clipboard_format_id_to_string(formatDataRequest->requestedFormatId, true)); + + ASSERT_NOT_COMPOSITOR_THREAD(b); + + /* Make sure clients requested the format we knew */ + index = clipboard_find_supported_format_by_format_id(formatDataRequest->requestedFormatId); + if (index < 0) { + weston_log("Client: %s client requests data format the server never reported in format list response. protocol error.\n", __func__); + goto error_return; + } + + request = zalloc(sizeof(*request)); + if (!request) { + weston_log("zalloc failed\n"); + goto error_return; + } + request->ctx = ctx; + request->requested_format_index = index; + rdp_dispatch_task_to_display_loop(ctx, clipboard_data_source_request, &request->task_base); + + return 0; + +error_return: + /* send FAIL response to client */ + clipboard_client_send_format_data_response_fail(ctx, NULL); + return 0; +} + +/********************\ + * Public functions * +\********************/ + +int +rdp_clipboard_init(freerdp_peer *client) +{ + RdpPeerContext *ctx = (RdpPeerContext *)client->context; + struct rdp_backend *b = ctx->rdpBackend; + struct weston_seat *seat = ctx->item.seat; + CliprdrServerContext *clip_ctx; + + assert(seat); + + ASSERT_COMPOSITOR_THREAD(b); + + ctx->clipboard_server_context = cliprdr_server_context_new(ctx->vcm); + if (!ctx->clipboard_server_context) + goto error; + + clip_ctx = ctx->clipboard_server_context; + clip_ctx->custom = (void *)client; + clip_ctx->TempDirectory = clipboard_client_temp_directory; + clip_ctx->ClientCapabilities = clipboard_client_capabilities; + clip_ctx->ClientFormatList = clipboard_client_format_list; + clip_ctx->ClientFormatListResponse = clipboard_client_format_list_response; + /* clip_ctx->ClientLockClipboardData */ + /* clip_ctx->ClientUnlockClipboardData */ + clip_ctx->ClientFormatDataRequest = clipboard_client_format_data_request; + clip_ctx->ClientFormatDataResponse = clipboard_client_format_data_response; + /* clip_ctxClientFileContentsRequest */ + /* clip_ctx->ClientFileContentsResponse */ + clip_ctx->useLongFormatNames = FALSE; /* ASCII8 format name only (No Windows-style 2 bytes Unicode). */ + clip_ctx->streamFileClipEnabled = FALSE; + clip_ctx->fileClipNoFilePaths = FALSE; + clip_ctx->canLockClipData = TRUE; + if (clip_ctx->Start(ctx->clipboard_server_context) != 0) + goto error; + + ctx->clipboard_selection_listener.notify = clipboard_set_selection; + wl_signal_add(&seat->selection_signal, + &ctx->clipboard_selection_listener); + + return 0; + +error: + if (ctx->clipboard_server_context) { + cliprdr_server_context_free(ctx->clipboard_server_context); + ctx->clipboard_server_context = NULL; + } + + return -1; +} + +void +rdp_clipboard_destroy(RdpPeerContext *ctx) +{ + struct rdp_clipboard_data_source *data_source; + struct rdp_backend *b = ctx->rdpBackend; + + ASSERT_COMPOSITOR_THREAD(b); + + if (ctx->clipboard_selection_listener.notify) { + wl_list_remove(&ctx->clipboard_selection_listener.link); + ctx->clipboard_selection_listener.notify = NULL; + } + + if (ctx->clipboard_inflight_client_data_source) { + data_source = ctx->clipboard_inflight_client_data_source; + ctx->clipboard_inflight_client_data_source = NULL; + clipboard_data_source_unref(data_source); + } + + if (ctx->clipboard_client_data_source) { + data_source = ctx->clipboard_client_data_source; + ctx->clipboard_client_data_source = NULL; + clipboard_data_source_unref(data_source); + } + + if (ctx->clipboard_server_context) { + ctx->clipboard_server_context->Stop(ctx->clipboard_server_context); + cliprdr_server_context_free(ctx->clipboard_server_context); + ctx->clipboard_server_context = NULL; + } +} diff --git a/libweston/backend-rdp/rdputil.c b/libweston/backend-rdp/rdputil.c index 296dd7fb..8b49b8e2 100644 --- a/libweston/backend-rdp/rdputil.c +++ b/libweston/backend-rdp/rdputil.c @@ -226,3 +226,39 @@ rdp_destroy_dispatch_task_event_source(RdpPeerContext *peerCtx) pthread_mutex_destroy(&peerCtx->loop_task_list_mutex); } + +/* This is a little tricky - it makes sure there's always at least + * one spare byte in the array in case the caller needs to add a + * null terminator to it. We can't just null terminate the array + * here, because some callers won't want that - and some won't + * like having an odd number of bytes. + */ +int +rdp_wl_array_read_fd(struct wl_array *array, int fd) +{ + int len, size; + char *data; + + /* Make sure we have at least 1024 bytes of space left */ + if (array->alloc - array->size < 1024) { + if (!wl_array_add(array, 1024)) { + errno = ENOMEM; + return -1; + } + array->size -= 1024; + } + data = (char *)array->data + array->size; + /* Leave one char in case the caller needs space for a + * null terminator */ + size = array->alloc - array->size - 1; + do { + len = read(fd, data, size); + } while (len == -1 && errno == EINTR); + + if (len == -1) + return -1; + + array->size += len; + + return len; +}