blob: 4332d35f32be3b6c408f28c936603a1a31f9806e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "channels/cliprdr.h"
#include "client.h"
#include "common/clipboard.h"
#include "common/iconv.h"
#include "plugins/channels.h"
#include "rdp.h"
#include <freerdp/client/cliprdr.h>
#include <freerdp/event.h>
#include <freerdp/freerdp.h>
#include <guacamole/client.h>
#include <guacamole/stream.h>
#include <guacamole/user.h>
#include <winpr/wtsapi.h>
#include <winpr/wtypes.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
/**
* Sends a Format List PDU to the RDP server containing the formats of
* clipboard data supported. This PDU is used both to indicate the general
* clipboard formats supported at the begining of an RDP session and to inform
* the RDP server that new clipboard data is available within the listed
* formats.
*
* @param cliprdr
* The CliprdrClientContext structure used by FreeRDP to handle the
* CLIPRDR channel for the current RDP session.
*
* @return
* CHANNEL_RC_OK (zero) if the Format List PDU was sent successfully, an
* error code (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_send_format_list(CliprdrClientContext* cliprdr) {
/* This function is only invoked within FreeRDP-specific handlers for
* CLIPRDR, which are not assigned, and thus not callable, until after the
* relevant guac_rdp_clipboard structure is allocated and associated with
* the CliprdrClientContext */
guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
assert(clipboard != NULL);
/* We support CP-1252 and UTF-16 text */
CLIPRDR_FORMAT_LIST format_list = {
.formats = (CLIPRDR_FORMAT[]) {
{ .formatId = CF_TEXT },
{ .formatId = CF_UNICODETEXT }
},
.numFormats = 2
};
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Sending "
"format list");
return cliprdr->ClientFormatList(cliprdr, &format_list);
}
/**
* Sends a Clipboard Capabilities PDU to the RDP server describing the features
* of the CLIPRDR channel that are supported by the client.
*
* @param cliprdr
* The CliprdrClientContext structure used by FreeRDP to handle the
* CLIPRDR channel for the current RDP session.
*
* @return
* CHANNEL_RC_OK (zero) if the Clipboard Capabilities PDU was sent
* successfully, an error code (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_send_capabilities(CliprdrClientContext* cliprdr) {
CLIPRDR_GENERAL_CAPABILITY_SET cap_set = {
.capabilitySetType = CB_CAPSTYPE_GENERAL, /* CLIPRDR specification requires that this is CB_CAPSTYPE_GENERAL, the only defined set type */
.capabilitySetLength = 12, /* The size of the capability set within the PDU - for CB_CAPSTYPE_GENERAL, this is ALWAYS 12 bytes */
.version = CB_CAPS_VERSION_2, /* The version of the CLIPRDR specification supported */
.generalFlags = CB_USE_LONG_FORMAT_NAMES /* Bitwise OR of all supported feature flags */
};
CLIPRDR_CAPABILITIES caps = {
.cCapabilitiesSets = 1,
.capabilitySets = (CLIPRDR_CAPABILITY_SET*) &cap_set
};
return cliprdr->ClientCapabilities(cliprdr, &caps);
}
/**
* Callback invoked by the FreeRDP CLIPRDR plugin for received Monitor Ready
* PDUs. The Monitor Ready PDU is sent by the RDP server only during
* initialization of the CLIPRDR channel. It is part of the CLIPRDR channel
* handshake and indicates that the RDP server's handling of clipboard
* redirection is ready to proceed.
*
* @param cliprdr
* The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
* channel for the current RDP session.
*
* @param monitor_ready
* The CLIPRDR_MONITOR_READY structure representing the Monitor Ready PDU
* that was received.
*
* @return
* CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
* (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_monitor_ready(CliprdrClientContext* cliprdr,
const CLIPRDR_MONITOR_READY* monitor_ready) {
/* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
* callable, until after the relevant guac_rdp_clipboard structure is
* allocated and associated with the CliprdrClientContext */
guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
assert(clipboard != NULL);
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
"monitor ready.");
/* Respond with capabilities ... */
int status = guac_rdp_cliprdr_send_capabilities(cliprdr);
if (status != CHANNEL_RC_OK)
return status;
/* ... and supported format list */
return guac_rdp_cliprdr_send_format_list(cliprdr);
}
/**
* Sends a Format Data Request PDU to the RDP server, requesting that available
* clipboard data be sent to the client in the specified format. This PDU is
* sent when the server indicates that clipboard data is available via a Format
* List PDU.
*
* @param client
* The guac_client associated with the current RDP session.
*
* @param format
* The clipboard format to request. This format must be one of the
* documented values used by the CLIPRDR channel for clipboard format IDs.
*
* @return
* CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
* (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_send_format_data_request(
CliprdrClientContext* cliprdr, UINT32 format) {
/* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
* callable, until after the relevant guac_rdp_clipboard structure is
* allocated and associated with the CliprdrClientContext */
guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
assert(clipboard != NULL);
/* Create new data request */
CLIPRDR_FORMAT_DATA_REQUEST data_request = {
.requestedFormatId = format
};
/* Note the format we've requested for reference later when the requested
* data is received via a Format Data Response PDU */
clipboard->requested_format = format;
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Sending "
"format data request.");
/* Send request */
return cliprdr->ClientFormatDataRequest(cliprdr, &data_request);
}
/**
* Returns whether the given Format List PDU indicates support for the given
* clipboard format.
*
* @param format_list
* The CLIPRDR_FORMAT_LIST structure representing the Format List PDU
* being tested.
*
* @param format_id
* The ID of the clipboard format to test, such as CF_TEXT or
* CF_UNICODETEXT.
*
* @return
* Non-zero if the given Format List PDU indicates support for the given
* clipboard format, zero otherwise.
*/
static int guac_rdp_cliprdr_format_supported(const CLIPRDR_FORMAT_LIST* format_list,
UINT format_id) {
/* Search format list for matching ID */
for (int i = 0; i < format_list->numFormats; i++) {
if (format_list->formats[i].formatId == format_id)
return 1;
}
/* If no matching ID, format is not supported */
return 0;
}
/**
* Callback invoked by the FreeRDP CLIPRDR plugin for received Format List
* PDUs. The Format List PDU is sent by the RDP server to indicate that new
* clipboard data has been copied and is available for retrieval in the formats
* listed. A client wishing to retrieve that data responds with a Format Data
* Request PDU.
*
* @param cliprdr
* The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
* channel for the current RDP session.
*
* @param format_list
* The CLIPRDR_FORMAT_LIST structure representing the Format List PDU that
* was received.
*
* @return
* CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
* (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_format_list(CliprdrClientContext* cliprdr,
const CLIPRDR_FORMAT_LIST* format_list) {
/* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
* callable, until after the relevant guac_rdp_clipboard structure is
* allocated and associated with the CliprdrClientContext */
guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
assert(clipboard != NULL);
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
"format list.");
CLIPRDR_FORMAT_LIST_RESPONSE format_list_response = {
.msgFlags = CB_RESPONSE_OK
};
/* Report successful processing of format list */
cliprdr->ClientFormatListResponse(cliprdr, &format_list_response);
/* Prefer Unicode (in this case, UTF-16) */
if (guac_rdp_cliprdr_format_supported(format_list, CF_UNICODETEXT))
return guac_rdp_cliprdr_send_format_data_request(cliprdr, CF_UNICODETEXT);
/* Use Windows' CP-1252 if Unicode unavailable */
if (guac_rdp_cliprdr_format_supported(format_list, CF_TEXT))
return guac_rdp_cliprdr_send_format_data_request(cliprdr, CF_TEXT);
/* Ignore any unsupported data */
guac_client_log(clipboard->client, GUAC_LOG_DEBUG, "Ignoring unsupported "
"clipboard data. Only Unicode and text clipboard formats are "
"currently supported.");
return CHANNEL_RC_OK;
}
/**
* Callback invoked by the FreeRDP CLIPRDR plugin for received Format Data
* Request PDUs. The Format Data Request PDU is sent by the RDP server when
* requesting that clipboard data be sent, in response to a received Format
* List PDU. The client is required to respond with a Format Data Response PDU
* containing the requested data.
*
* @param cliprdr
* The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
* channel for the current RDP session.
*
* @param format_data_request
* The CLIPRDR_FORMAT_DATA_REQUEST structure representing the Format Data
* Request PDU that was received.
*
* @return
* CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
* (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_format_data_request(CliprdrClientContext* cliprdr,
const CLIPRDR_FORMAT_DATA_REQUEST* format_data_request) {
/* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
* callable, until after the relevant guac_rdp_clipboard structure is
* allocated and associated with the CliprdrClientContext */
guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
assert(clipboard != NULL);
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
"format data request.");
guac_iconv_write* writer;
const char* input = clipboard->clipboard->buffer;
char* output = malloc(GUAC_RDP_CLIPBOARD_MAX_LENGTH);
/* Map requested clipboard format to a guac_iconv writer */
switch (format_data_request->requestedFormatId) {
case CF_TEXT:
writer = GUAC_WRITE_CP1252;
break;
case CF_UNICODETEXT:
writer = GUAC_WRITE_UTF16;
break;
/* Warn if clipboard data cannot be sent as intended due to a violation
* of the CLIPRDR spec */
default:
guac_client_log(clipboard->client, GUAC_LOG_WARNING, "Received "
"clipboard data cannot be sent to the RDP server because "
"the RDP server has requested a clipboard format which "
"was not declared as available. This violates the "
"specification for the CLIPRDR channel.");
free(output);
return CHANNEL_RC_OK;
}
/* Send received clipboard data to the RDP server in the format
* requested */
BYTE* start = (BYTE*) output;
guac_iconv(GUAC_READ_UTF8, &input, clipboard->clipboard->length,
writer, &output, GUAC_RDP_CLIPBOARD_MAX_LENGTH);
CLIPRDR_FORMAT_DATA_RESPONSE data_response = {
.requestedFormatData = (BYTE*) start,
.dataLen = ((BYTE*) output) - start,
.msgFlags = CB_RESPONSE_OK
};
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Sending "
"format data response.");
return cliprdr->ClientFormatDataResponse(cliprdr, &data_response);
}
/**
* Callback invoked by the FreeRDP CLIPRDR plugin for received Format Data
* Response PDUs. The Format Data Response PDU is sent by the RDP server when
* fullfilling a request for clipboard data received via a Format Data Request
* PDU.
*
* @param cliprdr
* The CliprdrClientContext structure used by FreeRDP to handle the CLIPRDR
* channel for the current RDP session.
*
* @param format_data_response
* The CLIPRDR_FORMAT_DATA_RESPONSE structure representing the Format Data
* Response PDU that was received.
*
* @return
* CHANNEL_RC_OK (zero) if the PDU was handled successfully, an error code
* (non-zero) otherwise.
*/
static UINT guac_rdp_cliprdr_format_data_response(CliprdrClientContext* cliprdr,
const CLIPRDR_FORMAT_DATA_RESPONSE* format_data_response) {
/* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
* callable, until after the relevant guac_rdp_clipboard structure is
* allocated and associated with the CliprdrClientContext */
guac_rdp_clipboard* clipboard = (guac_rdp_clipboard*) cliprdr->custom;
assert(clipboard != NULL);
guac_client_log(clipboard->client, GUAC_LOG_TRACE, "CLIPRDR: Received "
"format data response.");
char received_data[GUAC_RDP_CLIPBOARD_MAX_LENGTH];
guac_iconv_read* reader;
const char* input = (char*) format_data_response->requestedFormatData;
char* output = received_data;
/* Find correct source encoding */
switch (clipboard->requested_format) {
/* Non-Unicode (Windows CP-1252) */
case CF_TEXT:
reader = GUAC_READ_CP1252;
break;
/* Unicode (UTF-16) */
case CF_UNICODETEXT:
reader = GUAC_READ_UTF16;
break;
/* If the format ID stored within the guac_rdp_clipboard structure is actually
* not supported here, then something has been implemented incorrectly.
* Either incorrect values are (somehow) being stored, or support for
* the format indicated by that value is incomplete and must be added
* here. The values which may be stored within requested_format are
* completely within our control. */
default:
guac_client_log(clipboard->client, GUAC_LOG_DEBUG, "Requested "
"clipboard data in unsupported format (0x%X).",
clipboard->requested_format);
return CHANNEL_RC_OK;
}
/* Convert, store, and forward the clipboard data received from RDP
* server */
if (guac_iconv(reader, &input, format_data_response->dataLen,
GUAC_WRITE_UTF8, &output, sizeof(received_data))) {
int length = strnlen(received_data, sizeof(received_data));
guac_common_clipboard_reset(clipboard->clipboard, "text/plain");
guac_common_clipboard_append(clipboard->clipboard, received_data, length);
guac_common_clipboard_send(clipboard->clipboard, clipboard->client);
}
return CHANNEL_RC_OK;
}
/**
* Callback which associates handlers specific to Guacamole with the
* CliprdrClientContext instance allocated by FreeRDP to deal with received
* CLIPRDR (clipboard redirection) messages.
*
* This function is called whenever a channel connects via the PubSub event
* system within FreeRDP, but only has any effect if the connected channel is
* the CLIPRDR channel. This specific callback is registered with the PubSub
* system of the relevant rdpContext when guac_rdp_clipboard_load_plugin() is
* called.
*
* @param context
* The rdpContext associated with the active RDP session.
*
* @param e
* Event-specific arguments, mainly the name of the channel, and a
* reference to the associated plugin loaded for that channel by FreeRDP.
*/
static void guac_rdp_cliprdr_channel_connected(rdpContext* context,
ChannelConnectedEventArgs* e) {
guac_client* client = ((rdp_freerdp_context*) context)->client;
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
guac_rdp_clipboard* clipboard = rdp_client->clipboard;
/* FreeRDP-specific handlers for CLIPRDR are not assigned, and thus not
* callable, until after the relevant guac_rdp_clipboard structure is
* allocated and associated with the guac_rdp_client */
assert(clipboard != NULL);
/* Ignore connection event if it's not for the CLIPRDR channel */
if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) != 0)
return;
/* The structure pointed to by pInterface is guaranteed to be a
* CliprdrClientContext if the channel is CLIPRDR */
CliprdrClientContext* cliprdr = (CliprdrClientContext*) e->pInterface;
/* Associate FreeRDP CLIPRDR context and its Guacamole counterpart with
* eachother */
cliprdr->custom = clipboard;
clipboard->cliprdr = cliprdr;
cliprdr->MonitorReady = (pcCliprdrMonitorReady) guac_rdp_cliprdr_monitor_ready;
cliprdr->ServerFormatList = (pcCliprdrServerFormatList) guac_rdp_cliprdr_format_list;
cliprdr->ServerFormatDataRequest = (pcCliprdrServerFormatDataRequest) guac_rdp_cliprdr_format_data_request;
cliprdr->ServerFormatDataResponse = (pcCliprdrServerFormatDataResponse) guac_rdp_cliprdr_format_data_response;
guac_client_log(client, GUAC_LOG_DEBUG, "CLIPRDR (clipboard redirection) "
"channel connected.");
}
guac_rdp_clipboard* guac_rdp_clipboard_alloc(guac_client* client) {
/* Allocate clipboard and underlying storage */
guac_rdp_clipboard* clipboard = calloc(1, sizeof(guac_rdp_clipboard));
clipboard->client = client;
clipboard->clipboard = guac_common_clipboard_alloc(GUAC_RDP_CLIPBOARD_MAX_LENGTH);
clipboard->requested_format = CF_TEXT;
return clipboard;
}
void guac_rdp_clipboard_load_plugin(guac_rdp_clipboard* clipboard,
rdpContext* context) {
/* Attempt to load FreeRDP support for the CLIPRDR channel */
if (guac_freerdp_channels_load_plugin(context, "cliprdr", NULL)) {
guac_client_log(clipboard->client, GUAC_LOG_WARNING,
"Support for the CLIPRDR channel (clipboard redirection) "
"could not be loaded. This support normally takes the form of "
"a plugin which is built into FreeRDP. Lacking this support, "
"clipboard will not work.");
return;
}
/* Complete RDP side of initialization when channel is connected */
PubSub_SubscribeChannelConnected(context->pubSub,
(pChannelConnectedEventHandler) guac_rdp_cliprdr_channel_connected);
guac_client_log(clipboard->client, GUAC_LOG_DEBUG, "Support for CLIPRDR "
"(clipboard redirection) registered. Awaiting channel "
"connection.");
}
void guac_rdp_clipboard_free(guac_rdp_clipboard* clipboard) {
/* Do nothing if the clipboard is not actually allocated */
if (clipboard == NULL)
return;
/* Free clipboard and underlying storage */
guac_common_clipboard_free(clipboard->clipboard);
free(clipboard);
}
int guac_rdp_clipboard_handler(guac_user* user, guac_stream* stream,
char* mimetype) {
guac_client* client = user->client;
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
/* Ignore stream creation if no clipboard structure is available to handle
* received data */
guac_rdp_clipboard* clipboard = rdp_client->clipboard;
if (clipboard == NULL)
return 0;
/* Handle any future "blob" and "end" instructions for this stream with
* handlers that are aware of the RDP clipboard state */
stream->blob_handler = guac_rdp_clipboard_blob_handler;
stream->end_handler = guac_rdp_clipboard_end_handler;
/* Clear any current contents, assigning the mimetype the data which will
* be received */
guac_common_clipboard_reset(clipboard->clipboard, mimetype);
return 0;
}
int guac_rdp_clipboard_blob_handler(guac_user* user, guac_stream* stream,
void* data, int length) {
guac_client* client = user->client;
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
/* Ignore received data if no clipboard structure is available to handle
* that data */
guac_rdp_clipboard* clipboard = rdp_client->clipboard;
if (clipboard == NULL)
return 0;
/* Append received data to current clipboard contents */
guac_common_clipboard_append(clipboard->clipboard, (char*) data, length);
return 0;
}
int guac_rdp_clipboard_end_handler(guac_user* user, guac_stream* stream) {
guac_client* client = user->client;
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
/* Ignore end of stream if no clipboard structure is available to handle
* the data that was received */
guac_rdp_clipboard* clipboard = rdp_client->clipboard;
if (clipboard == NULL)
return 0;
/* Terminate clipboard data with NULL */
guac_common_clipboard_append(clipboard->clipboard, "", 1);
/* Notify RDP server of new data, if connected */
if (clipboard->cliprdr != NULL) {
guac_client_log(client, GUAC_LOG_DEBUG, "Clipboard data received. "
"Reporting availability of clipboard data to RDP server.");
guac_rdp_cliprdr_send_format_list(clipboard->cliprdr);
}
else
guac_client_log(client, GUAC_LOG_DEBUG, "Clipboard data has been "
"received, but cannot be sent to the RDP server because the "
"CLIPRDR channel is not yet connected.");
return 0;
}