diff --git a/configure.ac b/configure.ac index edd49b84..446a36e5 100644 --- a/configure.ac +++ b/configure.ac @@ -436,6 +436,7 @@ then have_freerdp=yes legacy_freerdp_extensions=no rdpsettings_interface=unknown + rdpsettings_audiocapture=yes rdpsettings_audioplayback=yes rdpsettings_deviceredirection=yes freerdp_interface=unknown @@ -578,6 +579,15 @@ then [#include ]) fi +# Availability of ADDIN_ARGV structure for configuring plugins +if test "x${have_freerdp}" = "xyes" +then + AC_CHECK_TYPE([ADDIN_ARGV], + [AC_DEFINE([HAVE_ADDIN_ARGV],, + [Whether the ADDIN_ARGV type is available])],, + [#include ]) +fi + # # FreeRDP: WinPR # @@ -686,6 +696,11 @@ then [rdpsettings_audioplayback=no], [[#include ]]) + # Legacy interface may not have AudioCapture settings + AC_CHECK_MEMBERS([rdpSettings.audio_capture],, + [rdpsettings_audiocapture=no], + [[#include ]]) + # Legacy interface may not have DeviceRedirection settings AC_CHECK_MEMBERS([rdpSettings.device_redirection],, [rdpsettings_deviceredirection=no], @@ -707,6 +722,12 @@ if test "x${have_freerdp}" = "xyes" -a "x${rdpsettings_audioplayback}" = "xyes"; [Whether the rdpSettings structure has AudioPlayback settings]) fi +# Activate audio capture settings if present +if test "x${have_freerdp}" = "xyes" -a "x${rdpsettings_audiocapture}" = "xyes"; then + AC_DEFINE([HAVE_RDPSETTINGS_AUDIOCAPTURE],, + [Whether the rdpSettings structure has AudioCapture settings]) +fi + # Activate device redirection settings if present if test "x${have_freerdp}" = "xyes" -a "x${rdpsettings_deviceredirection}" = "xyes"; then AC_DEFINE([HAVE_RDPSETTINGS_DEVICEREDIRECTION],, @@ -752,6 +773,32 @@ then [Whether the legacy rdpBitmap API was found])]) fi +# +# FreeRDP: IWTSVirtualChannelCallback +# + +if test "x${have_freerdp}" = "xyes" +then + AC_MSG_CHECKING([whether IWTSVirtualChannelCallback.OnDataReceived() uses a wStream]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#include + #include + #include + int __data_received( + IWTSVirtualChannelCallback* channel_callback, + wStream* stream); + IWTSVirtualChannelCallback cb = { + .OnDataReceived = __data_received + }; + int main() { + return + cb.OnDataReceived(NULL, NULL); + }]])], + [AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no]) + AC_DEFINE([LEGACY_IWTSVIRTUALCHANNELCALLBACK],, + [Whether the legacy IWTSVirtualChannelCallback API was found])]) +fi + # # FreeRDP: Decompression function variants # diff --git a/src/libguac/guacamole/protocol-types.h b/src/libguac/guacamole/protocol-types.h index ee9c6683..ea76910d 100644 --- a/src/libguac/guacamole/protocol-types.h +++ b/src/libguac/guacamole/protocol-types.h @@ -87,6 +87,12 @@ typedef enum guac_protocol_status { */ GUAC_PROTOCOL_STATUS_RESOURCE_CONFLICT = 0x205, + /** + * The operation could not be performed as the requested resource is now + * closed. + */ + GUAC_PROTOCOL_STATUS_RESOURCE_CLOSED = 0x0206, + /** * The operation could not be performed because bad parameters were * given. diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index 91593cbb..3669b3dd 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -24,8 +24,11 @@ lib_LTLIBRARIES = libguac-client-rdp.la libguac_client_rdp_la_SOURCES = \ _generated_keymaps.c \ + audio_input.c \ client.c \ + dvc.c \ input.c \ + ptr_string.c \ rdp.c \ rdp_bitmap.c \ rdp_cliprdr.c \ @@ -44,6 +47,12 @@ libguac_client_rdp_la_SOURCES = \ unicode.c \ user.c +guacai_sources = \ + audio_input.c \ + guac_ai/ai_messages.c \ + guac_ai/ai_service.c \ + ptr_string.c + guacsvc_sources = \ guac_svc/svc_service.c \ rdp_svc.c @@ -68,6 +77,8 @@ guacdr_sources = \ noinst_HEADERS = \ compat/client-cliprdr.h \ compat/rail.h \ + guac_ai/ai_messages.h \ + guac_ai/ai_service.h \ guac_rdpdr/rdpdr_fs_messages.h \ guac_rdpdr/rdpdr_fs_messages_dir_info.h \ guac_rdpdr/rdpdr_fs_messages_file_info.h \ @@ -79,8 +90,11 @@ noinst_HEADERS = \ guac_rdpsnd/rdpsnd_messages.h \ guac_rdpsnd/rdpsnd_service.h \ guac_svc/svc_service.h \ + audio_input.h \ client.h \ + dvc.h \ input.h \ + ptr_string.h \ rdp.h \ rdp_bitmap.h \ rdp_cliprdr.h \ @@ -104,6 +118,7 @@ noinst_HEADERS = \ if ! ENABLE_WINPR noinst_HEADERS += compat/winpr-stream.h compat/winpr-wtypes.h libguac_client_rdp_la_SOURCES += compat/winpr-stream.c +guacai_sources += compat/winpr-stream.c guacsvc_sources += compat/winpr-stream.c guacsnd_sources += compat/winpr-stream.c guacdr_sources += compat/winpr-stream.c @@ -148,6 +163,25 @@ guacdr_libadd = \ @COMMON_LTLIB@ \ @LIBGUAC_LTLIB@ +# +# Audio Input +# + +guacai_cflags = \ + -Werror -Wall -Iinclude \ + @COMMON_INCLUDE@ \ + @COMMON_SSH_INCLUDE@ \ + @LIBGUAC_INCLUDE@ + +guacai_ldflags = \ + -module -avoid-version -shared \ + @PTHREAD_LIBS@ \ + @RDP_LIBS@ + +guacai_libadd = \ + @COMMON_LTLIB@ \ + @LIBGUAC_LTLIB@ + # # RDPSND # @@ -224,10 +258,16 @@ if LEGACY_FREERDP_EXTENSIONS # FreeRDP 1.0-style extensions freerdp_LTLIBRARIES = \ + guacai.la \ guacdr.la \ guacsnd.la \ guacsvc.la +guacai_la_SOURCES = ${guacai_sources} +guacai_la_CFLAGS = ${guacai_cflags} +guacai_la_LDFLAGS = ${guacai_ldflags} +guacai_la_LIBADD = ${guacai_libadd} + guacdr_la_SOURCES = ${guacdr_sources} guacdr_la_CFLAGS = ${guacdr_cflags} guacdr_la_LDFLAGS = ${guacdr_ldflags} @@ -247,10 +287,16 @@ else # FreeRDP 1.1 (and hopefully onward) extensions freerdp_LTLIBRARIES = \ + guacai-client.la \ guacdr-client.la \ guacsnd-client.la \ guacsvc-client.la +guacai_client_la_SOURCES = ${guacai_sources} +guacai_client_la_CFLAGS = ${guacai_cflags} +guacai_client_la_LDFLAGS = ${guacai_ldflags} +guacai_client_la_LIBADD = ${guacai_libadd} + guacdr_client_la_SOURCES = ${guacdr_sources} guacdr_client_la_CFLAGS = ${guacdr_cflags} guacdr_client_la_LDFLAGS = ${guacdr_ldflags} diff --git a/src/protocols/rdp/audio_input.c b/src/protocols/rdp/audio_input.c new file mode 100644 index 00000000..dc078dc0 --- /dev/null +++ b/src/protocols/rdp/audio_input.c @@ -0,0 +1,484 @@ +/* + * 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 "config.h" +#include "audio_input.h" +#include "dvc.h" +#include "ptr_string.h" +#include "rdp.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Parses the given raw audio mimetype, producing the corresponding rate, + * number of channels, and bytes per sample. + * + * @param mimetype + * The raw auduio mimetype to parse. + * + * @param rate + * A pointer to an int where the sample rate for the PCM format described + * by the given mimetype should be stored. + * + * @param channels + * A pointer to an int where the number of channels used by the PCM format + * described by the given mimetype should be stored. + * + * @param bps + * A pointer to an int where the number of bytes used the PCM format for + * each sample (independent of number of channels) described by the given + * mimetype should be stored. + * + * @return + * Zero if the given mimetype is a raw audio mimetype and has been parsed + * successfully, non-zero otherwise. + */ +static int guac_rdp_audio_parse_mimetype(const char* mimetype, + int* rate, int* channels, int* bps) { + + int parsed_rate = -1; + int parsed_channels = 1; + int parsed_bps; + + /* PCM audio with one byte per sample */ + if (strncmp(mimetype, "audio/L8;", 9) == 0) { + mimetype += 8; /* Advance to semicolon ONLY */ + parsed_bps = 1; + } + + /* PCM audio with two bytes per sample */ + else if (strncmp(mimetype, "audio/L16;", 10) == 0) { + mimetype += 9; /* Advance to semicolon ONLY */ + parsed_bps = 2; + } + + /* Unsupported mimetype */ + else + return 1; + + /* Parse each parameter name/value pair within the mimetype */ + do { + + /* Advance to first character of parameter (current is either a + * semicolon or a comma) */ + mimetype++; + + /* Parse number of channels */ + if (strncmp(mimetype, "channels=", 9) == 0) { + + mimetype += 9; + parsed_channels = strtol(mimetype, (char**) &mimetype, 10); + + /* Fail if value invalid / out of range */ + if (errno == EINVAL || errno == ERANGE) + return 1; + + } + + /* Parse number of rate */ + else if (strncmp(mimetype, "rate=", 5) == 0) { + + mimetype += 5; + parsed_rate = strtol(mimetype, (char**) &mimetype, 10); + + /* Fail if value invalid / out of range */ + if (errno == EINVAL || errno == ERANGE) + return 1; + + } + + /* Advance to next parameter */ + mimetype = strchr(mimetype, ','); + + } while (mimetype != NULL); + + /* Mimetype is invalid if rate was not specified */ + if (parsed_rate == -1) + return 1; + + /* Parse success */ + *rate = parsed_rate; + *channels = parsed_channels; + *bps = parsed_bps; + + return 0; + +} + +int guac_rdp_audio_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + guac_client* client = user->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + int rate; + int channels; + int bps; + + /* Parse mimetype, abort on parse error */ + if (guac_rdp_audio_parse_mimetype(mimetype, &rate, &channels, &bps)) { + guac_user_log(user, GUAC_LOG_WARNING, "Denying user audio stream with " + "unsupported mimetype: \"%s\"", mimetype); + guac_protocol_send_ack(user->socket, stream, "Unsupported audio " + "mimetype", GUAC_PROTOCOL_STATUS_CLIENT_BAD_TYPE); + return 0; + } + + /* Init stream data */ + stream->blob_handler = guac_rdp_audio_blob_handler; + stream->end_handler = guac_rdp_audio_end_handler; + + /* Associate stream with audio buffer */ + guac_rdp_audio_buffer_set_stream(rdp_client->audio_input, user, stream, + rate, channels, bps); + + return 0; + +} + +int guac_rdp_audio_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; + + /* Write blob to audio stream, buffering if necessary */ + guac_rdp_audio_buffer_write(rdp_client->audio_input, data, length); + + return 0; + +} + +int guac_rdp_audio_end_handler(guac_user* user, guac_stream* stream) { + + /* Ignore - the AUDIO_INPUT channel will simply not receive anything */ + return 0; + +} + +void guac_rdp_audio_load_plugin(rdpContext* context, guac_rdp_dvc_list* list) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + + /* Add "AUDIO_INPUT" channel */ + guac_rdp_dvc_list_add(list, "guacai", guac_rdp_ptr_to_string(client), NULL); + +} + +guac_rdp_audio_buffer* guac_rdp_audio_buffer_alloc() { + guac_rdp_audio_buffer* buffer = calloc(1, sizeof(guac_rdp_audio_buffer)); + pthread_mutex_init(&(buffer->lock), NULL); + return buffer; +} + +/** + * Sends an "ack" instruction over the socket associated with the Guacamole + * stream over which audio data is being received. The "ack" instruction will + * only be sent if the Guacamole audio stream has been established (through + * receipt of an "audio" instruction), is still open (has not received an "end" + * instruction nor been associated with an "ack" having an error code), and is + * associated with an active RDP AUDIO_INPUT channel. + * + * @param audio_buffer + * The audio buffer associated with the guac_stream for which the "ack" + * instruction should be sent, if any. If there is no associated + * guac_stream, this function has no effect. + * + * @param message + * An arbitrary human-readable message to send along with the "ack". + * + * @param status + * The Guacamole protocol status code to send with the "ack". This should + * be GUAC_PROTOCOL_STATUS_SUCCESS if the audio stream has been set up + * successfully or GUAC_PROTOCOL_STATUS_RESOURCE_CLOSED if the audio stream + * has been closed (but may usable again if reopened). + */ +static void guac_rdp_audio_buffer_ack(guac_rdp_audio_buffer* audio_buffer, + const char* message, guac_protocol_status status) { + + guac_user* user = audio_buffer->user; + guac_stream* stream = audio_buffer->stream; + + /* Do not send ack unless both sides of the audio stream are ready */ + if (user == NULL || stream == NULL || audio_buffer->packet == NULL) + return; + + /* Send ack instruction */ + guac_protocol_send_ack(user->socket, stream, message, status); + guac_socket_flush(user->socket); + +} + +void guac_rdp_audio_buffer_set_stream(guac_rdp_audio_buffer* audio_buffer, + guac_user* user, guac_stream* stream, int rate, int channels, int bps) { + + pthread_mutex_lock(&(audio_buffer->lock)); + + /* Associate received stream */ + audio_buffer->user = user; + audio_buffer->stream = stream; + audio_buffer->in_format.rate = rate; + audio_buffer->in_format.channels = channels; + audio_buffer->in_format.bps = bps; + + /* Acknowledge stream creation (if buffer is ready to receive) */ + guac_rdp_audio_buffer_ack(audio_buffer, + "OK", GUAC_PROTOCOL_STATUS_SUCCESS); + + guac_user_log(user, GUAC_LOG_DEBUG, "User is requesting to provide audio " + "input as %i-channel, %i Hz PCM audio at %i bytes/sample.", + audio_buffer->in_format.channels, + audio_buffer->in_format.rate, + audio_buffer->in_format.bps); + + pthread_mutex_unlock(&(audio_buffer->lock)); + +} + +void guac_rdp_audio_buffer_set_output(guac_rdp_audio_buffer* audio_buffer, + int rate, int channels, int bps) { + + pthread_mutex_lock(&(audio_buffer->lock)); + + /* Set output format */ + audio_buffer->out_format.rate = rate; + audio_buffer->out_format.channels = channels; + audio_buffer->out_format.bps = bps; + + pthread_mutex_unlock(&(audio_buffer->lock)); + +} + +void guac_rdp_audio_buffer_begin(guac_rdp_audio_buffer* audio_buffer, + int packet_frames, guac_rdp_audio_buffer_flush_handler* flush_handler, + void* data) { + + pthread_mutex_lock(&(audio_buffer->lock)); + + /* Reset buffer state to provided values */ + audio_buffer->bytes_written = 0; + audio_buffer->flush_handler = flush_handler; + audio_buffer->data = data; + + /* Calculate size of each packet in bytes */ + audio_buffer->packet_size = packet_frames + * audio_buffer->out_format.channels + * audio_buffer->out_format.bps; + + /* Allocate new buffer */ + free(audio_buffer->packet); + audio_buffer->packet = malloc(audio_buffer->packet_size); + + /* Acknowledge stream creation (if stream is ready to receive) */ + guac_rdp_audio_buffer_ack(audio_buffer, + "OK", GUAC_PROTOCOL_STATUS_SUCCESS); + + pthread_mutex_unlock(&(audio_buffer->lock)); + +} + +/** + * Reads a single sample from the given buffer of data, using the input + * format defined within the given audio buffer. Each read sample is + * translated to a signed 16-bit value, even if the input format is 8-bit. + * The offset into the given buffer will be determined according to the + * input and output formats, the number of bytes sent thus far, and the + * number of bytes received (excluding the contents of the buffer). + * + * @param audio_buffer + * The audio buffer dictating the format of the given data buffer, as + * well as the offset from which the sample should be read. + * + * @param buffer + * The buffer of raw PCM audio data from which the sample should be read. + * This buffer MUST NOT contain data already taken into account by the + * audio buffer's total_bytes_received counter. + * + * @param length + * The number of bytes within the given buffer of PCM data. + * + * @param sample + * A pointer to the int16_t in which the read sample should be stored. If + * the input format is 8-bit, the sample will be shifted left by 8 bits + * to produce a 16-bit sample. + * + * @return + * Non-zero if a sample was successfully read, zero if no data remains + * within the given buffer that has not already been mapped to an + * output sample. + */ +static int guac_rdp_audio_buffer_read_sample( + guac_rdp_audio_buffer* audio_buffer, const char* buffer, int length, + int16_t* sample) { + + int in_bps = audio_buffer->in_format.bps; + int in_rate = audio_buffer->in_format.rate; + int in_channels = audio_buffer->in_format.channels; + + int out_bps = audio_buffer->out_format.bps; + int out_rate = audio_buffer->out_format.rate; + int out_channels = audio_buffer->out_format.channels; + + /* Calculate position within audio output */ + int current_sample = audio_buffer->total_bytes_sent / out_bps; + int current_frame = current_sample / out_channels; + int current_channel = current_sample % out_channels; + + /* Map output channel to input channel */ + if (current_channel >= in_channels) + current_channel = in_channels - 1; + + /* Transform output position to input position */ + current_frame = (int) current_frame * ((double) in_rate / out_rate); + current_sample = current_frame * in_channels + current_channel; + + /* Calculate offset within given buffer from absolute input position */ + int offset = current_sample * in_bps + - audio_buffer->total_bytes_received; + + /* It should be impossible for the offset to ever go negative */ + assert(offset >= 0); + + /* Apply offset to buffer */ + buffer += offset; + length -= offset; + + /* Read only if sufficient data is present in the given buffer */ + if (length < in_bps) + return 0; + + /* Simply read sample directly if input is 16-bit */ + if (in_bps == 2) { + *sample = *((int16_t*) buffer); + return 1; + } + + /* Translate to 16-bit if input is 8-bit */ + if (in_bps == 1) { + *sample = *buffer << 8; + return 1; + } + + /* Accepted audio formats are required to be 8- or 16-bit */ + return 0; + +} + +void guac_rdp_audio_buffer_write(guac_rdp_audio_buffer* audio_buffer, + char* buffer, int length) { + + int16_t sample; + + pthread_mutex_lock(&(audio_buffer->lock)); + + /* Ignore packet if there is no buffer */ + if (audio_buffer->packet_size == 0 || audio_buffer->packet == NULL) { + pthread_mutex_unlock(&(audio_buffer->lock)); + return; + } + + int out_bps = audio_buffer->out_format.bps; + + /* Continuously write packets until no data remains */ + while (guac_rdp_audio_buffer_read_sample(audio_buffer, + buffer, length, &sample) > 0) { + + char* current = audio_buffer->packet + audio_buffer->bytes_written; + + /* Store as 16-bit or 8-bit, depending on output format */ + if (out_bps == 2) + *((int16_t*) current) = sample; + else if (out_bps == 1) + *current = sample >> 8; + + /* Accepted audio formats are required to be 8- or 16-bit */ + else + assert(0); + + /* Update byte counters */ + audio_buffer->bytes_written += out_bps; + audio_buffer->total_bytes_sent += out_bps; + + /* Invoke flush handler if full */ + if (audio_buffer->bytes_written == audio_buffer->packet_size) { + + /* Only actually invoke if defined */ + if (audio_buffer->flush_handler) + audio_buffer->flush_handler(audio_buffer->packet, + audio_buffer->bytes_written, audio_buffer->data); + + /* Reset buffer in all cases */ + audio_buffer->bytes_written = 0; + + } + + } /* end packet write loop */ + + /* Track current position in audio stream */ + audio_buffer->total_bytes_received += length; + + pthread_mutex_unlock(&(audio_buffer->lock)); + +} + +void guac_rdp_audio_buffer_end(guac_rdp_audio_buffer* audio_buffer) { + + pthread_mutex_lock(&(audio_buffer->lock)); + + /* The stream is now closed */ + guac_rdp_audio_buffer_ack(audio_buffer, + "CLOSED", GUAC_PROTOCOL_STATUS_RESOURCE_CLOSED); + + /* Unset user and stream */ + audio_buffer->user = NULL; + audio_buffer->stream = NULL; + + /* Reset buffer state */ + audio_buffer->bytes_written = 0; + audio_buffer->packet_size = 0; + audio_buffer->flush_handler = NULL; + + /* Reset I/O counters */ + audio_buffer->total_bytes_sent = 0; + audio_buffer->total_bytes_received = 0; + + /* Free packet (if any) */ + free(audio_buffer->packet); + audio_buffer->packet = NULL; + + pthread_mutex_unlock(&(audio_buffer->lock)); + +} + +void guac_rdp_audio_buffer_free(guac_rdp_audio_buffer* audio_buffer) { + pthread_mutex_destroy(&(audio_buffer->lock)); + free(audio_buffer->packet); + free(audio_buffer); +} + diff --git a/src/protocols/rdp/audio_input.h b/src/protocols/rdp/audio_input.h new file mode 100644 index 00000000..62806628 --- /dev/null +++ b/src/protocols/rdp/audio_input.h @@ -0,0 +1,313 @@ +/* + * 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. + */ + +#ifndef GUAC_RDP_AUDIO_INPUT_H +#define GUAC_RDP_AUDIO_INPUT_H + +#include "config.h" +#include "dvc.h" + +#include +#include +#include + +#include + +/** + * Handler which is invoked when a guac_rdp_audio_buffer's internal packet + * buffer has reached capacity and must be flushed. + * + * @param buffer + * The buffer which needs to be flushed as an audio packet. + * + * @param length + * The number of bytes stored within the buffer. This is guaranteed to be + * identical to the packet_size value specified when the audio buffer was + * initialized. + * + * @param data + * The arbitrary data pointer provided when the audio buffer was + * initialized. + */ +typedef void guac_rdp_audio_buffer_flush_handler(char* buffer, int length, + void* data); + +/** + * A description of an arbitrary PCM audio format. + */ +typedef struct guac_rdp_audio_format { + + /** + * The rate of the audio data in samples per second. + */ + int rate; + + /** + * The number of channels included in the audio data. This will be 1 for + * monaural audio and 2 for stereo. + */ + int channels; + + /** + * The size of each sample within the audio data, in bytes. + */ + int bps; + +} guac_rdp_audio_format; + +/** + * A buffer of arbitrary audio data. Received audio data can be written to this + * buffer, and will automatically be flushed via a given handler once the + * internal buffer reaches capacity. + */ +typedef struct guac_rdp_audio_buffer { + + /** + * Lock which is acquired/released to ensure accesses to the audio buffer + * are atomic. + */ + pthread_mutex_t lock; + + /** + * The user from which this audio buffer will receive data. If no user has + * yet opened an associated audio stream, this will be NULL. + */ + guac_user* user; + + /** + * The stream from which this audio buffer will receive data. If no user + * has yet opened an associated audio stream, this will be NULL. + */ + guac_stream* stream; + + /** + * The PCM format of the audio stream being received from the user, if any. + * If no stream is yet associated, the values stored within this format are + * undefined. + */ + guac_rdp_audio_format in_format; + + /** + * The PCM format of the audio stream expected by RDP, if any. If no audio + * stream has yet been requested by the RDP server, the values stored + * within this format are undefined. + */ + guac_rdp_audio_format out_format; + + /** + * The size that each audio packet must be, in bytes. The packet buffer + * within this structure will be at least this size. + */ + int packet_size; + + /** + * The number of bytes currently stored within the packet buffer. + */ + int bytes_written; + + /** + * The total number of bytes having ever been received by the Guacamole + * server for the current audio stream. + */ + int total_bytes_received; + + /** + * The total number of bytes having ever been sent to the RDP server for + * the current audio stream. + */ + int total_bytes_sent; + + /** + * All audio data being prepared for sending to the AUDIO_INPUT channel. + */ + char* packet; + + /** + * Handler function which will be invoked when a full audio packet is + * ready to be flushed to the AUDIO_INPUT channel, if defined. If NULL, + * audio packets will simply be ignored. + */ + guac_rdp_audio_buffer_flush_handler* flush_handler; + + /** + * Arbitrary data assigned by the AUDIO_INPUT plugin implementation. + */ + void* data; + +} guac_rdp_audio_buffer; + +/** + * Allocates a new audio buffer. The new audio buffer will ignore any received + * data until guac_rdp_audio_buffer_begin() is invoked, and will resume + * ignoring received data once guac_rdp_audio_buffer_end() is invoked. + * + * @return + * A newly-allocated audio buffer. + */ +guac_rdp_audio_buffer* guac_rdp_audio_buffer_alloc(); + +/** + * Associates the given audio buffer with the underlying audio stream which + * has been received from the given Guacamole user. Once both the Guacamole + * audio stream and the RDP audio stream are ready, an appropriate "ack" + * message will be sent. + * + * @param audio_buffer + * The audio buffer associated with the audio stream just received. + * + * @param user + * The Guacamole user that created the audio stream. + * + * @param stream + * The guac_stream object representing the audio stream. + * + * @param rate + * The rate of the audio stream being received from the user, if any, in + * samples per second. + * + * @param channels + * The number of channels included in the audio stream being received from + * the user, if any. + * + * @param bps + * The size of each sample within the audio stream being received from the + * user, if any, in bytes. + */ +void guac_rdp_audio_buffer_set_stream(guac_rdp_audio_buffer* audio_buffer, + guac_user* user, guac_stream* stream, int rate, int channels, int bps); + +/** + * Defines the output format that should be used by the audio buffer when + * flushing packets of audio data received via guac_rdp_audio_buffer_write(). + * As this format determines how the underlying packet buffer will be + * allocated, this function MUST be called prior to the call to + * guac_rdp_audio_buffer_begin(). + * + * @param audio_buffer + * The audio buffer to set the output format of. + * + * @param rate + * The rate of the audio stream expected by RDP, in samples per second. + * + * @param channels + * The number of channels included in the audio stream expected by RDP. + * + * @param bps + * The size of each sample within the audio stream expected by RDP, in + * bytes. + */ +void guac_rdp_audio_buffer_set_output(guac_rdp_audio_buffer* audio_buffer, + int rate, int channels, int bps); + +/** + * Begins handling of audio data received via guac_rdp_audio_buffer_write() and + * allocates the necessary underlying packet buffer. Audio packets having + * exactly packet_frames frames will be flushed as available using the provided + * flush_handler. An audio frame is a set of single samples, one sample per + * channel. The guac_rdp_audio_buffer_set_output() function MUST have + * been invoked first. + * + * @param audio_buffer + * The audio buffer to begin. + * + * @param packet_frames + * The exact number of frames (a set of samples, one for each channel) + * which MUST be included in all audio packets provided to the + * given flush_handler. + * + * @param flush_handler + * The function to invoke when an audio packet must be flushed. + * + * @param data + * Arbitrary data to provide to the flush_handler when an audio packet + * needs to be flushed. + */ +void guac_rdp_audio_buffer_begin(guac_rdp_audio_buffer* audio_buffer, + int packet_frames, guac_rdp_audio_buffer_flush_handler* flush_handler, + void* data); + +/** + * Writes the given buffer of audio data to the given audio buffer. A new + * packet will be flushed using the associated flush handler once sufficient + * bytes have been accumulated. + * + * @param audio_buffer + * The audio buffer to which the given audio data should be written. + * + * @param buffer + * The buffer of audio data to write to the given audio buffer. + * + * @param length + * The number of bytes to write. + */ +void guac_rdp_audio_buffer_write(guac_rdp_audio_buffer* audio_buffer, + char* buffer, int length); + +/** + * Stops handling of audio data received via guac_rdp_audio_buffer_write() and + * frees the underlying packet buffer. Further audio data will be ignored until + * guac_rdp_audio_buffer_begin() is invoked again. + * + * @param audio_buffer + * The audio buffer to end. + */ +void guac_rdp_audio_buffer_end(guac_rdp_audio_buffer* audio_buffer); + +/** + * Frees the given audio buffer. If guac_rdp_audio_buffer_end() has not yet + * been called, its associated packet buffer will also be freed. + * + * @param audio_buffer + * The audio buffer to free. + */ +void guac_rdp_audio_buffer_free(guac_rdp_audio_buffer* audio_buffer); + +/** + * Handler for inbound audio data (audio input). + */ +guac_user_audio_handler guac_rdp_audio_handler; + +/** + * Handler for stream data related to audio input. + */ +guac_user_blob_handler guac_rdp_audio_blob_handler; + +/** + * Handler for end-of-stream related to audio input. + */ +guac_user_end_handler guac_rdp_audio_end_handler; + +/** + * Adds Guacamole's "guacai" plugin to the list of dynamic virtual channel + * plugins to be loaded by FreeRDP's "drdynvc" plugin. The plugin will only + * be loaded once guac_rdp_load_drdynvc() is invoked with the guac_rdp_dvc_list + * passed to this function. The "guacai" plugin ultimately adds support for the + * "AUDIO_INPUT" dynamic virtual channel. + * + * @param context + * The rdpContext associated with the active RDP session. + * + * @param list + * The guac_rdp_dvc_list to which the "guacai" plugin should be added, such + * that it may later be loaded by guac_rdp_load_drdynvc(). + */ +void guac_rdp_audio_load_plugin(rdpContext* context, guac_rdp_dvc_list* list); + +#endif + diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index 51321257..362f0aad 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -19,6 +19,7 @@ #include "config.h" +#include "audio_input.h" #include "client.h" #include "rdp.h" #include "rdp_disp.h" @@ -129,6 +130,10 @@ int guac_rdp_client_free_handler(guac_client* client) { if (rdp_client->audio != NULL) guac_audio_stream_free(rdp_client->audio); + /* Clean up audio input buffer, if allocated */ + if (rdp_client->audio_input != NULL) + guac_rdp_audio_buffer_free(rdp_client->audio_input); + /* Free client data */ guac_common_clipboard_free(rdp_client->clipboard); free(rdp_client); diff --git a/src/protocols/rdp/compat/winpr-stream.c b/src/protocols/rdp/compat/winpr-stream.c index 528177dc..8be45466 100644 --- a/src/protocols/rdp/compat/winpr-stream.c +++ b/src/protocols/rdp/compat/winpr-stream.c @@ -22,17 +22,26 @@ #include "winpr-stream.h" #include "winpr-wtypes.h" -/* - * NOTE: Because the old API did not support local allocation of the buffer - * for each stream, these compatibility implementations ignore - * the parameters of Stream_New() and Stream_Free() that provide them. - */ - wStream* Stream_New(BYTE* buffer, size_t size) { - return stream_new(size); + + /* If no buffer is provided, allocate a new stream of the given size */ + if (buffer == NULL) + return stream_new(size); + + /* Otherwise allocate an empty stream and assign the given buffer */ + wStream* stream = stream_new(0); + stream_attach(stream, buffer, size); + return stream; + } void Stream_Free(wStream* s, BOOL bFreeBuffer) { + + /* Disassociate buffer if it will be freed externally */ + if (!bFreeBuffer) + stream_detach(s); + stream_free(s); + } diff --git a/src/protocols/rdp/dvc.c b/src/protocols/rdp/dvc.c new file mode 100644 index 00000000..10c65d7f --- /dev/null +++ b/src/protocols/rdp/dvc.c @@ -0,0 +1,183 @@ +/* + * 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 "config.h" +#include "dvc.h" +#include "guac_list.h" +#include "rdp.h" + +#include +#include +#include + +#include +#include + +guac_rdp_dvc_list* guac_rdp_dvc_list_alloc() { + + guac_rdp_dvc_list* list = malloc(sizeof(guac_rdp_dvc_list)); + + /* Initialize with empty backing list */ + list->channels = guac_common_list_alloc(); + list->channel_count = 0; + + return list; + +} + +void guac_rdp_dvc_list_add(guac_rdp_dvc_list* list, const char* name, ...) { + + va_list args; + + guac_rdp_dvc* dvc = malloc(sizeof(guac_rdp_dvc)); + + va_start(args, name); + + /* Count number of arguments (excluding terminating NULL) */ + dvc->argc = 1; + while (va_arg(args, char*) != NULL) + dvc->argc++; + + /* Reset va_list */ + va_end(args); + va_start(args, name); + + /* Copy argument values into DVC entry */ + dvc->argv = malloc(sizeof(char*) * dvc->argc); + dvc->argv[0] = strdup(name); + int i; + for (i = 1; i < dvc->argc; i++) + dvc->argv[i] = strdup(va_arg(args, char*)); + + va_end(args); + + /* Add entry to DVC list */ + guac_common_list_add(list->channels, dvc); + + /* Update channel count */ + list->channel_count++; + +} + +void guac_rdp_dvc_list_free(guac_rdp_dvc_list* list) { + + /* For each channel */ + guac_common_list_element* current = list->channels->head; + while (current != NULL) { + + /* Free arguments declaration for current channel */ + guac_rdp_dvc* dvc = (guac_rdp_dvc*) current->data; + + /* Free the underlying arguments list if not delegated to FreeRDP */ + if (dvc->argv != NULL) { + + /* Free each argument value */ + for (int i = 0; i < dvc->argc; i++) + free(dvc->argv[i]); + + free(dvc->argv); + } + + free(dvc); + + current = current->next; + + } + + /* Free underlying list */ + guac_common_list_free(list->channels); + + /* Free the DVC list itself */ + free(list); + +} + +int guac_rdp_load_drdynvc(rdpContext* context, guac_rdp_dvc_list* list) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + rdpChannels* channels = context->channels; + + /* Skip if no channels will be loaded */ + if (list->channel_count == 0) + return 0; + +#ifndef HAVE_ADDIN_ARGV + /* Allocate plugin data array */ + RDP_PLUGIN_DATA* all_plugin_data = + calloc(list->channel_count + 1, sizeof(RDP_PLUGIN_DATA)); + + RDP_PLUGIN_DATA* current_plugin_data = all_plugin_data; +#endif + + /* For each channel */ + guac_common_list_element* current = list->channels->head; + while (current != NULL) { + + /* Get channel arguments */ + guac_rdp_dvc* dvc = (guac_rdp_dvc*) current->data; + current = current->next; + + /* guac_rdp_dvc_list_add() guarantees at one argument */ + assert(dvc->argc >= 1); + + /* guac_rdp_load_drdynvc() MUST only be invoked once */ + assert(dvc->argv != NULL); + + /* Log registration of plugin for current channel */ + guac_client_log(client, GUAC_LOG_DEBUG, + "Registering DVC plugin \"%s\"", dvc->argv[0]); + +#ifdef HAVE_ADDIN_ARGV + /* Register plugin with FreeRDP */ + ADDIN_ARGV* args = malloc(sizeof(ADDIN_ARGV)); + args->argc = dvc->argc; + args->argv = dvc->argv; + freerdp_dynamic_channel_collection_add(context->settings, args); +#else + /* Copy all arguments */ + for (int i = 0; i < dvc->argc; i++) + current_plugin_data->data[i] = dvc->argv[i]; + + /* Store size of entry */ + current_plugin_data->size = sizeof(*current_plugin_data); + + /* Advance to next set of plugin data */ + current_plugin_data++; +#endif + + /* Rely on FreeRDP to free argv storage */ + dvc->argv = NULL; + + } + +#ifdef HAVE_ADDIN_ARGV + /* Load virtual channel management plugin */ + return freerdp_channels_load_plugin(channels, context->instance->settings, + "drdynvc", context->instance->settings); +#else + /* Terminate with empty RDP_PLUGIN_DATA element */ + current_plugin_data->size = 0; + + /* Load virtual channel management plugin */ + return freerdp_channels_load_plugin(channels, context->instance->settings, + "drdynvc", all_plugin_data); +#endif + +} + diff --git a/src/protocols/rdp/dvc.h b/src/protocols/rdp/dvc.h new file mode 100644 index 00000000..02ca6437 --- /dev/null +++ b/src/protocols/rdp/dvc.h @@ -0,0 +1,138 @@ +/* + * 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. + */ + +#ifndef GUAC_RDP_DVC_H +#define GUAC_RDP_DVC_H + +#include "config.h" +#include "guac_list.h" + +#include + +/** + * The set of all arguments that should be passed to a given dynamic virtual + * channel plugin, including the name of that plugin. + */ +typedef struct guac_rdp_dvc { + + /** + * The number of arguments in the argv array. This MUST be at least 1. + */ + int argc; + + /** + * The argument values being passed to the dynamic virtual channel plugin. + * The first entry in this array is always the name of the plugin. If + * guac_rdp_load_drdynvc() has been invoked, and freeing the argument + * values is being delegated to FreeRDP, this will be NULL. + */ + char** argv; + +} guac_rdp_dvc; + +/** + * A list of dynamic virtual channels which should be provided to the DRDYNVC + * plugin once loaded via guac_rdp_load_drdynvc(). This interface exists purely + * to bridge incompatibilities between differing versions of FreeRDP and its + * DRDYNVC plugin. Any allocated guac_rdp_dvc_list is unlikely to be needed + * after the DRDYNVC plugin has been loaded. + */ +typedef struct guac_rdp_dvc_list { + + /** + * Array of all dynamic virtual channels which should be registered with + * the DRDYNVC plugin once loaded. Each list element will point to a + * guac_rdp_dvc structure which must eventually be freed. + */ + guac_common_list* channels; + + /** + * The number of channels within the list. + */ + int channel_count; + +} guac_rdp_dvc_list; + +/** + * Allocates a new, empty list of dynamic virtual channels. New channels may + * be added via guac_rdp_dvc_list_add(). The loading of those channels' + * associated plugins will be deferred until guac_rdp_load_drdynvc() is + * invoked. + * + * @return + * A newly-allocated, empty list of dynamic virtual channels. + */ +guac_rdp_dvc_list* guac_rdp_dvc_list_alloc(); + +/** + * Adds the given dynamic virtual channel plugin name and associated arguments + * to the list. The provied arguments list is NOT optional and MUST be + * NULL-terminated, even if there are no arguments for the named dynamic + * virtual channel plugin. Though FreeRDP requires that the arguments for a + * dynamic virtual channel plugin contain the name of the plugin itself as the + * first argument, the name must be excluded from the arguments provided here. + * This function will automatically take care of adding the plugin name to + * the arguments. + * + * @param list + * The guac_rdp_dvc_list to which the given plugin name and arguments + * should be added, for later bulk registration via + * guac_rdp_load_drdynvc(). + * + * @param name + * The name of the dynamic virtual channel plugin that should be given + * the provided arguments when guac_rdp_load_drdynvc() is invoked. + * + * @param ... + * The string (char*) arguments which should be passed to the dynamic + * virtual channel plugin when it is loaded via guac_rdp_load_drdynvc(), + * excluding the plugin name itself. + */ +void guac_rdp_dvc_list_add(guac_rdp_dvc_list* list, const char* name, ...); + +/** + * Frees the given list of dynamic virtual channels. Note that, while each + * individual entry within this list will be freed, it is partially up to + * FreeRDP to free the storage associated with the arguments passed to the + * virtual channels. + * + * @param list + * The list to free. + */ +void guac_rdp_dvc_list_free(guac_rdp_dvc_list* list); + +/** + * Loads FreeRDP's DRDYNVC plugin and registers the dynamic virtual channel + * plugins described by the given guac_rdp_dvc_list. This function MUST be + * invoked no more than once per RDP connection. Invoking this function + * multiple times, even if the guac_rdp_dvc_list is different each time, will + * result in undefined behavior. + * + * @param context + * The rdpContext associated with the RDP connection for which the DRDYNVC + * plugin should be loaded. + * + * @param list + * A guac_rdp_dvc_list describing the dynamic virtual channel plugins that + * should be registered with the DRDYNVC plugin, along with any arguments. + */ +int guac_rdp_load_drdynvc(rdpContext* context, guac_rdp_dvc_list* list); + +#endif + diff --git a/src/protocols/rdp/guac_ai/ai_messages.c b/src/protocols/rdp/guac_ai/ai_messages.c new file mode 100644 index 00000000..c3465878 --- /dev/null +++ b/src/protocols/rdp/guac_ai/ai_messages.c @@ -0,0 +1,336 @@ +/* + * 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 "config.h" + +#include "ai_messages.h" +#include "audio_input.h" +#include "rdp.h" + +#include + +#include +#include +#include +#include + +#ifdef ENABLE_WINPR +#include +#else +#include "compat/winpr-stream.h" +#endif + +/** + * Reads AUDIO_FORMAT data from the given stream into the given struct. + * + * @param stream + * The stream to read AUDIO_FORMAT data from. + * + * @param format + * The structure to populate with data from the stream. + */ +static void guac_rdp_ai_read_format(wStream* stream, + guac_rdp_ai_format* format) { + + /* Read audio format into structure */ + Stream_Read_UINT16(stream, format->tag); /* wFormatTag */ + Stream_Read_UINT16(stream, format->channels); /* nChannels */ + Stream_Read_UINT32(stream, format->rate); /* nSamplesPerSec */ + Stream_Read_UINT32(stream, format->bytes_per_sec); /* nAvgBytesPerSec */ + Stream_Read_UINT16(stream, format->block_align); /* nBlockAlign */ + Stream_Read_UINT16(stream, format->bps); /* wBitsPerSample */ + Stream_Read_UINT16(stream, format->data_size); /* cbSize */ + + /* Read arbitrary data block (if applicable) */ + if (format->data_size != 0) { + format->data = Stream_Pointer(stream); /* data */ + Stream_Seek(stream, format->data_size); + } + +} + +/** + * Writes AUDIO_FORMAT data to the given stream from the given struct. + * + * @param stream + * The stream to write AUDIO_FORMAT data to. + * + * @param format + * The structure containing the data that should be written to the stream. + */ +static void guac_rdp_ai_write_format(wStream* stream, + guac_rdp_ai_format* format) { + + /* Write audio format into structure */ + Stream_Write_UINT16(stream, format->tag); /* wFormatTag */ + Stream_Write_UINT16(stream, format->channels); /* nChannels */ + Stream_Write_UINT32(stream, format->rate); /* nSamplesPerSec */ + Stream_Write_UINT32(stream, format->bytes_per_sec); /* nAvgBytesPerSec */ + Stream_Write_UINT16(stream, format->block_align); /* nBlockAlign */ + Stream_Write_UINT16(stream, format->bps); /* wBitsPerSample */ + Stream_Write_UINT16(stream, format->data_size); /* cbSize */ + + /* Write arbitrary data block (if applicable) */ + if (format->data_size != 0) + Stream_Write(stream, format->data, format->data_size); + +} + +/** + * Sends a Data Incoming PDU along the given channel. A Data Incoming PDU is + * used by the client to indicate to the server that format or audio data is + * about to be sent. + * + * @param channel + * The channel along which the PDU should be sent. + */ +static void guac_rdp_ai_send_incoming_data(IWTSVirtualChannel* channel) { + + /* Build data incoming PDU */ + wStream* stream = Stream_New(NULL, 1); + Stream_Write_UINT8(stream, GUAC_RDP_MSG_SNDIN_DATA_INCOMING); /* MessageId */ + + /* Send stream */ + channel->Write(channel, (UINT32) Stream_GetPosition(stream), + Stream_Buffer(stream), NULL); + Stream_Free(stream, TRUE); + +} + +static void guac_rdp_ai_send_data(IWTSVirtualChannel* channel, + char* buffer, int length) { + + /* Build data PDU */ + wStream* stream = Stream_New(NULL, length + 1); + Stream_Write_UINT8(stream, GUAC_RDP_MSG_SNDIN_DATA); /* MessageId */ + Stream_Write(stream, buffer, length); /* Data */ + + /* Send stream */ + channel->Write(channel, (UINT32) Stream_GetPosition(stream), + Stream_Buffer(stream), NULL); + Stream_Free(stream, TRUE); + +} + +/** + * Sends a Sound Formats PDU along the given channel. A Sound Formats PDU is + * used by the client to indicate to the server which formats of audio it + * supports (in response to the server sending exactly the same type of PDU). + * This PDU MUST be preceded by the Data Incoming PDU. + * + * @param channel + * The channel along which the PDU should be sent. + * + * @param formats + * An array of all supported formats. + * + * @param num_formats + * The number of entries in the formats array. + */ +static void guac_rdp_ai_send_formats(IWTSVirtualChannel* channel, + guac_rdp_ai_format* formats, int num_formats) { + + int index; + int packet_size = 9; + + /* Calculate packet size */ + for (index = 0; index < num_formats; index++) + packet_size += 18 + formats[index].data_size; + + wStream* stream = Stream_New(NULL, packet_size); + + /* Write header */ + Stream_Write_UINT8(stream, GUAC_RDP_MSG_SNDIN_FORMATS); /* MessageId */ + Stream_Write_UINT32(stream, num_formats); /* NumFormats */ + Stream_Write_UINT32(stream, packet_size); /* cbSizeFormatsPacket */ + + /* Write all formats */ + for (index = 0; index < num_formats; index++) + guac_rdp_ai_write_format(stream, &(formats[index])); + + /* Send PDU */ + channel->Write(channel, (UINT32) Stream_GetPosition(stream), + Stream_Buffer(stream), NULL); + Stream_Free(stream, TRUE); + +} + +/** + * Sends an Open Reply PDU along the given channel. An Open Reply PDU is + * used by the client to acknowledge the successful opening of the AUDIO_INPUT + * channel. + * + * @param channel + * The channel along which the PDU should be sent. + * + * @param result + * The HRESULT code to send to the server indicating success, failure, etc. + */ +static void guac_rdp_ai_send_open_reply(IWTSVirtualChannel* channel, + UINT32 result) { + + /* Build open reply PDU */ + wStream* stream = Stream_New(NULL, 5); + Stream_Write_UINT8(stream, GUAC_RDP_MSG_SNDIN_OPEN_REPLY); /* MessageId */ + Stream_Write_UINT32(stream, result); /* Result */ + + /* Send stream */ + channel->Write(channel, (UINT32) Stream_GetPosition(stream), + Stream_Buffer(stream), NULL); + Stream_Free(stream, TRUE); + +} + +/** + * Sends a Format Change PDU along the given channel. A Format Change PDU is + * used by the client to acknowledge the format being used for data sent + * along the AUDIO_INPUT channel. + * + * @param channel + * The channel along which the PDU should be sent. + * + * @param format + * The index of the format being acknowledged, which must be the index of + * the format within the original Sound Formats PDU received from the + * server. + */ +static void guac_rdp_ai_send_formatchange(IWTSVirtualChannel* channel, + UINT32 format) { + + /* Build format change PDU */ + wStream* stream = Stream_New(NULL, 5); + Stream_Write_UINT8(stream, GUAC_RDP_MSG_SNDIN_FORMATCHANGE); /* MessageId */ + Stream_Write_UINT32(stream, format); /* NewFormat */ + + /* Send stream */ + channel->Write(channel, (UINT32) Stream_GetPosition(stream), + Stream_Buffer(stream), NULL); + Stream_Free(stream, TRUE); + +} + +void guac_rdp_ai_process_version(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream) { + + UINT32 version; + Stream_Read_UINT32(stream, version); + + /* Warn if server's version number is incorrect */ + if (version != 1) + guac_client_log(client, GUAC_LOG_WARNING, + "Server reports AUDIO_INPUT version %i, not 1", version); + + /* Build response version PDU */ + wStream* response = Stream_New(NULL, 5); + Stream_Write_UINT8(response, GUAC_RDP_MSG_SNDIN_VERSION); /* MessageId */ + Stream_Write_UINT32(response, 1); /* Version */ + + /* Send response */ + channel->Write(channel, (UINT32) Stream_GetPosition(response), + Stream_Buffer(response), NULL); + Stream_Free(response, TRUE); + +} + +void guac_rdp_ai_process_formats(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream) { + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_audio_buffer* audio_buffer = rdp_client->audio_input; + + UINT32 num_formats; + Stream_Read_UINT32(stream, num_formats); /* NumFormats */ + Stream_Seek_UINT32(stream); /* cbSizeFormatsPacket (MUST BE IGNORED) */ + + UINT32 index; + for (index = 0; index < num_formats; index++) { + + guac_rdp_ai_format format; + guac_rdp_ai_read_format(stream, &format); + + /* Ignore anything but WAVE_FORMAT_PCM */ + if (format.tag != GUAC_RDP_WAVE_FORMAT_PCM) + continue; + + /* Set output format of internal audio buffer to match RDP server */ + guac_rdp_audio_buffer_set_output(audio_buffer, format.rate, + format.channels, format.bps / 8); + + /* Accept single format */ + guac_rdp_ai_send_incoming_data(channel); + guac_rdp_ai_send_formats(channel, &format, 1); + return; + + } + + /* No formats available */ + guac_client_log(client, GUAC_LOG_WARNING, "AUDIO_INPUT: No WAVE format."); + guac_rdp_ai_send_incoming_data(channel); + guac_rdp_ai_send_formats(channel, NULL, 0); + +} + +static void guac_rdp_ai_flush_packet(char* buffer, int length, void* data) { + + IWTSVirtualChannel* channel = (IWTSVirtualChannel*) data; + + /* Send data over channel */ + guac_rdp_ai_send_incoming_data(channel); + guac_rdp_ai_send_data(channel, buffer, length); + +} + +void guac_rdp_ai_process_open(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream) { + + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_audio_buffer* audio_buffer = rdp_client->audio_input; + + UINT32 packet_frames; + UINT32 initial_format; + + Stream_Read_UINT32(stream, packet_frames); /* FramesPerPacket */ + Stream_Read_UINT32(stream, initial_format); /* InitialFormat */ + + guac_client_log(client, GUAC_LOG_DEBUG, "RDP server is accepting audio " + "input as %i-channel, %i Hz PCM audio at %i bytes/sample.", + audio_buffer->out_format.channels, + audio_buffer->out_format.rate, + audio_buffer->out_format.bps); + + /* Success */ + guac_rdp_ai_send_formatchange(channel, initial_format); + guac_rdp_ai_send_open_reply(channel, 0); + + /* Begin receiving audio data */ + guac_rdp_audio_buffer_begin(audio_buffer, packet_frames, + guac_rdp_ai_flush_packet, channel); + +} + +void guac_rdp_ai_process_formatchange(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream) { + + /* Should not be called as we only accept one format */ + guac_client_log(client, GUAC_LOG_DEBUG, + "RDP server requesting AUDIO_INPUT format change despite only one " + "format available."); + +} + diff --git a/src/protocols/rdp/guac_ai/ai_messages.h b/src/protocols/rdp/guac_ai/ai_messages.h new file mode 100644 index 00000000..55cf6e9a --- /dev/null +++ b/src/protocols/rdp/guac_ai/ai_messages.h @@ -0,0 +1,216 @@ +/* + * 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. + */ + +#ifndef GUAC_RDP_AI_MESSAGES_H +#define GUAC_RDP_AI_MESSAGES_H + +#include "config.h" + +#include +#include + +#ifdef ENABLE_WINPR +#include +#else +#include "compat/winpr-stream.h" +#endif + +/** + * The format tag associated with raw wave audio (WAVE_FORMAT_PCM). This format + * is required to be supported by all RDP servers. + */ +#define GUAC_RDP_WAVE_FORMAT_PCM 0x01 + +/** + * The message ID associated with the AUDIO_INPUT Version PDU. The Version PDU + * is sent by both the client and the server to indicate their version of the + * AUDIO_INPUT channel protocol (which must always be 1). + */ +#define GUAC_RDP_MSG_SNDIN_VERSION 0x01 + +/** + * The message ID associated with the AUDIO_INPUT Sound Formats PDU. The + * Sound Formats PDU is sent by the client and the server to indicate the + * formats of audio supported. + */ +#define GUAC_RDP_MSG_SNDIN_FORMATS 0x02 + +/** + * The message ID associated with the AUDIO_INPUT Open PDU. The Open PDU is + * sent by the server to inform the client that the AUDIO_INPUT channel is + * now open. + */ +#define GUAC_RDP_MSG_SNDIN_OPEN 0x03 + +/** + * The message ID associated with the AUDIO_INPUT Open Reply PDU. The Open + * Reply PDU is sent by the client (after sending a Format Change PDU) to + * acknowledge that the AUDIO_INPUT channel is open. + */ +#define GUAC_RDP_MSG_SNDIN_OPEN_REPLY 0x04 + +/** + * The message ID associated with the AUDIO_INPUT Incoming Data PDU. The + * Incoming Data PDU is sent by the client to inform the server of incoming + * sound format or audio data. + */ +#define GUAC_RDP_MSG_SNDIN_DATA_INCOMING 0x05 + +/** + * The message ID associated with the AUDIO_INPUT Data PDU. The Data PDU is + * sent by the client and contains audio data read from the microphone. + */ +#define GUAC_RDP_MSG_SNDIN_DATA 0x06 + +/** + * The message ID associated with the AUDIO_INPUT Format Change PDU. The Format + * Change PDU is sent by the client to acknowledge the current sound format, or + * by the server to request a different sound format. + */ +#define GUAC_RDP_MSG_SNDIN_FORMATCHANGE 0x07 + +/** + * An AUDIO_INPUT format, analogous to the AUDIO_FORMAT structure defined + * within Microsoft's RDP documentation. + */ +typedef struct guac_rdp_ai_format { + + /** + * The "format tag" denoting the overall format of audio data received, + * such as WAVE_FORMAT_PCM. + */ + UINT16 tag; + + /** + * The number of audio channels. + */ + UINT16 channels; + + /** + * The number of samples per second. + */ + UINT32 rate; + + /** + * The average number of bytes required for one second of audio. + */ + UINT32 bytes_per_sec; + + /** + * The absolute minimum number of bytes required to process audio in this + * format. + */ + UINT16 block_align; + + /** + * The number of bits per sample. + */ + UINT16 bps; + + /** + * The size of the arbitrary data block, if any. The meaning of the data + * within the arbitrary data block is determined by the format tag. + * WAVE_FORMAT_PCM audio has no associated arbitrary data. + */ + UINT16 data_size; + + /** + * Optional arbitrary data whose meaning is determined by the format tag. + * WAVE_FORMAT_PCM audio has no associated arbitrary data. + */ + BYTE* data; + +} guac_rdp_ai_format; + +/** + * Processes a Version PDU received from the RDP server. The Version PDU is + * sent by the server to indicate its version of the AUDIO_INPUT channel + * protocol (which must always be 1). + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param channel + * The IWTSVirtualChannel instance associated with the connected + * AUDIO_INPUT channel. + * + * @param stream + * The received PDU, with the read position just after the message ID field + * common to all AUDIO_INPUT PDUs. + */ +void guac_rdp_ai_process_version(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream); + +/** + * Processes a Sound Formats PDU received from the RDP server. The Sound + * Formats PDU is sent by the server to indicate the formats of audio + * supported. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param channel + * The IWTSVirtualChannel instance associated with the connected + * AUDIO_INPUT channel. + * + * @param stream + * The received PDU, with the read position just after the message ID field + * common to all AUDIO_INPUT PDUs. + */ +void guac_rdp_ai_process_formats(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream); + +/** + * Processes a Open PDU received from the RDP server. The Open PDU is sent by + * the server to inform the client that the AUDIO_INPUT channel is now open. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param channel + * The IWTSVirtualChannel instance associated with the connected + * AUDIO_INPUT channel. + * + * @param stream + * The received PDU, with the read position just after the message ID field + * common to all AUDIO_INPUT PDUs. + */ +void guac_rdp_ai_process_open(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream); + +/** + * Processes a Format Change PDU received from the RDP server. The Format + * Change PDU is sent by the server to request a different sound format. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param channel + * The IWTSVirtualChannel instance associated with the connected + * AUDIO_INPUT channel. + * + * @param stream + * The received PDU, with the read position just after the message ID field + * common to all AUDIO_INPUT PDUs. + */ +void guac_rdp_ai_process_formatchange(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream); + +#endif + diff --git a/src/protocols/rdp/guac_ai/ai_service.c b/src/protocols/rdp/guac_ai/ai_service.c new file mode 100644 index 00000000..5058ea0a --- /dev/null +++ b/src/protocols/rdp/guac_ai/ai_service.c @@ -0,0 +1,349 @@ +/* + * 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 "config.h" + +#include "ai_messages.h" +#include "ai_service.h" +#include "audio_input.h" +#include "ptr_string.h" +#include "rdp.h" + +#include +#include + +#include +#include +#include +#include + +#ifdef ENABLE_WINPR +#include +#else +#include "compat/winpr-stream.h" +#endif + +/** + * Handles the given data received along the AUDIO_INPUT channel of the RDP + * connection associated with the given guac_client. This handler is + * API-independent and is invoked by API-dependent guac_rdp_ai_data callback + * specific to the version of FreeRDP installed. + * + * @param client + * The guac_client associated with RDP connection having the AUDIO_INPUT + * connection along which the given data was received. + * + * @param channel + * A reference to the IWTSVirtualChannel instance along which responses + * should be sent. + * + * @param stream + * The data received along the AUDIO_INPUT channel. + */ +static void guac_rdp_ai_handle_data(guac_client* client, + IWTSVirtualChannel* channel, wStream* stream) { + + /* Read message ID from received PDU */ + BYTE message_id; + Stream_Read_UINT8(stream, message_id); + + /* Invoke appropriate message processor based on ID */ + switch (message_id) { + + /* Version PDU */ + case GUAC_RDP_MSG_SNDIN_VERSION: + guac_rdp_ai_process_version(client, channel, stream); + break; + + /* Sound Formats PDU */ + case GUAC_RDP_MSG_SNDIN_FORMATS: + guac_rdp_ai_process_formats(client, channel, stream); + break; + + /* Open PDU */ + case GUAC_RDP_MSG_SNDIN_OPEN: + guac_rdp_ai_process_open(client, channel, stream); + break; + + /* Format Change PDU */ + case GUAC_RDP_MSG_SNDIN_FORMATCHANGE: + guac_rdp_ai_process_formatchange(client, channel, stream); + break; + + /* Log unknown message IDs */ + default: + guac_client_log(client, GUAC_LOG_DEBUG, + "Unknown AUDIO_INPUT message ID: 0x%x", message_id); + + } + +} + +#ifdef LEGACY_IWTSVIRTUALCHANNELCALLBACK +/** + * Callback which is invoked when data is received along a connection to the + * AUDIO_INPUT plugin. This callback is specific to FreeRDP 1.1 and older. + * + * @param channel_callback + * The IWTSVirtualChannelCallback structure to which this callback was + * originally assigned. + * + * @param size + * The number of bytes received. + * + * @param buffer + * A buffer containing all bytes received. + * + * @return + * Always zero. + */ +static int guac_rdp_ai_data(IWTSVirtualChannelCallback* channel_callback, + UINT32 size, BYTE* buffer) { + + guac_rdp_ai_channel_callback* ai_channel_callback = + (guac_rdp_ai_channel_callback*) channel_callback; + IWTSVirtualChannel* channel = ai_channel_callback->channel; + + /* Invoke generalized (API-independent) data handler */ + wStream* stream = Stream_New(buffer, size); + guac_rdp_ai_handle_data(ai_channel_callback->client, channel, stream); + Stream_Free(stream, FALSE); + + return 0; + +} +#else +/** + * Callback which is invoked when data is received along a connection to the + * AUDIO_INPUT plugin. This callback is specific to FreeRDP 1.2 and newer. + * + * @param channel_callback + * The IWTSVirtualChannelCallback structure to which this callback was + * originally assigned. + * + * @param stream + * The data received. + * + * @return + * Always zero. + */ +static int guac_rdp_ai_data(IWTSVirtualChannelCallback* channel_callback, + wStream* stream) { + + guac_rdp_ai_channel_callback* ai_channel_callback = + (guac_rdp_ai_channel_callback*) channel_callback; + IWTSVirtualChannel* channel = ai_channel_callback->channel; + + /* Invoke generalized (API-independent) data handler */ + guac_rdp_ai_handle_data(ai_channel_callback->client, channel, stream); + + return 0; + +} +#endif + +/** + * Callback which is invoked when a connection to the AUDIO_INPUT plugin is + * closed. + * + * @param channel_callback + * The IWTSVirtualChannelCallback structure to which this callback was + * originally assigned. + * + * @return + * Always zero. + */ +static int guac_rdp_ai_close(IWTSVirtualChannelCallback* channel_callback) { + + guac_rdp_ai_channel_callback* ai_channel_callback = + (guac_rdp_ai_channel_callback*) channel_callback; + + guac_client* client = ai_channel_callback->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_audio_buffer* audio_buffer = rdp_client->audio_input; + + /* Log closure of AUDIO_INPUT channel */ + guac_client_log(client, GUAC_LOG_DEBUG, + "AUDIO_INPUT channel connection closed"); + + guac_rdp_audio_buffer_end(audio_buffer); + free(ai_channel_callback); + return 0; + +} + +/** + * Callback which is invoked when a new connection is received by the + * AUDIO_INPUT plugin. Additional callbacks required to handle received data + * and closure of the connection must be installed at this point. + * + * @param listener_callback + * The IWTSListenerCallback structure associated with the AUDIO_INPUT + * plugin receiving the new connection. + * + * @param channel + * A reference to the IWTSVirtualChannel instance along which data related + * to the AUDIO_INPUT channel should be sent. + * + * @param data + * Absolutely no idea. According to Microsoft's documentation for the + * function prototype on which FreeRDP's API appears to be based: "This + * parameter is not implemented and is reserved for future use." + * + * @param accept + * Pointer to a flag which should be set to TRUE if the connection should + * be accepted or FALSE otherwise. In the case of FreeRDP, this value + * defaults to TRUE, and TRUE absolutely MUST be identically 1 or it will + * be interpreted as FALSE. + * + * @param channel_callback + * A pointer to the location that the new IWTSVirtualChannelCallback + * structure containing the required callbacks should be assigned. + * + * @return + * Always zero. + */ +static int guac_rdp_ai_new_connection( + IWTSListenerCallback* listener_callback, IWTSVirtualChannel* channel, + BYTE* data, int* accept, + IWTSVirtualChannelCallback** channel_callback) { + + guac_rdp_ai_listener_callback* ai_listener_callback = + (guac_rdp_ai_listener_callback*) listener_callback; + + /* Log new AUDIO_INPUT connection */ + guac_client_log(ai_listener_callback->client, GUAC_LOG_DEBUG, + "New AUDIO_INPUT channel connection"); + + /* Allocate new channel callback */ + guac_rdp_ai_channel_callback* ai_channel_callback = + calloc(1, sizeof(guac_rdp_ai_channel_callback)); + + /* Init listener callback with data from plugin */ + ai_channel_callback->client = ai_listener_callback->client; + ai_channel_callback->channel = channel; + ai_channel_callback->parent.OnDataReceived = guac_rdp_ai_data; + ai_channel_callback->parent.OnClose = guac_rdp_ai_close; + + /* Return callback through pointer */ + *channel_callback = (IWTSVirtualChannelCallback*) ai_channel_callback; + return 0; + +} + +/** + * Callback which is invoked when the AUDIO_INPUT plugin has been loaded and + * needs to be initialized with other callbacks and data. + * + * @param plugin + * The AUDIO_INPUT plugin that needs to be initialied. + * + * @param manager + * The IWTSVirtualChannelManager instance with which the AUDIO_INPUT plugin + * must be registered. + * + * @return + * Always zero. + */ +static int guac_rdp_ai_initialize(IWTSPlugin* plugin, + IWTSVirtualChannelManager* manager) { + + /* Allocate new listener callback */ + guac_rdp_ai_listener_callback* ai_listener_callback = + calloc(1, sizeof(guac_rdp_ai_listener_callback)); + + /* Ensure listener callback is freed when plugin is terminated */ + guac_rdp_ai_plugin* ai_plugin = (guac_rdp_ai_plugin*) plugin; + ai_plugin->listener_callback = ai_listener_callback; + + /* Init listener callback with data from plugin */ + ai_listener_callback->client = ai_plugin->client; + ai_listener_callback->parent.OnNewChannelConnection = + guac_rdp_ai_new_connection; + + /* Register listener for "AUDIO_INPUT" channel */ + manager->CreateListener(manager, "AUDIO_INPUT", 0, + (IWTSListenerCallback*) ai_listener_callback, NULL); + + return 0; + +} + +/** + * Callback which is invoked when all connections to the AUDIO_INPUT plugin + * have closed and the plugin is being unloaded. + * + * @param plugin + * The AUDIO_INPUT plugin being unloaded. + * + * @return + * Always zero. + */ +static int guac_rdp_ai_terminated(IWTSPlugin* plugin) { + + guac_rdp_ai_plugin* ai_plugin = (guac_rdp_ai_plugin*) plugin; + guac_client* client = ai_plugin->client; + + /* Free all non-FreeRDP data */ + free(ai_plugin->listener_callback); + free(ai_plugin); + + guac_client_log(client, GUAC_LOG_DEBUG, "AUDIO_INPUT plugin unloaded."); + return 0; + +} + +/** + * Entry point for AUDIO_INPUT dynamic virtual channel. + */ +int DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) { + + /* Pull guac_client from arguments */ +#ifdef HAVE_ADDIN_ARGV + ADDIN_ARGV* args = pEntryPoints->GetPluginData(pEntryPoints); + guac_client* client = (guac_client*) guac_rdp_string_to_ptr(args->argv[1]); +#else + RDP_PLUGIN_DATA* data = pEntryPoints->GetPluginData(pEntryPoints); + guac_client* client = (guac_client*) guac_rdp_string_to_ptr(data->data[1]); +#endif + + /* Pull previously-allocated plugin */ + guac_rdp_ai_plugin* ai_plugin = (guac_rdp_ai_plugin*) + pEntryPoints->GetPlugin(pEntryPoints, "guacai"); + + /* If no such plugin allocated, allocate and register it now */ + if (ai_plugin == NULL) { + + /* Init plugin callbacks and data */ + ai_plugin = calloc(1, sizeof(guac_rdp_ai_plugin)); + ai_plugin->parent.Initialize = guac_rdp_ai_initialize; + ai_plugin->parent.Terminated = guac_rdp_ai_terminated; + ai_plugin->client = client; + + /* Register plugin as "guacai" for later retrieval */ + pEntryPoints->RegisterPlugin(pEntryPoints, "guacai", + (IWTSPlugin*) ai_plugin); + + guac_client_log(client, GUAC_LOG_DEBUG, "AUDIO_INPUT plugin loaded."); + } + + return 1; + +} + diff --git a/src/protocols/rdp/guac_ai/ai_service.h b/src/protocols/rdp/guac_ai/ai_service.h new file mode 100644 index 00000000..28f2227e --- /dev/null +++ b/src/protocols/rdp/guac_ai/ai_service.h @@ -0,0 +1,108 @@ +/* + * 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. + */ + +#ifndef GUAC_RDP_AI_SERVICE_H +#define GUAC_RDP_AI_SERVICE_H + +#include "config.h" + +#include +#include +#include +#include + +/** + * Extended version of the IWTSListenerCallback structure, providing additional + * access to Guacamole-specific data. The IWTSListenerCallback provides access + * to callbacks related to the receipt of new connections to the AUDIO_INPUT + * channel. + */ +typedef struct guac_rdp_ai_listener_callback { + + /** + * The parent IWTSListenerCallback structure that this structure extends. + * THIS MEMBER MUST BE FIRST! + */ + IWTSListenerCallback parent; + + /** + * The guac_client instance associated with the RDP connection using the + * AUDIO_INPUT plugin. + */ + guac_client* client; + +} guac_rdp_ai_listener_callback; + +/** + * Extended version of the IWTSVirtualChannelCallback structure, providing + * additional access to Guacamole-specific data. The IWTSVirtualChannelCallback + * provides access to callbacks related to an active connection to the + * AUDIO_INPUT channel, including receipt of data. + */ +typedef struct guac_rdp_ai_channel_callback { + + /** + * The parent IWTSVirtualChannelCallback structure that this structure + * extends. THIS MEMBER MUST BE FIRST! + */ + IWTSVirtualChannelCallback parent; + + /** + * The actual virtual channel instance along which the AUDIO_INPUT plugin + * should send any responses. + */ + IWTSVirtualChannel* channel; + + /** + * The guac_client instance associated with the RDP connection using the + * AUDIO_INPUT plugin. + */ + guac_client* client; + +} guac_rdp_ai_channel_callback; + +/** + * All data associated with Guacamole's AUDIO_INPUT plugin for FreeRDP. + */ +typedef struct guac_rdp_ai_plugin { + + /** + * The parent IWTSPlugin structure that this structure extends. THIS + * MEMBER MUST BE FIRST! + */ + IWTSPlugin parent; + + /** + * The listener callback structure allocated when the AUDIO_INPUT plugin + * was loaded, if any. If the plugin did not fully load, this will be NULL. + * If non-NULL, this callback structure must be freed when the plugin is + * terminated. + */ + guac_rdp_ai_listener_callback* listener_callback; + + /** + * The guac_client instance associated with the RDP connection using the + * AUDIO_INPUT plugin. + */ + guac_client* client; + +} guac_rdp_ai_plugin; + +#endif + diff --git a/src/protocols/rdp/ptr_string.c b/src/protocols/rdp/ptr_string.c new file mode 100644 index 00000000..1e791217 --- /dev/null +++ b/src/protocols/rdp/ptr_string.c @@ -0,0 +1,55 @@ +/* + * 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 "config.h" +#include "ptr_string.h" + +#include + +#include +#include + +/** + * The maximum number of bytes required to represent a pointer printed using + * printf()'s "%p". This will be the size of the hex prefix ("0x"), null + * terminator, plus two bytes for every byte required by a pointer. + */ +#define GUAC_RDP_PTR_STRING_LENGTH (sizeof("0x") + (sizeof(void*) * 2)) + +char* guac_rdp_ptr_to_string(void* data) { + + /* Convert pointer to string */ + char* str = malloc(GUAC_RDP_PTR_STRING_LENGTH); + sprintf(str, "%p", data); + + return str; + +} + +void* guac_rdp_string_to_ptr(const char* str) { + + void* data; + + /* Convert string to pointer */ + sscanf(str, "%p", &data); + + return data; + +} + diff --git a/src/protocols/rdp/ptr_string.h b/src/protocols/rdp/ptr_string.h new file mode 100644 index 00000000..6b277210 --- /dev/null +++ b/src/protocols/rdp/ptr_string.h @@ -0,0 +1,57 @@ +/* + * 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. + */ + +#ifndef GUAC_RDP_PTR_STRING_H +#define GUAC_RDP_PTR_STRING_H + +#include "config.h" + +#include + +/** + * Converts the given string back into a void pointer. The string MUST have + * been produced via guac_rdp_ptr_to_string(). + * + * @param str + * The string to convert back to a pointer. + * + * @return + * The pointer value of the given string, as originally passed to + * guac_rdp_ptr_to_string(). + */ +void* guac_rdp_string_to_ptr(const char* str); + +/** + * Converts a void pointer into a string representation, safe for use with + * parts of the FreeRDP API which provide only for passing arbitrary strings, + * despite being within the same memory area. The returned string must + * eventually be freed with a call to free(). + * + * @param data + * The void pointer to convert to a string. + * + * @return + * A newly-allocated string containing the string representation of the + * given void pointer. This string must eventually be freed with a call to + * free(). + */ +char* guac_rdp_ptr_to_string(void* data); + +#endif + diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index cd3ee231..d515fb11 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -19,7 +19,9 @@ #include "config.h" +#include "audio_input.h" #include "client.h" +#include "dvc.h" #include "guac_cursor.h" #include "guac_display.h" #include "guac_recording.h" @@ -211,6 +213,7 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { rdpPrimaryUpdate* primary; CLRCONV* clrconv; + guac_rdp_dvc_list* dvc_list = guac_rdp_dvc_list_alloc(); #ifdef HAVE_FREERDP_REGISTER_ADDIN_PROVIDER /* Init FreeRDP add-in provider */ @@ -224,23 +227,17 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { #endif #ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT - /* Load required plugins if display update is enabled */ - if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) { - - /* Load virtual channel management plugin (needed by display update) */ - if (freerdp_channels_load_plugin(channels, instance->settings, - "drdynvc", instance->settings)) - guac_client_log(client, GUAC_LOG_WARNING, - "Failed to load drdynvc plugin. Display update support " - "will be disabled."); - - /* Init display update plugin if "drdynvc" was loaded successfully */ - else - guac_rdp_disp_load_plugin(instance->context); - - } + /* Load "disp" plugin for display update */ + if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) + guac_rdp_disp_load_plugin(instance->context, dvc_list); #endif + /* Load "AUDIO_INPUT" plugin for audio input*/ + if (settings->enable_audio_input) { + rdp_client->audio_input = guac_rdp_audio_buffer_alloc(); + guac_rdp_audio_load_plugin(instance->context, dvc_list); + } + /* Load clipboard plugin */ if (freerdp_channels_load_plugin(channels, instance->settings, "cliprdr", NULL)) @@ -326,6 +323,15 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { } + /* Load DRDYNVC plugin if required */ + if (guac_rdp_load_drdynvc(instance->context, dvc_list)) + guac_client_log(client, GUAC_LOG_WARNING, + "Failed to load drdynvc plugin. Display update and audio " + "input support will be disabled."); + + /* Dynamic virtual channel list is no longer needed */ + guac_rdp_dvc_list_free(dvc_list); + /* Init color conversion structure */ clrconv = calloc(1, sizeof(CLRCONV)); clrconv->alpha = 1; diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index 9325a183..02d9ac6e 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -22,6 +22,7 @@ #include "config.h" +#include "audio_input.h" #include "guac_clipboard.h" #include "guac_display.h" #include "guac_surface.h" @@ -120,6 +121,11 @@ typedef struct guac_rdp_client { */ guac_audio_stream* audio; + /** + * Audio input buffer, if audio input is enabled. + */ + guac_rdp_audio_buffer* audio_input; + /** * The filesystem being shared, if any. */ diff --git a/src/protocols/rdp/rdp_disp.c b/src/protocols/rdp/rdp_disp.c index eb142867..7c7e059d 100644 --- a/src/protocols/rdp/rdp_disp.c +++ b/src/protocols/rdp/rdp_disp.c @@ -19,6 +19,7 @@ #include "config.h" #include "client.h" +#include "dvc.h" #include "rdp.h" #include "rdp_disp.h" #include "rdp_settings.h" @@ -54,20 +55,14 @@ void guac_rdp_disp_free(guac_rdp_disp* disp) { free(disp); } -void guac_rdp_disp_load_plugin(rdpContext* context) { +void guac_rdp_disp_load_plugin(rdpContext* context, guac_rdp_dvc_list* list) { -#ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT #ifdef HAVE_RDPSETTINGS_SUPPORTDISPLAYCONTROL context->settings->SupportDisplayControl = TRUE; #endif /* Add "disp" channel */ - ADDIN_ARGV* args = malloc(sizeof(ADDIN_ARGV)); - args->argc = 1; - args->argv = malloc(sizeof(char**) * 1); - args->argv[0] = strdup("disp"); - freerdp_dynamic_channel_collection_add(context->settings, args); -#endif + guac_rdp_dvc_list_add(list, "disp", NULL); } diff --git a/src/protocols/rdp/rdp_disp.h b/src/protocols/rdp/rdp_disp.h index 3378cf76..0f34fe12 100644 --- a/src/protocols/rdp/rdp_disp.h +++ b/src/protocols/rdp/rdp_disp.h @@ -20,6 +20,7 @@ #ifndef GUAC_RDP_DISP_H #define GUAC_RDP_DISP_H +#include "dvc.h" #include "rdp_settings.h" #include @@ -96,13 +97,25 @@ guac_rdp_disp* guac_rdp_disp_alloc(); void guac_rdp_disp_free(guac_rdp_disp* disp); /** - * Loads the "disp" plugin for FreeRDP. It is still up to external code to - * detect when the "disp" channel is connected, and update the guac_rdp_disp - * with a call to guac_rdp_disp_connect(). - * * @param context The rdpContext associated with the active RDP session. */ -void guac_rdp_disp_load_plugin(rdpContext* context); +/** + * Adds FreeRDP's "disp" plugin to the list of dynamic virtual channel plugins + * to be loaded by FreeRDP's "drdynvc" plugin. The plugin will only be loaded + * once guac_rdp_load_drdynvc() is invoked with the guac_rdp_dvc_list passed to + * this function. The "disp" plugin ultimately adds support for the Display + * Update channel. NOTE: It is still up to external code to detect when the + * "disp" channel is connected, and update the guac_rdp_disp with a call to + * guac_rdp_disp_connect(). + * + * @param context + * The rdpContext associated with the active RDP session. + * + * @param list + * The guac_rdp_dvc_list to which the "disp" plugin should be added, such + * that it may later be loaded by guac_rdp_load_drdynvc(). + */ +void guac_rdp_disp_load_plugin(rdpContext* context, guac_rdp_dvc_list* list); #ifdef HAVE_FREERDP_DISPLAY_UPDATE_SUPPORT /** diff --git a/src/protocols/rdp/rdp_settings.c b/src/protocols/rdp/rdp_settings.c index bc1d831b..82af859b 100644 --- a/src/protocols/rdp/rdp_settings.c +++ b/src/protocols/rdp/rdp_settings.c @@ -90,6 +90,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "recording-name", "create-recording-path", "resize-method", + "enable-audio-input", NULL }; @@ -378,6 +379,12 @@ enum RDP_ARGS_IDX { */ IDX_RESIZE_METHOD, + /** + * "true" if audio input (microphone) should be enabled for the RDP + * connection, "false" or blank otherwise. + */ + IDX_ENABLE_AUDIO_INPUT, + RDP_ARGS_COUNT }; @@ -739,6 +746,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->resize_method = GUAC_RESIZE_NONE; } + /* Audio input enable/disable */ + settings->enable_audio_input = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_ENABLE_AUDIO_INPUT, 0); + /* Success */ return settings; @@ -981,6 +993,17 @@ void guac_rdp_push_settings(guac_rdp_settings* guac_settings, freerdp* rdp) { #ifdef HAVE_RDPSETTINGS_AUDIOPLAYBACK rdp_settings->AudioPlayback = guac_settings->audio_enabled; #endif +#endif + + /* Audio capture */ +#ifdef LEGACY_RDPSETTINGS +#ifdef HAVE_RDPSETTINGS_AUDIOCAPTURE + rdp_settings->audio_capture = guac_settings->enable_audio_input; +#endif +#else +#ifdef HAVE_RDPSETTINGS_AUDIOCAPTURE + rdp_settings->AudioCapture = guac_settings->enable_audio_input; +#endif #endif /* Device redirection */ diff --git a/src/protocols/rdp/rdp_settings.h b/src/protocols/rdp/rdp_settings.h index 7e3fec94..90c79cee 100644 --- a/src/protocols/rdp/rdp_settings.h +++ b/src/protocols/rdp/rdp_settings.h @@ -378,6 +378,11 @@ typedef struct guac_rdp_settings { */ guac_rdp_resize_method resize_method; + /** + * Whether audio input (microphone) is enabled. + */ + int enable_audio_input; + } guac_rdp_settings; /** diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c index af597dbd..ef13205f 100644 --- a/src/protocols/rdp/user.c +++ b/src/protocols/rdp/user.c @@ -19,6 +19,7 @@ #include "config.h" +#include "audio_input.h" #include "input.h" #include "guac_display.h" #include "user.h" @@ -65,6 +66,10 @@ int guac_rdp_user_join_handler(guac_user* user, int argc, char** argv) { return 1; } + /* Handle inbound audio streams if audio input is enabled */ + if (settings->enable_audio_input) + user->audio_handler = guac_rdp_audio_handler; + } /* If not owner, synchronize with current state */