diff options
author | sss <sss@dark-alexandr.net> | 2023-01-17 00:38:19 +0300 |
---|---|---|
committer | sss <sss@dark-alexandr.net> | 2023-01-17 00:38:19 +0300 |
commit | cc3f33db7a8d3c4ad373e646b199808e01bc5d9b (patch) | |
tree | ec09d690c7656ab5f2cc72607e05fb359c24d8b2 /src/rdp/rdp_clipboard.c |
added webrdp public code
Diffstat (limited to 'src/rdp/rdp_clipboard.c')
-rw-r--r-- | src/rdp/rdp_clipboard.c | 1524 |
1 files changed, 1524 insertions, 0 deletions
diff --git a/src/rdp/rdp_clipboard.c b/src/rdp/rdp_clipboard.c new file mode 100644 index 0000000..e8ab882 --- /dev/null +++ b/src/rdp/rdp_clipboard.c @@ -0,0 +1,1524 @@ +/* BSD-2-Clause license + * + * Copyright (c) 2018-2023 NST <www.newinfosec.ru>, sss <sss at dark-alexandr dot net>. + * + */ + +/* this code mostly ported from XFreeRDP */ + +#include <arpa/inet.h> +#include <iconv.h> + +#include "rdp_clipboard.h" +#include "rdp_ft.h" + +static UINT +rdp_clip_ServerCapabilities( + CliprdrClientContext *context, const CLIPRDR_CAPABILITIES *capabilities) +{ + UINT32 i; + const CLIPRDR_CAPABILITY_SET *caps; + const CLIPRDR_GENERAL_CAPABILITY_SET *generalCaps; + const BYTE *capsPtr = (const BYTE *)capabilities->capabilitySets; + my_rdp_clipboard *clipboard = (my_rdp_clipboard *)context->custom; + clipboard->streams_supported = false; + + for (i = 0; i < capabilities->cCapabilitiesSets; i++) + { + caps = (const CLIPRDR_CAPABILITY_SET *)capsPtr; + + if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL) + { + generalCaps + = (const CLIPRDR_GENERAL_CAPABILITY_SET *)caps; + + if (generalCaps->generalFlags + & CB_STREAM_FILECLIP_ENABLED) + { + clipboard->streams_supported = true; + } + } + + capsPtr += caps->capabilitySetLength; + } + { + const char *msg + = "rdp_module: cliprdr: server capabilities received"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_trace, 0); + } + + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_ClientCapabilities(CliprdrClientContext* context, + CLIPRDR_CAPABILITIES* capabilities) +{ + return CHANNEL_RC_OK; +}*/ + +static UINT +rdp_cliprdr_send_client_capabilities(my_rdp_clipboard *clipboard) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets + = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES; + /*|CB_CAN_LOCK_CLIPDATA; */ + + if (clipboard->streams_supported) + { + generalCapabilitySet.generalFlags |= CB_STREAM_FILECLIP_ENABLED + | CB_FILECLIP_NO_FILE_PATHS + | 0x00000020; + } + + { + const char *msg + = "rdp_module: cliprdr: client capabilities sent"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_trace, 0); + } + + return clipboard->clip_context->ClientCapabilities( + clipboard->clip_context, &capabilities); +} + +static UINT +rdp_cliprdr_send_client_format_list(my_rdp_clipboard *clipboard) +{ + const uint8_t format_count = 2; + CLIPRDR_FORMAT formats[format_count]; + CLIPRDR_FORMAT_LIST formatList; + memset(formats, 0, sizeof(CLIPRDR_FORMAT) * format_count); + formats[0].formatId = CF_RAW; + formats[1].formatId = CF_TEXT; + formatList.common.msgFlags = CB_RESPONSE_OK; + formatList.numFormats = format_count; + formatList.formats = formats; + + { + const char *msg + = "rdp_module: cliprdr: client format list sent"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_trace, 0); + } + + return clipboard->clip_context->ClientFormatList( + clipboard->clip_context, &formatList); +} + +static UINT +rdp_clip_MonitorReady( + CliprdrClientContext *context, const CLIPRDR_MONITOR_READY *monitorReady) +{ + my_rdp_clipboard *clipboard = context->custom; + UINT ret; + + if ((ret = rdp_cliprdr_send_client_capabilities(clipboard)) + != CHANNEL_RC_OK) + return ret; + + if ((ret = rdp_cliprdr_send_client_format_list(clipboard)) + != CHANNEL_RC_OK) + return ret; + + /* clipboard->sync = true; */ + { + const char *msg + = "rdp_module: cliprdr: monitor ready message handled"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_trace, 0); + } + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_TempDirectory(CliprdrClientContext* context, + CLIPRDR_TEMP_DIRECTORY* tempDirectory) +{ + return CHANNEL_RC_OK; +} */ + +/* unused for now + * static UINT +rdp_clip_ClientFormatList(CliprdrClientContext* context, + CLIPRDR_FORMAT_LIST* formatList) +{ + return CHANNEL_RC_OK; +}*/ + +static UINT +rdp_cliprdr_send_client_format_list_response( + my_rdp_clipboard *clipboard, BOOL status) +{ + CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse; + formatListResponse.common.msgType = CB_FORMAT_LIST_RESPONSE; + formatListResponse.common.msgFlags + = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + formatListResponse.common.dataLen = 0; + return clipboard->clip_context->ClientFormatListResponse( + clipboard->clip_context, &formatListResponse); +} + +static wrdp_enum_clip_format +clip_format_from_string(const char *fmt) +{ + if (!strcmp(fmt, "FileGroupDescriptorW")) + { + return clip_format_file_list; + } + /* + * unused + else if (!strcmp(fmt, "FileContents")) + { + } + else if (!strcmp(fmt, "Preffered DropEffect")) + { + } + */ + return clip_format_unsupported; +} + +static wrdp_enum_clip_format +clip_format_from_id(UINT32 id, my_rdp_clipboard *c) +{ + if (id == CF_TEXT || id == CF_OEMTEXT) + { + return clip_format_text; + } + else if (id == CF_RAW) + { + return clip_format_raw; + } + else if (id == CF_UNICODETEXT) + { + return clip_format_unicode; + } + else if (id == CB_FORMAT_TEXTURILIST) + { + return clip_format_file_list; + } + return clip_format_unsupported; +} + +static UINT +rdp_clip_ServerFormatList( + CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST *formatList) +{ + UINT32 i; + uint8_t num_supported_fmts = 0, *out_fmts = 0, num_server_formats; + my_rdp_clipboard *clipboard = context->custom; + my_clip_format *server_formats = 0; + UINT ret; + + if (clipboard->my_rdp_context->ft_to_server->is_runing) + { + const char *msg + = "rdp_module: clipboard: new clipboard formats list " + "during runing filetransfer"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + /* return CHANNEL_RC_OK; */ + } + + num_server_formats = formatList->numFormats + 1; /* +1 for CF_RAW */ + + if (!(server_formats + = calloc(num_server_formats, sizeof(my_clip_format)))) + { + char buf[128]; + snprintf(buf, 127, + "failed to allocate %d my_clip_format structs", + num_server_formats); + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)buf, strlen(buf), + wrdp_log_level_error, 0); + return CHANNEL_RC_NO_MEMORY; + } + + if (clipboard->srv_fmts && clipboard->srv_fmts_count) + { + for (i = 0; i < clipboard->srv_fmts_count; ++i) + { + if (clipboard->srv_fmts[i].rdp_fmt.formatName) + { + free(clipboard->srv_fmts[i].rdp_fmt.formatName); + } + } + free(clipboard->srv_fmts); + } + + clipboard->srv_fmts_count = formatList->numFormats; + for (i = 0; i < formatList->numFormats; i++) + { + CLIPRDR_FORMAT *format = &formatList->formats[i]; + server_formats[i].rdp_fmt.formatId = format->formatId; + wrdp_enum_clip_format fmt + = clip_format_from_id(format->formatId, clipboard); + if (format->formatName) + { + server_formats[i].rdp_fmt.formatName + = strdup(format->formatName); + } + if (fmt == clip_format_unsupported && format->formatName) + { + fmt = clip_format_from_string(format->formatName); + } + if (fmt != clip_format_unsupported) + { + num_supported_fmts++; + } + server_formats[i].my_fmt = fmt; + } + /* CF_RAW is always implicitly supported by the server */ + { + my_clip_format *format + = &server_formats[formatList->numFormats]; + format->rdp_fmt.formatId = CF_RAW; + format->rdp_fmt.formatName = NULL; + format->my_fmt = clip_format_raw; + num_supported_fmts++; + } + { + out_fmts = calloc(num_supported_fmts, sizeof(uint8_t)); + if (!out_fmts) + { + perror("calloc"); + if (server_formats) + { + free(server_formats); + } + return CHANNEL_RC_NO_MEMORY; + } + uint8_t pos = 0; + for (i = 0; i < num_supported_fmts; ++i) + { + if (server_formats[i].my_fmt == clip_format_unsupported) + { + continue; + } + out_fmts[pos] = server_formats[i].my_fmt; + pos++; + } + clipboard->my_rdp_context->my_internals->core->api_clipboard + ->clipboard_changed(out_fmts, num_supported_fmts, + clipboard->my_rdp_context->my_internals->task_info); + free(out_fmts); + clipboard->srv_fmts = server_formats; + } + + { + const char *msg = "rdp_module: cliprdr: server format list" + " changed message received"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_trace, 0); + } + + ret = rdp_cliprdr_send_client_format_list_response(clipboard, TRUE); + return ret; +} + +/* unused for now + * static UINT +rdp_clip_ClientFormatListResponse(CliprdrClientContext* context, + CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse) +{ + return CHANNEL_RC_OK; +}*/ + +static UINT +rdp_clip_ServerFormatListResponse(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse) +{ + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_ClientLockClipboardData(CliprdrClientContext* context, + CLIPRDR_LOCK_CLIPBOARD_DATA* lockClipboardData) +{ + return CHANNEL_RC_OK; +}*/ + +static UINT +rdp_clip_ServerLockClipboardData(CliprdrClientContext *context, + const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData) +{ + /* my_rdp_clipboard* clipboard = context->custom; + clipboard->my_rdp_context->ft_to_server->clip_data_id = + lockClipboardData->clipDataId; */ + return CHANNEL_RC_OK; +} + +/* + * unused +static UINT +rdp_clip_ClientUnlockClipboardData(CliprdrClientContext* context, + CLIPRDR_UNLOCK_CLIPBOARD_DATA* unlockClipboardData) +{ + return CHANNEL_RC_OK; +} +*/ + +static UINT +rdp_clip_ServerUnlockClipboardData(CliprdrClientContext *context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData) +{ + /* TODO: */ + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_ClientFormatDataRequest(CliprdrClientContext* context, + CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest) +{ + return CHANNEL_RC_OK; +}*/ + +static UINT +rdp_clip_ServerFormatDataRequest(CliprdrClientContext *context, + const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest) +{ + my_rdp_clipboard *clipboard = context->custom; + wrdp_enum_clip_format fmt = clip_format_from_id( + formatDataRequest->requestedFormatId, clipboard); + clipboard->my_rdp_context->my_internals->core->api_clipboard + ->request_data( + fmt, clipboard->my_rdp_context->my_internals->task_info); + + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_ClientFormatDataResponse(CliprdrClientContext* context, + CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse) +{ + return CHANNEL_RC_OK; +}*/ + +/*static UINT rdp_cliprdr_send_data_response(my_rdp_clipboard* clipboard, + BYTE* data, int size) +{ + CLIPRDR_FORMAT_DATA_RESPONSE response; + ZeroMemory(&response, sizeof(CLIPRDR_FORMAT_DATA_RESPONSE)); + response.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL; + response.dataLen = size; + response.requestedFormatData = data; + return clipboard->clip_context->ClientFormatDataResponse( + clipboard->clip_context, &response); +} */ + +static UINT +rdp_clip_ServerFormatDataResponse(CliprdrClientContext *context, + const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse) +{ + UINT32 size = formatDataResponse->common.dataLen; + const BYTE *data = formatDataResponse->requestedFormatData; + my_rdp_clipboard *clipboard = context->custom; + + if (!data) + { + const char *msg + = "rdp_module: cliprdr: failed to get clipboard data"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_error, 0); + return CHANNEL_RC_OK; + } + + switch (clipboard->cli_req_fmt_id.my_fmt) + { + case clip_format_file_list: + { + /* TODO: check this */ + UINT error = NO_ERROR; + FILEDESCRIPTORW *file_array = 0; + UINT32 file_count = 0, pos = 0; + uint8_t *msg_struct = 0, *msg = 0; + wrdp_backend_ft_list_entry *list = 0; + size_t msg_size_result = 0; + error = cliprdr_parse_file_list( + data, size, &file_array, &file_count); + if (error) + { + char buf[128]; + snprintf(buf, 127, + "failed to deserialize" + " CLIPRDR_FILELIST: 0x%08X", + error); + clipboard->my_rdp_context->my_internals->core + ->api_utils->log_msg((const uint8_t *)buf, + strlen(buf), wrdp_log_level_error, 0); + return CHANNEL_RC_OK; + } + msg_struct = calloc( + file_count, sizeof(wrdp_backend_ft_list_entry)); + if (!msg_struct) + { + perror("calloc"); + if (file_count && file_array) + { + free(file_array); + } + return CHANNEL_RC_OK; + } + msg_size_result + = (file_count * sizeof(wrdp_backend_ft_list_entry)) + + 3; + char *filename_array[file_count]; + { + int i = 0; + for (; i < file_count; ++i) + { + filename_array[i] = 0; + } + } + list = (wrdp_backend_ft_list_entry *)msg_struct; + for (; file_array && pos < file_count; ++pos) + { + list[pos].file_id = pos; + if (file_array[pos].cFileName[0]) + { + iconv_t utf16_to_utf8 + = iconv_open("UTF-8", "UTF-16LE"); + char *ptr_w, *ptr_r, + *tmp_buf + = calloc(520, sizeof(char)); + size_t name_len_utf16 + = 260 * sizeof(WCHAR), + name_len_utf8 = 520, + name_len_result = 0; + if (!tmp_buf) + { + perror("calloc"); + iconv_close(utf16_to_utf8); + free(msg_struct); + if (file_count) + { + int i = 0; + for (; (i < file_count) + && filename_array + [i]; + ++i) + { + free( + filename_array + [i]); + } + free(file_array); + } + return CHANNEL_RC_OK; + } + ptr_w = tmp_buf; + ptr_r + = (char *)file_array[pos].cFileName; + while (name_len_utf16 && name_len_utf8) + { + iconv(utf16_to_utf8, &ptr_r, + &name_len_utf16, &ptr_w, + &name_len_utf8); + } +#ifdef DEBUG + if (!name_len_utf8 && name_len_utf16) + { + log_msg_info i = {0}; + i.level = wrdp_log_level_trace; + i.buf = (const uint8_t + *)"rdp_module: " + "cliprdr: iconv: not " + "sufficient space in " + "output buffer"; + i.task_info + = clipboard->my_internals + ->task_info; + clipboard->my_internals->core + ->api_utils->log_msg_ex(&i); + } +#endif + iconv(utf16_to_utf8, 0, 0, &ptr_w, + &name_len_utf8); + name_len_result = strlen(tmp_buf); + msg_size_result += name_len_result; + list[pos].filename_len + = name_len_result; + filename_array[pos] = strdup(tmp_buf); + iconv_close(utf16_to_utf8); + free(tmp_buf); + } + /* TODO: check/change endianness */ + memcpy(&(list[pos].file_size), + &file_array[pos].nFileSizeLow, 4); + memcpy(((uint8_t *)&(list[pos].file_size)) + 4, + &file_array[pos].nFileSizeHigh, 4); + } + msg = malloc(msg_size_result); + if (!msg) + { + perror("malloc"); + for (pos = 0; pos < file_count; ++pos) + { + free(filename_array[pos]); + } + free(msg_struct); + return CHANNEL_RC_OK; + } + memset(msg, 0, msg_size_result); + *((uint8_t *)msg) = clip_format_file_list; + memcpy(msg + 1, &file_count, 2); + { + size_t msg_pos = 3; + for (pos = 0; pos < file_count; ++pos) + { + memcpy(msg + msg_pos, + &list[pos].filename_len, 2); + msg_pos += 2; + memcpy(msg + msg_pos, + &list[pos].file_id, 4); + msg_pos += 4; + memcpy(msg + msg_pos, + &list[pos].file_size, 8); + msg_pos += 8; + /* TODO: looks like i have missed + * something ... "Null pointer passed as + * an argument to a 'nonnull' parameter" + * just added null ptr check for now */ + if (filename_array[pos]) + { + memcpy(msg + msg_pos, + filename_array[pos], + list[pos].filename_len); + free(filename_array[pos]); + } + msg_pos += list[pos].filename_len; + } + } + free(msg_struct); + clipboard->my_rdp_context->my_internals->core + ->api_clipboard->send_data((uint8_t *)msg, + msg_size_result, + clipboard->my_rdp_context->my_internals + ->task_info); + free(msg); + if (file_count && file_array) + { + free(file_array); + } + return CHANNEL_RC_OK; + } + break; + case clip_format_raw: + case clip_format_text: + { + size_t msg_len = size + 1; + uint8_t *msg = malloc(msg_len); + if (!msg) + { + perror("malloc"); + return CHANNEL_RC_OK; + } + *(uint8_t *)msg = clip_format_text; + memcpy(msg + 1, data, size); + clipboard->my_rdp_context->my_internals->core + ->api_clipboard->send_data(msg, msg_len, + clipboard->my_rdp_context->my_internals + ->task_info); + free(msg); + } + break; + case clip_format_unicode: + { + iconv_t utf16_to_utf8 = iconv_open("UTF-8", "UTF-16LE"); + size_t in_left = size, out_left = size; + uint8_t *tmp_buf = malloc(out_left), *tmp_buf_ptr; + uint8_t *msg = 0; + if (!tmp_buf) + { + perror("malloc"); + return CHANNEL_RC_OK; + } + tmp_buf_ptr = tmp_buf; + /* wcstombs((char*)msg + 1, tmp_buf, text_len); */ + while (in_left && out_left) + { + if (iconv(utf16_to_utf8, (char **)&data, + &in_left, (char **)&tmp_buf_ptr, + &out_left) + == (size_t)-1) + { + log_msg_info mi = {0}; + mi.level = wrdp_log_level_warning; + mi.buf = (const uint8_t + *)"rdp_module: cliprdr: iconv: " + "failed to encode outgoing " + "buffer from utf8 to utf16le"; + mi.task_info = clipboard->my_internals + ->task_info; + clipboard->my_internals->core->api_utils + ->log_msg_ex(&mi); + free(tmp_buf); + iconv_close(utf16_to_utf8); + return CHANNEL_RC_NO_BUFFER; + } + } +#ifdef DEBUG + if (!out_left && in_left) + { + log_msg_info i = {0}; + i.level = wrdp_log_level_trace; + i.buf = (const uint8_t + *)"rdp_module: cliprdr: iconv: not " + "sufficient space in output buffer"; + i.task_info + = clipboard->my_internals->task_info; + clipboard->my_internals->core->api_utils + ->log_msg_ex(&i); + } +#endif + iconv(utf16_to_utf8, 0, 0, (char **)&tmp_buf_ptr, + &out_left); + iconv_close(utf16_to_utf8); + msg = malloc(size - out_left + 1); + if (!msg) + { + perror("malloc"); + return CHANNEL_RC_OK; + } + *(uint8_t *)msg = clip_format_unicode; + memcpy(msg + 1, tmp_buf, size - out_left); +#ifdef DEBUG + { + log_msg_info i = {0}; + size_t len = 1024; + char msg_buf[len]; + snprintf(msg_buf, len - 1, + "rdp_module: cliprdr: sending encoded from " + "utf16le to utf8 text: %s", + tmp_buf); + i.level = wrdp_log_level_trace; + i.buf = (const uint8_t *)msg_buf; + i.task_info + = clipboard->my_internals->task_info; + clipboard->my_internals->core->api_utils + ->log_msg_ex(&i); + } +#endif + free(tmp_buf); + clipboard->my_rdp_context->my_internals->core + ->api_clipboard->send_data(msg, size - out_left + 1, + clipboard->my_rdp_context->my_internals + ->task_info); + free(msg); + } + break; + default: + { + const char *msg = "rdp_module: cliprdr: unsupported" + " clipboardformat requested"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + } + break; + } + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_ClientFileContentsRequest(CliprdrClientContext* context, + CLIPRDR_FILE_CONTENTS_REQUEST* fileContentsRequest) +{ + return CHANNEL_RC_OK; +}*/ + +static UINT +rdp_clip_ServerFileContentsRequest(CliprdrClientContext *context, + const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest) +{ + my_rdp_clipboard *clipboard = context->custom; + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + CLIPRDR_FILE_CONTENTS_RESPONSE resp; + if (fileContentsRequest->nPositionLow + || fileContentsRequest->nPositionHigh + || (fileContentsRequest->cbRequested != 0x00000008)) + { + const char *msg + = "rdp_module: ft: ServerFileContentsRequest: " + "malformed request"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + return CHANNEL_RC_OK; + } + memset(&resp, 0, sizeof(CLIPRDR_FILE_CONTENTS_RESPONSE)); + resp.common.msgType = CB_FILECONTENTS_RESPONSE; + resp.common.msgFlags = CB_RESPONSE_OK; + resp.streamId = fileContentsRequest->streamId; + resp.cbRequested = sizeof(uint64_t); + resp.requestedData = (BYTE *)&( + clipboard + ->client_filelist_cache[fileContentsRequest->listIndex] + .file_size); + clipboard->clip_context->ClientFileContentsResponse( + clipboard->clip_context, &resp); + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + wrdp_backend_ft_file_request req; + memset(&req, 0, sizeof(wrdp_backend_ft_list_entry)); + /* TODO: better way to detect runing transfer */ + clipboard->my_rdp_context->ft_to_server->is_runing = true; + req.file_id = fileContentsRequest->listIndex; + req.req_size = fileContentsRequest->cbRequested; + clipboard->my_rdp_context->ft_to_server->transfer_id + = fileContentsRequest->streamId; + memcpy( + &req.file_offset, &(fileContentsRequest->nPositionLow), 4); + memcpy(((uint8_t *)&req.file_offset) + 4, + &(fileContentsRequest->nPositionHigh), 4); + clipboard->my_rdp_context->my_internals->core->api_filetransfers + ->ft_request(&req, + clipboard->my_rdp_context->my_internals->task_info); + } + return CHANNEL_RC_OK; +} + +/* unused for now + * static UINT +rdp_clip_ClientFileContentsResponse(CliprdrClientContext* context, + CLIPRDR_FILE_CONTENTS_RESPONSE* fileContentsResponse) +{ + return CHANNEL_RC_OK; +} */ + +static UINT +rdp_clip_ServerFileContentsResponse( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *resp) +{ + /* TODO: somehow avoid memory copying here */ + my_rdp_clipboard *clipboard = context->custom; + wrdp_backend_ft_chunk chunk; + if (resp->common.msgFlags & CB_RESPONSE_FAIL) + { + const char *msg = "rdp_module: ft: ServerFileContentsResponse: " + "CB_RESPONSE_FAIL"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_error, 0); + goto transfer_failed; + } + if (!resp->cbRequested + && (clipboard->my_rdp_context->ft_to_client->transferred + != clipboard->my_rdp_context->ft_to_client->file_size)) + { + char msg[128]; + snprintf(msg, 127, + "rdp_module: ft: chunk with zero size received on" + " position %lu of %lu", + clipboard->my_rdp_context->ft_to_client->transferred, + clipboard->my_rdp_context->ft_to_client->file_size); + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_error, 0); + goto transfer_failed; + } + else if (clipboard->my_rdp_context->ft_to_client->transfer_id + != resp->streamId) + { + const char *msg + = "rdp_module: clipboard: ft: transfer_id mismatch in " + "ServerFileContentsResponse"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_error, 0); + goto transfer_failed; + } + else if (resp->cbRequested) + { + chunk.transfer_id = resp->streamId; + chunk.size = resp->cbRequested; + clipboard->my_rdp_context->my_internals->core->api_filetransfers + ->ft_send_chunk(&chunk, (uint8_t *)(resp->requestedData), + clipboard->my_rdp_context->my_internals->task_info); + clipboard->my_rdp_context->ft_to_client->transferred + += resp->cbRequested; + } + /* request next file chunk */ + if (clipboard->my_rdp_context->ft_to_client->transferred + < clipboard->my_rdp_context->ft_to_client->file_size) + { + my_rdp_context *c = clipboard->my_rdp_context; + CLIPRDR_FILE_CONTENTS_REQUEST file_req; + uint32_t chunk_len = 512000; + uint64_t left = 0; + memset(&file_req, 0, sizeof(CLIPRDR_FILE_CONTENTS_REQUEST)); + + left + = c->ft_to_client->file_size - c->ft_to_client->transferred; + if (left < chunk_len) + { + chunk_len = left; + } + file_req.cbRequested = chunk_len; + c->ft_to_client->transfer_id = rand(); + file_req.streamId = c->ft_to_client->transfer_id; + file_req.listIndex = c->ft_to_client->file_id; + file_req.dwFlags = FILECONTENTS_RANGE; + + /* TODO: check/change endianness */ + memcpy(&(file_req.nPositionLow), + &(c->ft_to_client->transferred), 4); + memcpy(&(file_req.nPositionHigh), + (((uint8_t *)&(c->ft_to_client->transferred)) + 4), 4); + c->clipboard->clip_context->ClientFileContentsRequest( + c->clipboard->clip_context, &file_req); + } + else if (clipboard->my_rdp_context->ft_to_client->is_runing == true) + { + my_rdp_context *c = clipboard->my_rdp_context; + wrdp_backend_ft_status status; + { + char msg[128]; + snprintf(msg, 127, + "rdp_module: ft: status: transfer finished," + " transfered size: %lu", + clipboard->my_rdp_context->ft_to_client + ->transferred); + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_trace, 0); + } + memset(&status, 0, sizeof(wrdp_backend_ft_status)); + status.status = ft_status_success; + status.file_id = c->ft_to_client->file_id; + status.transfer_id = c->ft_to_client->transfer_id; + c->my_internals->core->api_filetransfers->ft_finish( + &status, c->my_internals->task_info); + clipboard->my_rdp_context->ft_to_client->file_size = 0; + clipboard->my_rdp_context->ft_to_client->transfer_id = 0; + clipboard->my_rdp_context->ft_to_client->file_id = 0; + clipboard->my_rdp_context->ft_to_client->is_runing = false; + clipboard->my_rdp_context->ft_to_client->transferred = 0; + } + else + { + const char *msg = "rdp_module: clipboard: ft:" + " unexpected data chunk on finished transfer"; + clipboard->my_rdp_context->my_internals->core->api_utils + ->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + } + return CHANNEL_RC_OK; +transfer_failed: +{ + my_rdp_context *c = clipboard->my_rdp_context; + wrdp_backend_ft_status status; + memset(&status, 0, sizeof(wrdp_backend_ft_status)); + status.status = ft_status_failure; + status.file_id = c->ft_to_client->file_id; + status.transfer_id = c->ft_to_client->transfer_id; + c->my_internals->core->api_filetransfers->ft_finish( + &status, c->my_internals->task_info); + clipboard->my_rdp_context->ft_to_client->file_size = 0; + clipboard->my_rdp_context->ft_to_client->transfer_id = 0; + clipboard->my_rdp_context->ft_to_client->file_id = 0; + clipboard->my_rdp_context->ft_to_client->is_runing = false; + clipboard->my_rdp_context->ft_to_client->transferred = 0; +} + return CHANNEL_RC_OK; +} + +void +rdp_cliprdr_init(my_rdp_context *ctx, CliprdrClientContext *cliprdr) +{ + ctx->clipboard->clip_context = cliprdr; + cliprdr->custom = (void *)ctx->clipboard; + ctx->clipboard->my_internals = ctx->my_internals; + cliprdr->MonitorReady = rdp_clip_MonitorReady; + cliprdr->ServerCapabilities = rdp_clip_ServerCapabilities; + cliprdr->ServerFormatList = rdp_clip_ServerFormatList; + cliprdr->ServerFormatListResponse = rdp_clip_ServerFormatListResponse; + cliprdr->ServerFormatDataRequest = rdp_clip_ServerFormatDataRequest; + cliprdr->ServerFormatDataResponse = rdp_clip_ServerFormatDataResponse; + cliprdr->ServerFileContentsRequest = rdp_clip_ServerFileContentsRequest; + cliprdr->ServerFileContentsResponse + = rdp_clip_ServerFileContentsResponse; + cliprdr->ServerLockClipboardData = rdp_clip_ServerLockClipboardData; + cliprdr->ServerUnlockClipboardData = rdp_clip_ServerUnlockClipboardData; +} + +void +rdp_cliprdr_uninit(my_rdp_context *ctx, CliprdrClientContext *cliprdr) +{ + /* do not need to do it here, done during backend destruction */ + /* cliprdr->custom = NULL; + + if (ctx->clipboard) + ctx->clipboard->clip_context = NULL; + ctx->clipboard = NULL; */ +} + +/* unused for now + * static UINT +ClientRequestFileSize(wClipboardDelegate* delegate, + const wClipboardFileSizeRequest* request) +{ + return 0; +}*/ + +static UINT +ClipboardFileSizeSuccess(wClipboardDelegate *delegate, + const wClipboardFileSizeRequest *request, UINT64 fileSize) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response; + my_rdp_clipboard *clipboard = delegate->custom; + ZeroMemory(&response, sizeof(response)); + response.common.msgFlags = CB_RESPONSE_OK; + response.streamId = request->streamId; + response.cbRequested = sizeof(UINT64); + response.requestedData = (BYTE *)&fileSize; + return clipboard->clip_context->ClientFileContentsResponse( + clipboard->clip_context, &response); + // return 0; +} +static UINT +ClipboardFileSizeFailure(wClipboardDelegate *delegate, + const wClipboardFileSizeRequest *request, UINT errorCode) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response; + my_rdp_clipboard *clipboard = delegate->custom; + ZeroMemory(&response, sizeof(response)); + response.common.msgFlags = CB_RESPONSE_FAIL; + response.streamId = request->streamId; + return clipboard->clip_context->ClientFileContentsResponse( + clipboard->clip_context, &response); +} + +/* unused for now + * static UINT +ClientRequestFileRange(wClipboardDelegate* delegate, + const wClipboardFileRangeRequest* request) +{ + return 0; +} */ + +static UINT +ClipboardFileRangeSuccess(wClipboardDelegate *delegate, + const wClipboardFileRangeRequest *request, const BYTE *data, UINT32 size) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response; + my_rdp_clipboard *clipboard = delegate->custom; + ZeroMemory(&response, sizeof(response)); + response.common.msgFlags = CB_RESPONSE_OK; + response.streamId = request->streamId; + response.cbRequested = size; + response.requestedData = (BYTE *)data; + return clipboard->clip_context->ClientFileContentsResponse( + clipboard->clip_context, &response); +} + +static UINT +ClipboardFileRangeFailure(wClipboardDelegate *delegate, + const wClipboardFileRangeRequest *request, UINT errorCode) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE response; + my_rdp_clipboard *clipboard = delegate->custom; + ZeroMemory(&response, sizeof(response)); + response.common.msgFlags = CB_RESPONSE_FAIL; + response.streamId = request->streamId; + return clipboard->clip_context->ClientFileContentsResponse( + clipboard->clip_context, &response); +} + +my_rdp_clipboard * +rdp_clipboard_new(my_rdp_context *context) +{ + my_rdp_clipboard *clip = calloc(1, sizeof(my_rdp_clipboard)); + if (!clip) + { + perror("calloc"); + return 0; + } + clip->my_rdp_context = context; + clip->clipboard = ClipboardCreate(); + clip->delegate = ClipboardGetDelegate(clip->clipboard); + clip->delegate->custom = clip; + + clip->delegate->ClipboardFileSizeSuccess = ClipboardFileSizeSuccess; + clip->delegate->ClipboardFileSizeFailure = ClipboardFileSizeFailure; + clip->delegate->ClipboardFileRangeSuccess = ClipboardFileRangeSuccess; + clip->delegate->ClipboardFileRangeFailure = ClipboardFileRangeFailure; + + context->clipboard = clip; + + return clip; +} + +static UINT +cliprdr_send_data_request(my_rdp_clipboard *clipboard) +{ + CLIPRDR_FORMAT_DATA_REQUEST request; + ZeroMemory(&request, sizeof(CLIPRDR_FORMAT_DATA_REQUEST)); + request.requestedFormatId = clipboard->cli_req_fmt_id.rdp_fmt.formatId; + return clipboard->clip_context->ClientFormatDataRequest( + clipboard->clip_context, &request); +} + +static bool +clip_api_handle_request_data( + const wrdp_backend_clipbrd_data_request *request, void *backend_internals) +{ + rdp_internals *i = backend_internals; + i->context->clipboard->cli_req_fmt_id.my_fmt = request->format; + switch (request->format) + { + case clip_format_raw: + { + /* i->context->clipboard->cli_req_fmt_id = CF_RAW; + * raw format does not work for some reason + */ + i->context->clipboard->cli_req_fmt_id.rdp_fmt.formatId + = CF_TEXT; + } + break; + case clip_format_text: + { + i->context->clipboard->cli_req_fmt_id.rdp_fmt.formatId + = CF_TEXT; + } + break; + case clip_format_file_list: + { + /* 4.5.3 Format Data Request PDU + The following is an annotated dump of a Format + Data Request PDU (section 2.2.5.1). The format being + requested is the File List that was advertised + in section 4.5.1 (the advertised ID in the Format + List PDU was 49273). + */ + uint8_t p; + bool format_found = false; + for (p = 0; p < i->context->clipboard->srv_fmts_count; + ++p) + { + if (!strcmp("FileGroupDescriptorW", + i->context->clipboard->srv_fmts[p] + .rdp_fmt.formatName)) + { + i->context->clipboard->cli_req_fmt_id + .rdp_fmt.formatId + = i->context->clipboard->srv_fmts[p] + .rdp_fmt.formatId; + format_found = true; + break; + } + } + if (!format_found) + { + const char *msg = "rdp_module: cliprdr: " + "requested data format" + " id not found"; + i->core->api_utils->log_msg( + (const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + i->context->clipboard->cli_req_fmt_id.rdp_fmt + .formatId + = CF_RAW; + } + } + break; + case clip_format_unicode: + { + i->context->clipboard->cli_req_fmt_id.rdp_fmt.formatId + = CF_UNICODETEXT; + } + break; + default: + { + const char *msg + = "rdp_module: cliprdr: unsuported data format" + " requested"; + i->core->api_utils->log_msg((const uint8_t *)msg, + strlen(msg), wrdp_log_level_warning, 0); + } + break; + } + cliprdr_send_data_request(i->context->clipboard); + return true; +} + +static bool +clip_api_handle_data_changed( + const wrdp_backend_clipbrd_fmts *fmts, void *backend_internals) +{ + rdp_internals *i = backend_internals; + my_rdp_clipboard *clip = i->context->clipboard; + uint8_t pos = 0; + + CLIPRDR_FORMAT *formats = 0; + CLIPRDR_FORMAT_LIST formatList; + + if (!fmts->count) + { + const char *msg = "received empty clipboard formats list"; + i->core->api_utils->log_msg((const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + return false; + } + formats = calloc(fmts->count, sizeof(CLIPRDR_FORMAT)); + if (!formats) + { + perror("calloc"); + return false; + } + + for (; pos < fmts->count; ++pos) + { + switch ((wrdp_enum_clip_format)fmts->formats[pos]) + { + case clip_format_raw: + { + formats[pos].formatId = CF_RAW; + } + break; + case clip_format_text: + { + formats[pos].formatId = CF_TEXT; + } + break; + case clip_format_unicode: + { + formats[pos].formatId = CF_UNICODETEXT; + } + break; + case clip_format_file_list: + { + formats[pos].formatId = CB_FORMAT_TEXTURILIST; + formats[pos].formatName + = "FileGroupDescriptorW"; + } + break; + default: + { + formats[pos].formatId = CF_RAW; + } + break; + } + } + formatList.common.msgFlags = CB_RESPONSE_OK; + formatList.numFormats = fmts->count; + formatList.formats = formats; + /* TODO: check return value of ClientFormatList */ + clip->clip_context->ClientFormatList(clip->clip_context, &formatList); + free(formats); + + return true; +} + +static bool +clip_api_handle_send_data( + const wrdp_backend_clipbrd_data *data, void *backend_internals) +{ + rdp_internals *i = backend_internals; + my_rdp_clipboard *clip = i->context->clipboard; + CLIPRDR_FORMAT_DATA_RESPONSE response; + + uint8_t type = data->data[0]; + memset(&response, 0, sizeof(CLIPRDR_FORMAT_DATA_RESPONSE)); + response.common.msgType = CB_FORMAT_DATA_RESPONSE; + response.common.msgFlags = CB_RESPONSE_FAIL; + response.common.dataLen = 0; + response.requestedFormatData = 0; +#ifdef DEBUG + { + const char *msg + = "rdp_module: clipboard: from client: raw clipboard" + " data:"; + i->core->api_utils->log_msg( + (const uint8_t *)msg, strlen(msg), wrdp_log_level_trace, 0); + i->core->api_utils->log_msg(data->data, data->size, + wrdp_log_level_trace, wrdp_log_flag_binary); + } +#endif + switch (type) + { + case clip_format_raw: + case clip_format_text: + { + if (data->size > 1) + { + response.common.msgFlags = CB_RESPONSE_OK; + response.common.dataLen = data->size - 1; + response.requestedFormatData = data->data + 1; + } + clip->clip_context->ClientFormatDataResponse( + clip->clip_context, &response); + return true; + } + break; + case clip_format_unicode: + { + uint8_t *data_buf = 0; + BYTE *msg_buf = 0; + iconv_t utf8_to_utf16 = iconv_open("UTF-16LE", "UTF-8"); + char *ptr_w, *ptr_r; + size_t text_len_utf16 = (data->size - 1) * 2, + text_len_utf8 = data->size - 1, + text_len_result = text_len_utf16; + data_buf = calloc(text_len_utf16, sizeof(uint8_t)); + if (!data_buf) + { + perror("calloc"); + return true; + } + ptr_w = (char *)data_buf; + ptr_r = (char *)(data->data + 1); + while (text_len_utf8 && text_len_utf16) + { + if (iconv(utf8_to_utf16, &ptr_r, &text_len_utf8, + &ptr_w, &text_len_utf16) + == (size_t)-1) + { + log_msg_info mi = {0}; + free(data_buf); + clip->clip_context + ->ClientFormatDataResponse( + clip->clip_context, &response); + mi.level = wrdp_log_level_warning; + mi.buf = (const uint8_t + *)"rdp_module: cliprdr: iconv: " + "failed to encode outgoing " + "buffer from utf8 to utf16le"; + mi.task_info = i->task_info; + i->core->api_utils->log_msg_ex(&mi); + iconv_close(utf8_to_utf16); + return true; + } + } +#ifdef DEBUG + if (text_len_utf8 && !text_len_utf16) + { + log_msg_info mi = {0}; + mi.level = wrdp_log_level_trace; + mi.buf = (const uint8_t + *)"rdp_module: cliprdr: iconv: not " + "sufficient space in output buffer"; + mi.task_info = i->task_info; + i->core->api_utils->log_msg_ex(&mi); + } +#endif + iconv(utf8_to_utf16, 0, 0, &ptr_w, &text_len_utf16); + text_len_result = text_len_result - text_len_utf16; + iconv_close(utf8_to_utf16); + if (text_len_result) + { + msg_buf = calloc(text_len_result, sizeof(BYTE)); + memcpy(msg_buf, data_buf, text_len_result); + free(data_buf); + response.common.msgFlags = CB_RESPONSE_OK; + response.common.dataLen = text_len_result; + response.requestedFormatData = msg_buf; + } +#ifdef DEBUG + if (msg_buf) + { + log_msg_info mi = {0}; + mi.level = wrdp_log_level_trace; + mi.buf = (const uint8_t + *)"rdp_module: clipboard: to server:" + " utf16 encoded buffer"; + mi.task_info = i->task_info; + i->core->api_utils->log_msg_ex(&mi); + mi.flags = wrdp_log_flag_binary; + mi.buf = msg_buf; + mi.buf_size = text_len_result; + i->core->api_utils->log_msg_ex(&mi); + } +#endif + clip->clip_context->ClientFormatDataResponse( + clip->clip_context, &response); + if (msg_buf) + { + free(msg_buf); + } + return true; + } + break; + case clip_format_file_list: + { + uint16_t file_count; + bool broken_msg = false; + int _i; + uint64_t data_offset = 3; + FILEDESCRIPTORW *file_list = 0; + BYTE *format_data = 0; + UINT32 format_data_length = 0; + memcpy(&file_count, data->data + 1, 2); + if (!file_count) + { + const char *msg = "rdp_module: clipboard: ft: " + "empty file_list received"; + i->core->api_utils->log_msg( + (const uint8_t *)msg, strlen(msg), + wrdp_log_level_warning, 0); + return true; + } + if (clip->client_filelist_cache) + { + free(clip->client_filelist_cache); + } + clip->client_filelist_cache + = calloc(file_count, sizeof(file_list_cache_entry)); + if (!clip->client_filelist_cache) + { + perror("calloc"); + return false; + } + file_list = calloc(file_count, sizeof(FILEDESCRIPTOR)); + if (!file_list) + { + perror("calloc"); + return false; + } + for (_i = 0; _i < file_count; ++_i) + { + uint16_t filename_len; + uint32_t file_id; + uint64_t file_size; + char *filename, *ptr_w, *ptr_r; + iconv_t utf8_to_utf16 + = iconv_open("UTF-16LE", "UTF-8"); + size_t text_len_utf16 = 260 * sizeof(WCHAR), + text_len_utf8; + memcpy( + &filename_len, data->data + data_offset, 2); + data_offset += 2; + if (data_offset >= data->size) + { + broken_msg = true; + break; + } + text_len_utf8 = filename_len; + memcpy(&file_id, data->data + data_offset, 4); + clip->client_filelist_cache[_i].file_id + = file_id; + data_offset += 4; + if (data_offset >= data->size) + { + broken_msg = true; + break; + } + memcpy(&file_size, data->data + data_offset, 8); + clip->client_filelist_cache[_i].file_size + = file_size; + data_offset += 8; + if ((data_offset >= data->size) + && (_i < file_count)) + { + broken_msg = true; + break; + } + filename = (char *)data->data + data_offset; + ptr_r = filename; + ptr_w = (char *)(file_list[_i].cFileName); + iconv(utf8_to_utf16, &ptr_r, &text_len_utf8, + &ptr_w, &text_len_utf16); + iconv(utf8_to_utf16, 0, 0, &ptr_w, + &text_len_utf16); + iconv_close(utf8_to_utf16); + data_offset += filename_len; + file_list[_i].dwFileAttributes + = FILE_ATTRIBUTE_NORMAL; + file_list[_i].dwFlags = FD_ATTRIBUTES + | FD_FILESIZE + | FD_SHOWPROGRESSUI; + memcpy(&(file_list[_i].nFileSizeLow), + &file_size, 4); + memcpy(&(file_list[_i].nFileSizeHigh), + ((uint8_t *)&file_size) + 4, 4); + } + if (broken_msg) + { + char msg[128]; + snprintf(msg, 127, + "rdp_module: cliprdr: error: wrong file" + " list message size: %d, file count: %d\n", + data->size, file_count); + i->core->api_utils->log_msg( + (const uint8_t *)msg, strlen(msg), + wrdp_log_level_error, 0); + } + else + { + UINT error = NO_ERROR; + error = cliprdr_serialize_file_list(file_list, + file_count, &format_data, + &format_data_length); + if (error) + { + char buf[128]; + snprintf(buf, 127, + "failed to serialize " + "CLIPRDR_FILELIST:" + " 0x%08X", + error); + i->core->api_utils->log_msg( + (const uint8_t *)buf, strlen(buf), + wrdp_log_level_error, 0); + } + } + free(file_list); + if (format_data_length) + { + response.common.msgFlags = CB_RESPONSE_OK; + response.common.dataLen = format_data_length; + response.requestedFormatData = format_data; + } + clip->clip_context->ClientFormatDataResponse( + clip->clip_context, &response); + + free(format_data); + return true; + } + break; + default: + { + response.common.msgFlags = CB_RESPONSE_FAIL; + response.common.dataLen = 0; + response.requestedFormatData = 0; + clip->clip_context->ClientFormatDataResponse( + clip->clip_context, &response); + return true; + } + break; + } + /* TODO: check return code from ClientFormatDataResponse */ + return true; +} + +void +register_clipboard(wrdp_backend_module *backend) +{ + backend->callbacks_clipbrd->request_data = clip_api_handle_request_data; + backend->callbacks_clipbrd->send_data = clip_api_handle_send_data; + backend->callbacks_clipbrd->data_changed = clip_api_handle_data_changed; +} |