diff --git a/Makefile.am b/Makefile.am index 6ce3814a..9c62b6f6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -37,6 +37,7 @@ DIST_SUBDIRS = \ src/pulse \ src/protocols/kubernetes \ src/protocols/rdp \ + src/protocols/spice \ src/protocols/ssh \ src/protocols/telnet \ src/protocols/vnc @@ -65,6 +66,10 @@ if ENABLE_RDP SUBDIRS += src/protocols/rdp endif +if ENABLE_SPICE +SUBDIRS += src/protocols/spice +endif + if ENABLE_SSH SUBDIRS += src/protocols/ssh endif diff --git a/configure.ac b/configure.ac index 624aae13..0d89a22c 100644 --- a/configure.ac +++ b/configure.ac @@ -634,6 +634,27 @@ then fi +# +# spice-glib +# + +have_spice_glib=disabled +AC_ARG_WITH([spice], + [AS_HELP_STRING([--with-spice], + [support SPICE @<:@default=check@:>@])], + [], + [with_spice=check]) + +if test "x$with_spice" != "xno" +then + have_spice_glib=yes + PKG_CHECK_MODULES([GLIB2], [glib-2.0],, [have_spice_glib=no]) + PKG_CHECK_MODULES([SPICE], [spice-client-glib-2.0],, [have_spice_glib=no]) +fi + +AM_CONDITIONAL([ENABLE_SPICE], [test "x${have_spice_glib}" = "xyes"]) +AC_SUBST(SPICE_LIBS) + # # FreeRDP 2 (libfreerdp2, libfreerdp-client2, and libwinpr2) @@ -1188,6 +1209,7 @@ AC_CONFIG_FILES([Makefile src/protocols/kubernetes/tests/Makefile src/protocols/rdp/Makefile src/protocols/rdp/tests/Makefile + src/protocols/spice/Makefile src/protocols/ssh/Makefile src/protocols/telnet/Makefile src/protocols/vnc/Makefile]) @@ -1199,6 +1221,7 @@ AC_OUTPUT AM_COND_IF([ENABLE_KUBERNETES], [build_kubernetes=yes], [build_kubernetes=no]) AM_COND_IF([ENABLE_RDP], [build_rdp=yes], [build_rdp=no]) +AM_COND_IF([ENABLE_SPICE], [build_spice=yes], [build_spice=no]) AM_COND_IF([ENABLE_SSH], [build_ssh=yes], [build_ssh=no]) AM_COND_IF([ENABLE_TELNET], [build_telnet=yes], [build_telnet=no]) AM_COND_IF([ENABLE_VNC], [build_vnc=yes], [build_vnc=no]) @@ -1260,6 +1283,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION Kubernetes .... ${build_kubernetes} RDP ........... ${build_rdp} + SPICE ......... ${build_spice} SSH ........... ${build_ssh} Telnet ........ ${build_telnet} VNC ........... ${build_vnc} diff --git a/src/protocols/spice/Makefile.am b/src/protocols/spice/Makefile.am new file mode 100644 index 00000000..902e42bd --- /dev/null +++ b/src/protocols/spice/Makefile.am @@ -0,0 +1,139 @@ +# +# 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. +# +# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim +# into Makefile.in. Though the build system (GNU Autotools) automatically adds +# its own license boilerplate to the generated Makefile.in, that boilerplate +# does not apply to the transcluded portions of Makefile.am which are licensed +# to you by the ASF under the Apache License, Version 2.0, as described above. +# + +AUTOMAKE_OPTIONS = foreign +ACLOCAL_AMFLAGS = -I m4 + +lib_LTLIBRARIES = libguac-client-spice.la + +nodist_libguac_client_spice_la_SOURCES = \ + _generated_keymaps.c + +libguac_client_spice_la_SOURCES = \ + argv.c \ + auth.c \ + channels/audio.c \ + channels/clipboard.c \ + channels/cursor.c \ + channels/display.c \ + channels/file.c \ + channels/file-download.c \ + channels/file-ls.c \ + channels/file-upload.c \ + client.c \ + decompose.c \ + input.c \ + keyboard.c \ + keymap.c \ + log.c \ + settings.c \ + spice.c \ + user.c + +noinst_HEADERS = \ + argv.h \ + auth.h \ + channels/audio.h \ + channels/clipboard.h \ + channels/cursor.h \ + channels/display.h \ + channels/file.h \ + channels/file-download.h \ + channels/file-ls.h \ + channels/file-upload.h \ + client.h \ + decompose.h \ + input.h \ + keyboard.h \ + keymap.h \ + log.h \ + settings.h \ + spice.h \ + user.h + +libguac_client_spice_la_CFLAGS = \ + -Werror -Wall -pedantic -Iinclude \ + @COMMON_INCLUDE@ \ + @COMMON_SSH_INCLUDE@ \ + @LIBGUAC_INCLUDE@ \ + @GLIB2_CFLAGS@ \ + @SPICE_CFLAGS@ + +libguac_client_spice_la_LDFLAGS = \ + -version-info 0:0:0 \ + @CAIRO_LIBS@ \ + @GLIB2_LIBS@ \ + @SPICE_LIBS@ + +libguac_client_spice_la_LIBADD = \ + @COMMON_LTLIB@ \ + @LIBGUAC_LTLIB@ + +# Optional SFTP support +if ENABLE_COMMON_SSH +libguac_client_spice_la_SOURCES += sftp.c +noinst_HEADERS += sftp.h +libguac_client_spice_la_LIBADD += @COMMON_SSH_LTLIB@ +endif + +# +# Autogenerated keymaps and channel wrapper functions +# + +CLEANFILES = \ + _generated_keymaps.c + +BUILT_SOURCES = \ + _generated_keymaps.c + +spice_keymaps = \ + $(srcdir)/keymaps/base.keymap \ + $(srcdir)/keymaps/failsafe.keymap \ + $(srcdir)/keymaps/de_de_qwertz.keymap \ + $(srcdir)/keymaps/de_ch_qwertz.keymap \ + $(srcdir)/keymaps/en_gb_qwerty.keymap \ + $(srcdir)/keymaps/en_us_qwerty.keymap \ + $(srcdir)/keymaps/es_es_qwerty.keymap \ + $(srcdir)/keymaps/es_latam_qwerty.keymap \ + $(srcdir)/keymaps/fr_be_azerty.keymap \ + $(srcdir)/keymaps/fr_ca_qwerty.keymap \ + $(srcdir)/keymaps/fr_ch_qwertz.keymap \ + $(srcdir)/keymaps/fr_fr_azerty.keymap \ + $(srcdir)/keymaps/hu_hu_qwertz.keymap \ + $(srcdir)/keymaps/it_it_qwerty.keymap \ + $(srcdir)/keymaps/ja_jp_qwerty.keymap \ + $(srcdir)/keymaps/no_no_qwerty.keymap \ + $(srcdir)/keymaps/pl_pl_qwerty.keymap \ + $(srcdir)/keymaps/pt_br_qwerty.keymap \ + $(srcdir)/keymaps/sv_se_qwerty.keymap \ + $(srcdir)/keymaps/da_dk_qwerty.keymap \ + $(srcdir)/keymaps/tr_tr_qwerty.keymap + +_generated_keymaps.c: $(spice_keymaps) $(srcdir)/keymaps/generate.pl + $(AM_V_GEN) $(srcdir)/keymaps/generate.pl $(spice_keymaps) + +EXTRA_DIST = \ + $(spice_keymaps) \ + keymaps/generate.pl \ No newline at end of file diff --git a/src/protocols/spice/argv.c b/src/protocols/spice/argv.c new file mode 100644 index 00000000..ee326a8a --- /dev/null +++ b/src/protocols/spice/argv.c @@ -0,0 +1,53 @@ +/* + * 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 "argv.h" +#include "spice.h" + +#include +#include +#include + +#include +#include +#include + +int guac_spice_argv_callback(guac_user* user, const char* mimetype, + const char* name, const char* value, void* data) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_settings* settings = spice_client->settings; + + /* Update username */ + if (strcmp(name, GUAC_SPICE_ARGV_USERNAME) == 0) { + free(settings->username); + settings->username = strdup(value); + } + + /* Update password */ + else if (strcmp(name, GUAC_SPICE_ARGV_PASSWORD) == 0) { + free(settings->password); + settings->password = strdup(value); + } + + return 0; + +} \ No newline at end of file diff --git a/src/protocols/spice/argv.h b/src/protocols/spice/argv.h new file mode 100644 index 00000000..7da86b39 --- /dev/null +++ b/src/protocols/spice/argv.h @@ -0,0 +1,47 @@ +/* + * 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_SPICE_ARGV_H +#define GUAC_SPICE_ARGV_H + +#include "config.h" + +#include +#include + +/** + * Handles a received argument value from a Guacamole "argv" instruction, + * updating the given connection parameter. + */ +guac_argv_callback guac_spice_argv_callback; + +/** + * The name of the parameter Guacamole will use to specify/update the username + * for the Spice connection. + */ +#define GUAC_SPICE_ARGV_USERNAME "username" + +/** + * The name of the parameter Guacamole will use to specify/update the password + * for the Spice connection. + */ +#define GUAC_SPICE_ARGV_PASSWORD "password" + +#endif /* GUAC_SPICE_ARGV_H */ + diff --git a/src/protocols/spice/auth.c b/src/protocols/spice/auth.c new file mode 100644 index 00000000..58c851d3 --- /dev/null +++ b/src/protocols/spice/auth.c @@ -0,0 +1,71 @@ +/* + * 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 "argv.h" +#include "auth.h" +#include "spice.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +gboolean guac_spice_get_credentials(guac_client* client) { + + guac_spice_client* spice_client = ((guac_spice_client*) client->data); + guac_spice_settings* settings = spice_client->settings; + + char* params[3] = {NULL}; + int i = 0; + + /* Check if username is not provided. */ + if (settings->username == NULL) { + guac_argv_register(GUAC_SPICE_ARGV_USERNAME, guac_spice_argv_callback, NULL, 0); + params[i] = GUAC_SPICE_ARGV_USERNAME; + i++; + } + + /* Check if password is not provided. */ + if (settings->password == NULL) { + guac_argv_register(GUAC_SPICE_ARGV_PASSWORD, guac_spice_argv_callback, NULL, 0); + params[i] = GUAC_SPICE_ARGV_PASSWORD; + i++; + } + + params[i] = NULL; + + /* If we have empty parameters, request them and await response. */ + if (i > 0) { + guac_client_owner_send_required(client, (const char**) params); + guac_argv_await((const char**) params); + return true; + } + + guac_client_log(client, GUAC_LOG_DEBUG, + "Unable to retrieve any credentials from the user."); + return false; + +} diff --git a/src/protocols/spice/auth.h b/src/protocols/spice/auth.h new file mode 100644 index 00000000..ac9d3065 --- /dev/null +++ b/src/protocols/spice/auth.h @@ -0,0 +1,46 @@ +/* + * 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_SPICE_AUTH_H +#define GUAC_SPICE_AUTH_H + +#include "config.h" + +#include + +#include + +/** + * Handler invoked when an authentication error is received from the Spice + * server, which retrieves the credentials from the Guacamole Client accessing + * the connection, if those credentials have not been explicitly set in the + * configuration. Returns TRUE if credentials are successfully retrieved, or + * FALSE otherwise. + * + * @param client + * The guac_client that is attempting to connect to the Spice server and + * that will be asked for the credentials. + * + * @return + * TRUE if the credentials are retrieved from the users; otherwise FALSE. + */ +gboolean guac_spice_get_credentials(guac_client* client); + +#endif /* GUAC_SPICE_AUTH_H */ + diff --git a/src/protocols/spice/channels/audio.c b/src/protocols/spice/channels/audio.c new file mode 100644 index 00000000..4f0d0335 --- /dev/null +++ b/src/protocols/spice/channels/audio.c @@ -0,0 +1,360 @@ +/* + * 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.h" +#include "spice.h" + +#include +#include + +#include +#include + +void guac_spice_client_audio_playback_data_handler( + SpicePlaybackChannel* channel, gpointer data, gint size, + guac_client* client) { + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_audio_stream_write_pcm(spice_client->audio_playback, data, size); + +} + +void guac_spice_client_audio_playback_delay_handler( + SpicePlaybackChannel* channel, guac_client* client) { + + guac_client_log(client, GUAC_LOG_WARNING, + "Delay handler for audio playback is not currently implemented."); + +} + +void guac_spice_client_audio_playback_start_handler( + SpicePlaybackChannel* channel, gint format, gint channels, gint rate, + guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Starting audio playback."); + guac_client_log(client, GUAC_LOG_DEBUG, "Format: %d", format); + guac_client_log(client, GUAC_LOG_DEBUG, "Channels: %d", channels); + guac_client_log(client, GUAC_LOG_DEBUG, "Rate: %d", rate); + + /* Spice only supports a single audio format. */ + if (format != SPICE_AUDIO_FMT_S16) { + guac_client_log(client, GUAC_LOG_WARNING, "Unknown Spice audio format: %d", format); + return; + } + + /* Allocate the stream. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + spice_client->audio_playback = guac_audio_stream_alloc(client, NULL, rate, channels, 16); + +} + +void guac_spice_client_audio_playback_stop_handler( + SpicePlaybackChannel* channel, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Stoppig audio playback.."); + + /* Free the audio stream. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_audio_stream_free(spice_client->audio_playback); + +} + +/** + * Parses the given raw audio mimetype, producing the corresponding rate, + * number of channels, and bytes per sample. + * + * @param mimetype + * The raw audio 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_spice_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 two bytes per sample */ + 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; + +} + +/** + * A callback function that is invoked to send audio data from the given + * stream to the Spice server. + * + * @param user + * The user who owns the connection and the stream. This is unused by + * this function. + * + * @param stream + * The stream where the audio data originated. This is unused by this + * function. + * + * @param data + * The audio data to send. + * + * @param length + * The number of bytes of audio data to send. + * + * @return + * Zero on success, non-zero on error. + */ +static int guac_spice_audio_blob_handler(guac_user* user, guac_stream* stream, + void* data, int length) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Write blob to audio stream */ + spice_record_channel_send_data(spice_client->record_channel, data, length, (unsigned long) time(NULL)); + + return 0; + +} + +/** + * A callback function that is called when the audio stream ends sending data + * to the Spice server. + * + * @param user + * The user who owns the connection and the stream. + * + * @param stream + * The stream that was sending the audio data. + * + * @return + * Zero on success, non-zero on failure. + */ +static int guac_spice_audio_end_handler(guac_user* user, guac_stream* stream) { + + /* Ignore - the RECORD_CHANNEL channel will simply not receive anything */ + return 0; + +} + + +int guac_spice_client_audio_record_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + guac_user_log(user, GUAC_LOG_DEBUG, "Calling audio input handler."); + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + spice_client->audio_input = stream; + + int rate; + int channels; + int bps; + + /* Parse mimetype, abort on parse error */ + if (guac_spice_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; + } + + /* Initialize stream handlers */ + stream->blob_handler = guac_spice_audio_blob_handler; + stream->end_handler = guac_spice_audio_end_handler; + + return 0; + + +} + +/** + * 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 Spice RECORD_CHANNEL channel. + * + * @param user + * The guac_user associated with the audio input stream. + * + * @param stream + * The guac_stream associated with the audio input for the client. + * + * @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_spice_audio_stream_ack(guac_user* user, guac_stream* stream, + const char* message, guac_protocol_status status) { + + /* Do not send if the connection owner or stream is null. */ + if (user == NULL || stream == NULL) + return; + + /* Send ack instruction */ + guac_protocol_send_ack(user->socket, stream, message, status); + guac_socket_flush(user->socket); + +} + +/** + * A callback that is invoked for the connection owner when audio recording + * starts, which will notify the client the owner is connected from to start + * sending audio data. + * + * @param owner + * The owner of the connection. + * + * @param data + * A pointer to the guac_client associated with this connection. + * + * @return + * Always NULL; + */ +static void* spice_client_record_start_callback(guac_user* owner, void* data) { + + guac_spice_client* spice_client = (guac_spice_client*) data; + + guac_spice_audio_stream_ack(owner, spice_client->audio_input, "OK", + GUAC_PROTOCOL_STATUS_SUCCESS); + + return NULL; + +} + +/** + * A callback that is invoked for the connection owner when audio recording + * is stopped, telling the client to stop sending audio data. + * + * @param owner + * The user who owns this connection. + * + * @param data + * A pointer to the guac_client associated with this connection. + * + * @return + * Always NULL; + */ +static void* spice_client_record_stop_callback(guac_user* owner, void* data) { + + guac_spice_client* spice_client = (guac_spice_client*) data; + + /* The stream is now closed */ + guac_spice_audio_stream_ack(owner, spice_client->audio_input, "CLOSED", + GUAC_PROTOCOL_STATUS_RESOURCE_CLOSED); + + return NULL; + +} + +void guac_spice_client_audio_record_start_handler(SpiceRecordChannel* channel, + gint format, gint channels, gint rate, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Calling audio record start handler."); + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_client_for_owner(client, spice_client_record_start_callback, spice_client); + +} + +void guac_spice_client_audio_record_stop_handler(SpiceRecordChannel* channel, + guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Calling audio record stop handler."); + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_client_for_owner(client, spice_client_record_stop_callback, spice_client); + +} \ No newline at end of file diff --git a/src/protocols/spice/channels/audio.h b/src/protocols/spice/channels/audio.h new file mode 100644 index 00000000..cba31540 --- /dev/null +++ b/src/protocols/spice/channels/audio.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_SPICE_AUDIO_H +#define GUAC_SPICE_AUDIO_H + +#include "config.h" + +#include + +#include + +/** + * A callback function invoked with the SPICE client receives audio playback + * data from the Spice server. + * + * @param channel + * The SpicePlaybackChannel on which the data was sent. + * + * @param data + * The audio data. + * + * @param size + * The number of bytes of data sent. + * + * @param client + * The guac_client associated with this event. + */ +void guac_spice_client_audio_playback_data_handler( + SpicePlaybackChannel* channel, gpointer data, gint size, + guac_client* client); + +/** + * A callback function invoked when the Spice server requests the audio playback + * delay value from the client. + * + * @param channel + * The SpicePlaybackChannel channel on which this event occurred. + * + * @param client + * The guac_client associated with this event. + */ +void guac_spice_client_audio_playback_delay_handler( + SpicePlaybackChannel* channel, guac_client* client); + +/** + * A callback function invoked by the client when the Spice server sends a + * signal indicating that it is starting an audio transmission. + * + * @param channel + * The SpicePlaybackChannel on which this event occurred. + * + * @param format + * The SPICE_AUDIO_FMT value of the format of the audio data. + * + * @param channels + * The number of channels of audio data present. + * + * @param rate + * The audio sampling rate at which the data will be sent. + * + * @param client + * The guac_client associated with this event. + */ +void guac_spice_client_audio_playback_start_handler( + SpicePlaybackChannel* channel, gint format, gint channels, gint rate, + guac_client* client); + +/** + * The callback function invoked by the client when the SPICE server sends + * an event indicating that audio playback will cease. + * + * @param channel + * The SpicePlaybackChannel on which the recording is being halted. + * + * @param client + * The guac_client associated with this event. + */ +void guac_spice_client_audio_playback_stop_handler( + SpicePlaybackChannel* channel, guac_client* client); + +/** + * Handler for inbound audio data (audio input). + */ +guac_user_audio_handler guac_spice_client_audio_record_handler; + +/** + * The callback function invoked by the Spice client when the Spice server + * requests that the client begin recording audio data to send to the server. + * + * @param channel + * The SpiceRecordChannel on which the record event is being requested. + * + * @param format + * The SPICE_AUDIO_FMT value representing the required recording format. + * + * @param channels + * The number of audio channels that should be recorded. + * + * @param rate + * The rate at which the recording should take place. + * + * @param client + * The guac_client associated with this event. + */ +void guac_spice_client_audio_record_start_handler(SpiceRecordChannel* channel, + gint format, gint channels, gint rate, guac_client* client); + +/** + * The callback function invoked by the Spice client when the Spice server sends + * an event requesting that recording be stopped. + * + * @param channel + * The channel associated with the event. + * + * @param client + * The guac_client associated with the event. + */ +void guac_spice_client_audio_record_stop_handler(SpiceRecordChannel* channel, + guac_client* client); +#endif /* GUAC_SPICE_AUDIO_H */ + diff --git a/src/protocols/spice/channels/clipboard.c b/src/protocols/spice/channels/clipboard.c new file mode 100644 index 00000000..7384ac13 --- /dev/null +++ b/src/protocols/spice/channels/clipboard.c @@ -0,0 +1,184 @@ +/* + * 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 "client.h" +#include "clipboard.h" +#include "common/clipboard.h" +#include "common/iconv.h" +#include "spice.h" +#include "spice-constants.h" +#include "user.h" + +#include +#include +#include + +#include + +int guac_spice_clipboard_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + guac_spice_client* spice_client = (guac_spice_client*) user->client->data; + + /* Some versions of VDAgent do not support sending clipboard data. */ + if (!spice_main_channel_agent_test_capability(spice_client->main_channel, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)) { + guac_client_log(user->client, GUAC_LOG_WARNING, "Spice guest agent does" + " not support sending clipboard data on demand."); + return 0; + } + + /* Clear the current clipboard and send the grab command to the agent. */ + guac_common_clipboard_reset(spice_client->clipboard, mimetype); + guint32 clipboard_types[] = { VD_AGENT_CLIPBOARD_UTF8_TEXT }; + spice_main_channel_clipboard_selection_grab(spice_client->main_channel, + VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD, clipboard_types, 1); + + /* Set handlers for clipboard stream */ + stream->blob_handler = guac_spice_clipboard_blob_handler; + stream->end_handler = guac_spice_clipboard_end_handler; + + return 0; +} + +int guac_spice_clipboard_blob_handler(guac_user* user, guac_stream* stream, + void* data, int length) { + + /* Append new data */ + guac_spice_client* spice_client = (guac_spice_client*) user->client->data; + guac_common_clipboard_append(spice_client->clipboard, (char*) data, length); + + return 0; +} + +int guac_spice_clipboard_end_handler(guac_user* user, guac_stream* stream) { + + guac_spice_client* spice_client = (guac_spice_client*) user->client->data; + + /* Send via Spice only if finished connecting */ + if (spice_client->main_channel != NULL) { + spice_main_channel_clipboard_selection_notify(spice_client->main_channel, + VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD, + VD_AGENT_CLIPBOARD_UTF8_TEXT, + (const unsigned char*) spice_client->clipboard->buffer, + spice_client->clipboard->length); + } + + return 0; +} + +void guac_spice_clipboard_selection_handler(SpiceMainChannel* channel, + guint selection, guint type, gpointer data, guint size, + guac_client* client) { + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Loop through clipboard types - currently Guacamole only supports text. */ + switch (type) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + guac_client_log(client, GUAC_LOG_DEBUG, "Notifying client of text " + " on clipboard from server: %s", (char *) data); + guac_common_clipboard_append(spice_client->clipboard, (char *) data, size); + break; + + default: + guac_client_log(client, GUAC_LOG_WARNING, "Guacamole currently does" + " not support clipboard data other than plain text."); + } + + guac_common_clipboard_send(spice_client->clipboard, client); + +} + +void guac_spice_clipboard_selection_grab_handler(SpiceMainChannel* channel, + guint selection, guint32* types, guint ntypes, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Notifying client of clipboard grab" + " in the guest."); + guac_client_log(client, GUAC_LOG_DEBUG, "Arg: channel: 0x%08x", channel); + guac_client_log(client, GUAC_LOG_DEBUG, "Arg: selection: 0x%08x", selection); + guac_client_log(client, GUAC_LOG_DEBUG, "Arg: types: 0x%08x", types); + guac_client_log(client, GUAC_LOG_DEBUG, "Arg: ntypes: 0x%08x", ntypes); + + /* Ignore selection types other than clipboard. */ + if (selection != VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) { + guac_client_log(client, GUAC_LOG_WARNING, "Unsupported clipboard grab type: %d", selection); + return; + } + + /* Loop through the data types sent by the Spice server and process them. */ + for (int i = 0; i < ntypes; i++) { + + /* Currently Guacamole only supports text. */ + if (types[i] != VD_AGENT_CLIPBOARD_UTF8_TEXT) { + guac_client_log(client, GUAC_LOG_WARNING, "Unsupported clipboard data type: %d", types[i]); + continue; + } + + /* Reset our clipboard and request the data from the Spice serer. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_common_clipboard_reset(spice_client->clipboard, "text/plain"); + spice_main_channel_clipboard_selection_request(channel, selection, types[i]); + } + + + +} + +void guac_spice_clipboard_selection_release_handler(SpiceMainChannel* channel, + guint selection, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Notifying client of clipboard" + " release in the guest."); + + /* Transfer data from guest to Guacamole clipboard. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_common_clipboard_send(spice_client->clipboard, client); + +} + +void guac_spice_clipboard_selection_request_handler(SpiceMainChannel* channel, + guint selection, guint type, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Requesting clipboard data from" + " the client."); + + /* Guacamole only supports one clipboard selection type. */ + if (selection != VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) { + guac_client_log(client, GUAC_LOG_WARNING, "Unsupported selection type: %d", selection); + return; + } + + /* Currently Guacamole only implements text support - other types are images. */ + if (type != VD_AGENT_CLIPBOARD_UTF8_TEXT) { + guac_client_log(client, GUAC_LOG_WARNING, "Unsupported clipboard data type: %d", type); + return; + } + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_client_log(client, GUAC_LOG_DEBUG, "Sending clipboard data to server: %s", spice_client->clipboard->buffer); + + /* Send the clipboard data to the guest. */ + spice_main_channel_clipboard_selection_notify(channel, + selection, + type, + (const unsigned char*) spice_client->clipboard->buffer, + spice_client->clipboard->length); + +} diff --git a/src/protocols/spice/channels/clipboard.h b/src/protocols/spice/channels/clipboard.h new file mode 100644 index 00000000..9fef9e39 --- /dev/null +++ b/src/protocols/spice/channels/clipboard.h @@ -0,0 +1,131 @@ +/* + * 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_SPICE_CLIPBOARD_H +#define GUAC_SPICE_CLIPBOARD_H + +#include "config.h" + +#include +#include + +#include + +/** + * Handler for inbound clipboard data from Guacamole users. + */ +guac_user_clipboard_handler guac_spice_clipboard_handler; + +/** + * Handler for stream data related to clipboard. + */ +guac_user_blob_handler guac_spice_clipboard_blob_handler; + +/** + * Handler for end-of-stream related to clipboard. + */ +guac_user_end_handler guac_spice_clipboard_end_handler; + +/** + * A handler that will be registered with the Spice client to handle clipboard + * data sent from the Spice server to the client. + * + * @param channel + * The main Spice channel on which this event was fired. + * + * @param selection + * The clipboard on which the selection occurred. + * + * @param type + * The type of the data that is on the clipboard. + * + * @param data + * A pointer to the location containing the data that is on the clipboard. + * + * @param size + * The amount of data in bytes. + * + * @param client + * The guac_client associated with this event handler, passed when the + * handler was registered. + */ +void guac_spice_clipboard_selection_handler(SpiceMainChannel* channel, + guint selection, guint type, gpointer data, guint size, + guac_client* client); + +/** + * A handler that will be registered with the Spice client to handle clipboard + * events where the guest (vdagent) within the Spice server notifies the client + * that data is available on the clipboard. + * + * @param channel + * The main SpiceChannel on which this event is fired. + * + * @param selection + * The Spice clipboard from which the event is fired. + * + * @param types + * The type of data being sent by the agent. + * + * @param ntypes + * The number of data types represented. + * + * @param client + * The guac_client that was passed in when the callback was registered. + */ +void guac_spice_clipboard_selection_grab_handler(SpiceMainChannel* channel, + guint selection, guint32* types, guint ntypes, guac_client* client); + +/** + * A handler that will be called by the Spice client when the Spice server + * is done with the clipboard and releases control of it. + * + * @param chennl + * The main Spice channel on which this event is fired. + * + * @param selection + * The Spice server clipboard releasing control. + * + * @param client + * The guac_client that was registered with the callback. + */ +void guac_spice_clipboard_selection_release_handler(SpiceMainChannel* channel, + guint selection, guac_client* client); + +/** + * A handler that will be called by the Spice client when the Spice server + * would like to check and receive the contents of the client's clipboard. + * + * @param channel + * The main Spice channel on which this event is fired. + * + * @param selection + * The Spice server clipboard that is requesting data. + * + * @param type + * The type of data to be sent to the Spice server. + * + * @param client + * The guac_client object that was registered with the callback. + */ +void guac_spice_clipboard_selection_request_handler(SpiceMainChannel* channel, + guint selection, guint type, guac_client* client); + +#endif /* GUAC_SPICE_CLIPBOARD_H */ + diff --git a/src/protocols/spice/channels/cursor.c b/src/protocols/spice/channels/cursor.c new file mode 100644 index 00000000..12b28c29 --- /dev/null +++ b/src/protocols/spice/channels/cursor.c @@ -0,0 +1,78 @@ +/* + * 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 "client.h" +#include "common/cursor.h" +#include "common/display.h" +#include "common/surface.h" +#include "spice.h" +#include "spice-constants.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +void guac_spice_cursor_hide(SpiceChannel* channel, guac_client* client) { + + guac_client_log(client, GUAC_LOG_TRACE, "Hiding the cursor."); + + /* Set the cursor to a blank image, hiding it. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_common_cursor_set_blank(spice_client->display->cursor); +} + +void guac_spice_cursor_move(SpiceChannel* channel, int x, int y, + guac_client* client) { + + guac_client_log(client, GUAC_LOG_TRACE, "Cursor move signal received: %d, %d", x, y); + + /* Update the cursor with the new coordinates. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_common_cursor_update(spice_client->display->cursor, client->__owner, x, y, spice_client->display->cursor->button_mask); +} + +void guac_spice_cursor_reset(SpiceChannel* channel, guac_client* client) { + guac_client_log(client, GUAC_LOG_DEBUG, + "Cursor reset signal received, not yet implemented"); +} + +void guac_spice_cursor_set(SpiceChannel* channel, int width, int height, + int x, int y, gpointer* rgba, guac_client* client) { + + guac_client_log(client, GUAC_LOG_TRACE, "Cursor set signal received."); + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + + /* Update stored cursor information */ + guac_common_cursor_set_argb(spice_client->display->cursor, x, y, + (const unsigned char*) rgba, width, height, stride); + +} diff --git a/src/protocols/spice/channels/cursor.h b/src/protocols/spice/channels/cursor.h new file mode 100644 index 00000000..6effed74 --- /dev/null +++ b/src/protocols/spice/channels/cursor.h @@ -0,0 +1,101 @@ +/* + * 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_SPICE_CURSOR_H +#define GUAC_SPICE_CURSOR_H + +#include "config.h" + +#include + +/** + * The callback function that is executed when the cursor hide signal is + * received from the Spice server. + * + * @param channel + * The channel which received the cursor hide event. + * + * @param client + * The guac_client associated with this Spice session. + */ +void guac_spice_cursor_hide(SpiceCursorChannel* channel, guac_client* client); + +/** + * The callback function that is executed when the cursor move signal is + * received from the Spice server. + * + * @param channel + * The channel that received the cursor move event. + * + * @param x + * The x position of the cursor. + * + * @param y + * The y position of the cursor. + * + * @param client + * The guac_client associated with this Spice session. + */ +void guac_spice_cursor_move(SpiceCursorChannel* channel, int x, int y, + guac_client* client); + +/** + * The callback function that is executed in response to the cursor reset + * signal, resetting the cursor to the default context. + * + * @param channel + * The channel that received the cursor reset signal. + * + * @param client + * The guac_client associated with this Spice session. + */ +void guac_spice_cursor_reset(SpiceCursorChannel* channel, guac_client* client); + +/** + * The callback function that is executed in response to receiving the cursor + * set signal from the Spice server, which sets the width, height, and image + * of the cursor, and the x and y coordinates of the cursor hotspot. + * + * @param channel + * The channel that received the cursor set signal. + * + * @param width + * The width of the cursor image. + * + * @param height + * The height of the cursor image. + * + * @param x + * The x coordinate of the cursor hotspot. + * + * @param y + * The y coordinate of the cursor hotspot. + * + * @param rgba + * A pointer to the memory region containing the image data for the cursor, + * or NULL if the default cursor image should be used. + * + * @param client + * The guac_client associated with this Spice session. + */ +void guac_spice_cursor_set(SpiceCursorChannel* channel, int width, int height, + int x, int y, gpointer* rgba, guac_client* client); + +#endif /* GUAC_SPICE_CURSOR_H */ + diff --git a/src/protocols/spice/channels/display.c b/src/protocols/spice/channels/display.c new file mode 100644 index 00000000..75400499 --- /dev/null +++ b/src/protocols/spice/channels/display.c @@ -0,0 +1,167 @@ +/* + * 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 "client.h" +#include "common/display.h" +#include "common/iconv.h" +#include "common/surface.h" +#include "spice.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +void guac_spice_client_display_update(SpiceChannel* channel, int x, + int y, int w, int h, guac_client* client) { + + guac_client_log(client, GUAC_LOG_TRACE, + "Received request to update Spice display: %d, %d, %d, %d", + x, y, w, h); + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Retrieve the primary display buffer */ + SpiceDisplayPrimary primary; + if (spice_display_channel_get_primary(channel, 0, &primary)) { + + /* Create cairo surface from decoded buffer */ + cairo_surface_t* surface = cairo_image_surface_create_for_data( + primary.data, + CAIRO_FORMAT_RGB24, + primary.width, + primary.height, + primary.stride); + + /* A region smaller than the entire display should be updated. */ + if ((x > 0 || y > 0 ) && (w < primary.width || h < primary.height)) { + + cairo_surface_t* updateArea = cairo_surface_create_similar_image(surface, CAIRO_FORMAT_RGB24, w, h); + cairo_t* updateContext = cairo_create(updateArea); + cairo_set_operator(updateContext, CAIRO_OPERATOR_SOURCE); + cairo_set_source_surface(updateContext, surface, 0 - x, 0 - y); + cairo_rectangle(updateContext, 0, 0, w, h); + cairo_fill(updateContext); + guac_common_surface_draw(spice_client->display->default_surface, x, y, updateArea); + cairo_surface_destroy(updateArea); + + } + + /* The entire display should be updated. */ + else { + guac_common_surface_draw(spice_client->display->default_surface, + 0, 0, surface); + } + + /* Free surfaces */ + cairo_surface_destroy(surface); + } + + /* Flush surface and mark end of frame. */ + guac_common_surface_flush(spice_client->display->default_surface); + guac_client_end_frame(client); + guac_socket_flush(client->socket); +} + +void guac_spice_client_display_gl_draw(SpiceChannel* channel, int x, + int y, int w, int h, guac_client* client) { + + guac_client_log(client, GUAC_LOG_TRACE, "Received GL draw request."); + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Copy specified rectangle within default layer */ + guac_common_surface_copy(spice_client->display->default_surface, + x, y, w, h, + spice_client->display->default_surface, x, y); + + +} + +void guac_spice_client_display_mark(SpiceChannel* channel, gint mark, + guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, + "Received signal to mark display, which currently has no effect."); + +} + +void guac_spice_client_display_primary_create(SpiceChannel* channel, + gint format, gint width, gint height, gint stride, gint shmid, + gpointer imgdata, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Received request to create primary display."); + + /* Allocate the Guacamole display. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + spice_client->display = guac_common_display_alloc(client, + width, height); + + /* Create a matching Cairo image surface. */ + guac_client_log(client, GUAC_LOG_TRACE, "Creating Cairo image surface."); + cairo_surface_t* surface = cairo_image_surface_create_for_data(imgdata, CAIRO_FORMAT_RGB24, + width, height, stride); + + /* Draw directly to default layer */ + guac_client_log(client, GUAC_LOG_TRACE, "Drawing to the default surface."); + guac_common_surface_draw(spice_client->display->default_surface, + 0, 0, surface); + + /* Flush the default surface. */ + guac_client_log(client, GUAC_LOG_TRACE, "Flushing the default surface."); + guac_common_surface_flush(spice_client->display->default_surface); + + /* Mark the end of the frame and flush the socket. */ + guac_client_end_frame(client); + guac_socket_flush(client->socket); + + +} + +void guac_spice_client_display_primary_destroy(SpiceChannel* channel, + guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Received request to destroy the primary display."); + + /* Free the Guacamole display. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_common_display_free(spice_client->display); + +} + +void* guac_spice_client_streaming_handler(SpiceChannel* channel, + gboolean streaming_mode, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Received call to streaming handler."); + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + return spice_client->display; + +} + + diff --git a/src/protocols/spice/channels/display.h b/src/protocols/spice/channels/display.h new file mode 100644 index 00000000..13462979 --- /dev/null +++ b/src/protocols/spice/channels/display.h @@ -0,0 +1,187 @@ +/* + * 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_SPICE_DISPLAY_H +#define GUAC_SPICE_DISPLAY_H + +#include "config.h" + +#include + +/* Define cairo_format_stride_for_width() if missing */ +#ifndef HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH +#define cairo_format_stride_for_width(format, width) (width*4) +#endif + +/** + * Callback invoked by the Spice library when it receives a new binary image + * data from the Spice server. The image itself will be stored in the designated + * sub-rectangle of client->framebuffer. + * + * @param channel + * The SpiceDisplayChannel that received the update event. + * + * @param x + * The X coordinate of the upper-left corner of the destination rectangle + * in which the image should be drawn, in pixels. + * + * @param y + * The Y coordinate of the upper-left corner of the destination rectangle + * in which the image should be drawn, in pixels. + * + * @param w + * The width of the image, in pixels. + * + * @param h + * The height of the image, in pixels. + * + * @param guac_client + * The guac_client associated with the event. + */ +void guac_spice_client_display_update(SpiceDisplayChannel* channel, int x, + int y, int w, int h, guac_client* client); + +/** + * The callback function invoked when the RED_DISPLAY_MARK command is received + * from the Spice server and the display should be exposed. + * + * @param channel + * The SpiceDisplayChannel on which the event was received. + * + * @param mark + * Non-zero when the display mark has been received. + * + * @param client + * The guac_client associated with this channel and event. + */ +void guac_spice_client_display_mark(SpiceDisplayChannel* channel, gint mark, + guac_client* client); + +/** + * The callback function invoked when primary display buffer data is sent from + * the Spice server to the client. + * + * @param channel + * The SpiceDisplayChannel on which this event was received. + * + * @param format + * The Spice format of the received data. + * + * @param width + * The total width of the display. + * + * @param height + * The total height of the display. + * + * @param stride + * The buffer width padding. + * + * @param shmid + * The identifier of the shared memory segment associated with the data, or + * -1 if shared memory is not in use. + * + * @param imgdata + * A pointer to the buffer containing the surface data. + * + * @param client + * The guac_client associated with this channel/event. + */ +void guac_spice_client_display_primary_create(SpiceDisplayChannel* channel, + gint format, gint width, gint height, gint stride, gint shmid, + gpointer imgdata, guac_client* client); + +/** + * The callback function invoked by the client when the primary surface is + * destroyed and should no longer be accessed. + * + * @param channel + * The SpiceDisplayChannel on which the primary surface destroy event was + * received. + * + * @param client + * The guac_client associated with this channel/event. + */ +void guac_spice_client_display_primary_destroy(SpiceDisplayChannel* channel, + guac_client* client); + +/** + * Callback function that is invoked when a channel specifies that a display + * is marked as active and should be exposed. + * + * @param channel + * The channel on which the mark was received. + * + * @param marked + * TRUE if the display should be marked; otherwise false. + * + * @param client + * The guac_client associated with the channel/event. + */ +void guac_spice_client_display_mark(SpiceDisplayChannel* channel, + gboolean marked, guac_client* client); + +/** + * Callback invoked by the Spice client when it receives a CopyRect message. + * CopyRect specified a rectangle of source data within the display and a + * set of X/Y coordinates to which that rectangle should be copied. + * + * @param channel + * The SpiceDisplayChannel that received the gl_draw event. + * + * @param x + * The X coordinate of the upper-left corner of the source rectangle + * from which the image data should be copied, in pixels. + * + * @param y + * The Y coordinate of the upper-left corner of the source rectangle + * from which the image data should be copied, in pixels. + * + * @param w + * The width of the source and destination rectangles, in pixels. + * + * @param h + * The height of the source and destination rectangles, in pixels. + * + * @param guac_client + * The guac_client associated with this gl_draw event. + */ +void guac_spice_client_display_gl_draw(SpiceDisplayChannel* channel, int x, + int y, int w, int h, guac_client* client); + +/** + * The callback function invoked by the client when it receives a request to + * change streaming mode. + * + * @param channel + * The SpiceDisplayChannel that received the streaming mode change request. + * + * @param streaming_mode + * TRUE if the display channel should be in streaming mode; otherwise FALSE. + * + * @param guac_client + * The guac_client associated with this event. + * + * @return + * A pointer to the display. + */ +void* guac_spice_client_streaming_handler(SpiceDisplayChannel* channel, + gboolean streaming_mode, guac_client* client); + +#endif /* GUAC_SPICE_DISPLAY_H */ + diff --git a/src/protocols/spice/channels/file-download.c b/src/protocols/spice/channels/file-download.c new file mode 100644 index 00000000..1cda4c16 --- /dev/null +++ b/src/protocols/spice/channels/file-download.c @@ -0,0 +1,321 @@ +/* + * 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 "common/json.h" +#include "file-download.h" +#include "file-ls.h" +#include "file.h" +#include "spice.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +void* guac_spice_file_download_monitor(void* data) { + + guac_spice_folder* folder = (guac_spice_folder*) data; + char download_path[GUAC_SPICE_FOLDER_MAX_PATH]; + char download_events[GUAC_SPICE_FOLDER_MAX_EVENTS]; + char file_path[GUAC_SPICE_FOLDER_MAX_PATH]; + const struct inotify_event *event; + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Starting up file monitor thread.", __func__); + + /* If folder has already been freed, or isn't open, yet, don't do anything. */ + if (folder == NULL) + return NULL; + + download_path[0] = '\0'; + guac_strlcat(download_path, folder->path, GUAC_SPICE_FOLDER_MAX_PATH); + guac_strlcat(download_path, "/Download", GUAC_SPICE_FOLDER_MAX_PATH); + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Watching folder at path \"%s\".", __func__, download_path); + + int notify = inotify_init(); + + if (notify == -1) { + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: Failed to start inotify, automatic downloads will not work: %s", + __func__, strerror(errno)); + return NULL; + } + + if(inotify_add_watch(notify, download_path, IN_CREATE | IN_ATTRIB | IN_CLOSE_WRITE | IN_MOVED_TO | IN_ONLYDIR | IN_EXCL_UNLINK) == -1) { + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: Failed to set inotify flags for \"%s\".", + __func__, download_path); + return NULL; + } + + while (true) { + int events = read(notify, download_events, sizeof(download_events)); + if (events == -1 && errno != EAGAIN) { + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: Failed to read inotify events: %s", + __func__, strerror(errno)); + return NULL; + } + + if (events <= 0) + continue; + + + for (char* ptr = download_events; ptr < download_events + events; ptr += sizeof(struct inotify_event) + event->len) { + + event = (const struct inotify_event *) ptr; + + if (event->mask & IN_ISDIR) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Ignoring event 0x%x for directory %s.", __func__, event->mask, event->name); + continue; + } + + guac_client_log(folder->client, GUAC_LOG_ERROR, + "%s: 0x%x - Downloading the file: %s", __func__, event->mask, event->name, event->cookie); + + file_path[0] = '\0'; + guac_strlcat(file_path, "/Download/", GUAC_SPICE_FOLDER_MAX_PATH); + guac_strlcat(file_path, event->name, GUAC_SPICE_FOLDER_MAX_PATH); + // guac_client_for_owner(folder->client, guac_spice_file_download_to_user, file_path); + //int fileid = guac_spice_folder_open(folder, file_path, O_WRONLY, 0, 0); + // guac_spice_folder_delete(folder, fileid); + + + } + + } + + return NULL; + +} + +int guac_spice_file_download_ack_handler(guac_user* user, guac_stream* stream, + char* message, guac_protocol_status status) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_file_download_status* download_status = (guac_spice_file_download_status*) stream->data; + + /* Get folder, return error if no folder */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* If successful, read data */ + if (status == GUAC_PROTOCOL_STATUS_SUCCESS) { + + /* Attempt read into buffer */ + char buffer[4096]; + int bytes_read = guac_spice_folder_read(folder, + download_status->file_id, + download_status->offset, buffer, sizeof(buffer)); + + /* If bytes read, send as blob */ + if (bytes_read > 0) { + download_status->offset += bytes_read; + guac_protocol_send_blob(user->socket, stream, + buffer, bytes_read); + } + + /* If EOF, send end */ + else if (bytes_read == 0) { + guac_protocol_send_end(user->socket, stream); + guac_user_free_stream(user, stream); + free(download_status); + } + + /* Otherwise, fail stream */ + else { + guac_user_log(user, GUAC_LOG_ERROR, + "Error reading file for download"); + guac_protocol_send_end(user->socket, stream); + guac_user_free_stream(user, stream); + free(download_status); + } + + guac_socket_flush(user->socket); + + } + + /* Otherwise, return stream to user */ + else + guac_user_free_stream(user, stream); + + return 0; + +} + +int guac_spice_file_download_get_handler(guac_user* user, guac_object* object, + char* name) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + int flags = 0; + + /* Get folder, ignore request if no folder */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) + return 0; + + flags |= O_RDONLY; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: folder->path=%s, name=%s", __func__, folder->path, name); + + /* Attempt to open file for reading */ + int file_id = guac_spice_folder_open(folder, name, flags, 0, 0); + if (file_id < 0) { + guac_user_log(user, GUAC_LOG_INFO, "Unable to read file \"%s\"", + name); + return 0; + } + + /* Get opened file */ + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Successful open produced bad file_id: %i", + __func__, file_id); + return 0; + } + + /* If directory, send contents of directory */ + if (S_ISDIR(file->stmode)) { + + /* Create stream data */ + guac_spice_file_ls_status* ls_status = malloc(sizeof(guac_spice_file_ls_status)); + ls_status->folder = folder; + ls_status->file_id = file_id; + guac_strlcpy(ls_status->directory_name, name, + sizeof(ls_status->directory_name)); + + /* Allocate stream for body */ + guac_stream* stream = guac_user_alloc_stream(user); + stream->ack_handler = guac_spice_file_ls_ack_handler; + stream->data = ls_status; + + /* Init JSON object state */ + guac_common_json_begin_object(user, stream, + &ls_status->json_state); + + /* Associate new stream with get request */ + guac_protocol_send_body(user->socket, object, stream, + GUAC_USER_STREAM_INDEX_MIMETYPE, name); + + } + + /* Otherwise, send file contents if downloads are allowed */ + else if (!folder->disable_download) { + + /* Create stream data */ + guac_spice_file_download_status* download_status = malloc(sizeof(guac_spice_file_download_status)); + download_status->file_id = file_id; + download_status->offset = 0; + + /* Allocate stream for body */ + guac_stream* stream = guac_user_alloc_stream(user); + stream->data = download_status; + stream->ack_handler = guac_spice_file_download_ack_handler; + + /* Associate new stream with get request */ + guac_protocol_send_body(user->socket, object, stream, + "application/octet-stream", name); + + } + + else + guac_client_log(client, GUAC_LOG_INFO, "Unable to download file " + "\"%s\", file downloads have been disabled.", name); + + guac_socket_flush(user->socket); + return 0; +} + +void* guac_spice_file_download_to_user(guac_user* user, void* data) { + + /* Do not bother attempting the download if the user has left */ + if (user == NULL) + return NULL; + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_folder* folder = spice_client->shared_folder; + int flags = 0; + + /* Ignore download if folder has been unloaded */ + if (folder == NULL) + return NULL; + + /* Ignore download if downloads have been disabled */ + if (folder->disable_download) { + guac_client_log(client, GUAC_LOG_WARNING, "A download attempt has " + "been blocked due to downloads being disabled, however it " + "should have been blocked at a higher level. This is likely " + "a bug."); + return NULL; + } + + /* Attempt to open requested file */ + char* path = (char*) data; + flags |= O_RDONLY; + int file_id = guac_spice_folder_open(folder, path, + flags, 0, 0); + + /* If file opened successfully, start stream */ + if (file_id >= 0) { + + /* Associate stream with transfer status */ + guac_stream* stream = guac_user_alloc_stream(user); + guac_spice_file_download_status* download_status = malloc(sizeof(guac_spice_file_download_status)); + stream->data = download_status; + stream->ack_handler = guac_spice_file_download_ack_handler; + download_status->file_id = file_id; + download_status->offset = 0; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: Initiating download " + "of \"%s\"", __func__, path); + + /* Begin stream */ + guac_protocol_send_file(user->socket, stream, + "application/octet-stream", guac_spice_folder_basename(path)); + guac_socket_flush(user->socket); + + /* Download started successfully */ + return stream; + + } + + /* Download failed */ + guac_user_log(user, GUAC_LOG_ERROR, "Unable to download \"%s\"", path); + return NULL; + +} + diff --git a/src/protocols/spice/channels/file-download.h b/src/protocols/spice/channels/file-download.h new file mode 100644 index 00000000..b3b518be --- /dev/null +++ b/src/protocols/spice/channels/file-download.h @@ -0,0 +1,83 @@ +/* + * 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_SPICE_FILE_DOWNLOAD_H +#define GUAC_SPICE_FILE_DOWNLOAD_H + +#include "common/json.h" + +#include +#include +#include + +#include + +/** + * The transfer status of a file being downloaded. + */ +typedef struct guac_spice_file_download_status { + + /** + * The file ID of the file being downloaded. + */ + int file_id; + + /** + * The current position within the file. + */ + uint64_t offset; + +} guac_spice_file_download_status; + +/** + * Function which uses Linux's inotify facility to monitor the "Download" + * directory of a shared folder for changes and trigger the automatic download + * of that data to the Guacamole user who has access to the shared folder. + * + * @param data + * A pointer to the guac_spice_folder structure in which the Download + * folder is located. + * + * @return + * Always NULL + */ +void* guac_spice_file_download_monitor(void* data); + +/** + * Handler for acknowledgements of receipt of data related to file downloads. + */ +guac_user_ack_handler guac_spice_file_download_ack_handler; + +/** + * Handler for get messages. In context of downloads and the filesystem exposed + * via the Guacamole protocol, get messages request the body of a file within + * the filesystem. + */ +guac_user_get_handler guac_spice_file_download_get_handler; + +/** + * Callback for guac_client_for_user() and similar functions which initiates a + * file download to a specific user if that user is still connected. The path + * for the file to be downloaded must be passed as the arbitrary data parameter + * for the function invoking this callback. + */ +guac_user_callback guac_spice_file_download_to_user; + +#endif + diff --git a/src/protocols/spice/channels/file-ls.c b/src/protocols/spice/channels/file-ls.c new file mode 100644 index 00000000..b6bee65f --- /dev/null +++ b/src/protocols/spice/channels/file-ls.c @@ -0,0 +1,127 @@ +/* + * 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 "file.h" +#include "file-ls.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +int guac_spice_file_ls_ack_handler(guac_user* user, guac_stream* stream, + char* message, guac_protocol_status status) { + + int blob_written = 0; + const char* filename; + + guac_spice_file_ls_status* ls_status = (guac_spice_file_ls_status*) stream->data; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: folder=\"%s\"", __func__, ls_status->folder->path); + + /* If unsuccessful, free stream and abort */ + if (status != GUAC_PROTOCOL_STATUS_SUCCESS) { + guac_spice_folder_close(ls_status->folder, ls_status->file_id); + guac_user_free_stream(user, stream); + free(ls_status); + return 0; + } + + /* While directory entries remain */ + while ((filename = guac_spice_folder_read_dir(ls_status->folder, + ls_status->file_id)) != NULL + && !blob_written) { + + char absolute_path[GUAC_SPICE_FOLDER_MAX_PATH]; + + /* Skip current and parent directory entries */ + if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) + continue; + + /* Concatenate into absolute path - skip if invalid */ + if (!guac_spice_folder_append_filename(absolute_path, + ls_status->directory_name, filename)) { + + guac_user_log(user, GUAC_LOG_DEBUG, + "Skipping filename \"%s\" - filename is invalid or " + "resulting path is too long", filename); + + continue; + } + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: absolute_path=\"%s\"", __func__, absolute_path); + + /* Attempt to open file to determine type */ + int flags = (0 | O_RDONLY); + int file_id = guac_spice_folder_open(ls_status->folder, absolute_path, + flags, 0, 0); + if (file_id < 0) + continue; + + /* Get opened file */ + guac_spice_folder_file* file = guac_spice_folder_get_file(ls_status->folder, file_id); + if (file == NULL) { + guac_user_log(user, GUAC_LOG_DEBUG, "%s: Successful open produced " + "bad file_id: %i", __func__, file_id); + return 0; + } + + /* Determine mimetype */ + const char* mimetype; + if (S_ISDIR(file->stmode)) + mimetype = GUAC_USER_STREAM_INDEX_MIMETYPE; + else + mimetype = "application/octet-stream"; + + /* Write entry */ + blob_written |= guac_common_json_write_property(user, stream, + &ls_status->json_state, absolute_path, mimetype); + + guac_spice_folder_close(ls_status->folder, file_id); + + } + + /* Complete JSON and cleanup at end of directory */ + if (filename == NULL) { + + /* Complete JSON object */ + guac_common_json_end_object(user, stream, &ls_status->json_state); + guac_common_json_flush(user, stream, &ls_status->json_state); + + /* Clean up resources */ + guac_spice_folder_close(ls_status->folder, ls_status->file_id); + free(ls_status); + + /* Signal of stream */ + guac_protocol_send_end(user->socket, stream); + guac_user_free_stream(user, stream); + + } + + guac_socket_flush(user->socket); + return 0; + +} + diff --git a/src/protocols/spice/channels/file-ls.h b/src/protocols/spice/channels/file-ls.h new file mode 100644 index 00000000..f9d00c1f --- /dev/null +++ b/src/protocols/spice/channels/file-ls.h @@ -0,0 +1,66 @@ +/* + * 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_LS_H +#define GUAC_RDP_LS_H + +#include "common/json.h" +#include "file.h" + +#include +#include +#include + +#include + +/** + * The current state of a directory listing operation. + */ +typedef struct guac_spice_file_ls_status { + + /** + * The filesystem associated with the directory being listed. + */ + guac_spice_folder* folder; + + /** + * The file ID of the directory being listed. + */ + int file_id; + + /** + * The absolute path of the directory being listed. + */ + char directory_name[GUAC_SPICE_FOLDER_MAX_PATH]; + + /** + * The current state of the JSON directory object being written. + */ + guac_common_json_state json_state; + +} guac_spice_file_ls_status; + +/** + * Handler for ack messages received due to receipt of a "body" or "blob" + * instruction associated with a directory list operation. + */ +guac_user_ack_handler guac_spice_file_ls_ack_handler; + +#endif + diff --git a/src/protocols/spice/channels/file-upload.c b/src/protocols/spice/channels/file-upload.c new file mode 100644 index 00000000..8a2b1575 --- /dev/null +++ b/src/protocols/spice/channels/file-upload.c @@ -0,0 +1,261 @@ +/* + * 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 "file.h" +#include "spice.h" +#include "file-upload.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/** + * Writes the given filename to the given upload path, sanitizing the filename + * and translating the filename to the root directory. + * + * @param filename + * The filename to sanitize and move to the root directory. + * + * @param path + * A pointer to a buffer which should receive the sanitized path. The + * buffer must have at least GUAC_RDP_FS_MAX_PATH bytes available. + */ +static void __generate_upload_path(const char* filename, char* path) { + + int i; + + /* Add initial backslash */ + *(path++) = '\\'; + + for (i=1; iclient; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + int file_id; + char file_path[GUAC_SPICE_FOLDER_MAX_PATH]; + + /* Get filesystem, return error if no filesystem */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FS)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Ignore upload if uploads have been disabled */ + if (folder->disable_upload) { + guac_client_log(client, GUAC_LOG_WARNING, "A upload attempt has " + "been blocked due to uploads being disabled, however it " + "should have been blocked at a higher level. This is likely " + "a bug."); + guac_protocol_send_ack(user->socket, stream, "FAIL (UPLOAD DISABLED)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Translate name */ + __generate_upload_path(filename, file_path); + + /* Open file */ + file_id = guac_spice_folder_open(folder, file_path, (O_WRONLY | O_CREAT | O_TRUNC), + 1, 0); + if (file_id < 0) { + guac_protocol_send_ack(user->socket, stream, "FAIL (CANNOT OPEN)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Init upload status */ + guac_spice_file_upload_status* upload_status = malloc(sizeof(guac_spice_file_upload_status)); + upload_status->offset = 0; + upload_status->file_id = file_id; + stream->data = upload_status; + stream->blob_handler = guac_spice_file_upload_blob_handler; + stream->end_handler = guac_spice_file_upload_end_handler; + + guac_protocol_send_ack(user->socket, stream, "OK (STREAM BEGIN)", + GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + return 0; + +} + +int guac_spice_file_upload_blob_handler(guac_user* user, guac_stream* stream, + void* data, int length) { + + int bytes_written; + guac_spice_file_upload_status* upload_status = (guac_spice_file_upload_status*) stream->data; + + /* Get filesystem, return error if no filesystem */ + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Write entire block */ + while (length > 0) { + + /* Attempt write */ + bytes_written = guac_spice_folder_write(folder, upload_status->file_id, + upload_status->offset, data, length); + + /* On error, abort */ + if (bytes_written < 0) { + guac_protocol_send_ack(user->socket, stream, + "FAIL (BAD WRITE)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Update counters */ + upload_status->offset += bytes_written; + data = (char *)data + bytes_written; + length -= bytes_written; + + } + + guac_protocol_send_ack(user->socket, stream, "OK (DATA RECEIVED)", + GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + return 0; + +} + +int guac_spice_file_upload_end_handler(guac_user* user, guac_stream* stream) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_file_upload_status* upload_status = (guac_spice_file_upload_status*) stream->data; + + /* Get folder, return error if no filesystem */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Close file */ + guac_spice_folder_close(folder, upload_status->file_id); + + /* Acknowledge stream end */ + guac_protocol_send_ack(user->socket, stream, "OK (STREAM END)", + GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + + free(upload_status); + return 0; + +} + +int guac_spice_file_upload_put_handler(guac_user* user, guac_object* object, + guac_stream* stream, char* mimetype, char* name) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Get folder, return error if no filesystem */ + guac_spice_folder* folder = spice_client->shared_folder; + if (folder == NULL) { + guac_protocol_send_ack(user->socket, stream, "FAIL (NO FOLDER)", + GUAC_PROTOCOL_STATUS_SERVER_ERROR); + guac_socket_flush(user->socket); + return 0; + } + + /* Ignore upload if uploads have been disabled */ + if (folder->disable_upload) { + guac_client_log(client, GUAC_LOG_WARNING, "A upload attempt has " + "been blocked due to uploads being disabled, however it " + "should have been blocked at a higher level. This is likely " + "a bug."); + guac_protocol_send_ack(user->socket, stream, "FAIL (UPLOAD DISABLED)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Open file */ + int file_id = guac_spice_folder_open(folder, name, (O_WRONLY | O_CREAT | O_TRUNC), + 1, 0); + + /* Abort on failure */ + if (file_id < 0) { + guac_protocol_send_ack(user->socket, stream, "FAIL (CANNOT OPEN)", + GUAC_PROTOCOL_STATUS_CLIENT_FORBIDDEN); + guac_socket_flush(user->socket); + return 0; + } + + /* Init upload stream data */ + guac_spice_file_upload_status* upload_status = malloc(sizeof(guac_spice_file_upload_status)); + upload_status->offset = 0; + upload_status->file_id = file_id; + + /* Allocate stream, init for file upload */ + stream->data = upload_status; + stream->blob_handler = guac_spice_file_upload_blob_handler; + stream->end_handler = guac_spice_file_upload_end_handler; + + /* Acknowledge stream creation */ + guac_protocol_send_ack(user->socket, stream, "OK (STREAM BEGIN)", + GUAC_PROTOCOL_STATUS_SUCCESS); + guac_socket_flush(user->socket); + return 0; +} + diff --git a/src/protocols/spice/channels/file-upload.h b/src/protocols/spice/channels/file-upload.h new file mode 100644 index 00000000..d17e170b --- /dev/null +++ b/src/protocols/spice/channels/file-upload.h @@ -0,0 +1,72 @@ +/* + * 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_SPICE_FILE_UPLOAD_H +#define GUAC_SPICE_FILE_UPLOAD_H + +#include "common/json.h" + +#include +#include +#include + +#include + +/** + * Structure which represents the current state of an upload. + */ +typedef struct guac_spice_file_upload_status { + + /** + * The overall offset within the file that the next write should + * occur at. + */ + uint64_t offset; + + /** + * The ID of the file being written to. + */ + int file_id; + +} guac_spice_file_upload_status; + +/** + * Handler for inbound files related to file uploads. + */ +guac_user_file_handler guac_spice_file_upload_file_handler; + +/** + * Handler for stream data related to file uploads. + */ +guac_user_blob_handler guac_spice_file_upload_blob_handler; + +/** + * Handler for end-of-stream related to file uploads. + */ +guac_user_end_handler guac_spice_file_upload_end_handler; + +/** + * Handler for put messages. In context of uploads and the filesystem exposed + * via the Guacamole protocol, put messages request write access to a file + * within the filesystem. + */ +guac_user_put_handler guac_spice_file_upload_put_handler; + +#endif + diff --git a/src/protocols/spice/channels/file.c b/src/protocols/spice/channels/file.c new file mode 100644 index 00000000..4a409242 --- /dev/null +++ b/src/protocols/spice/channels/file.c @@ -0,0 +1,701 @@ +/* + * 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 "file.h" +#include "file-download.h" +#include "file-ls.h" +#include "file-upload.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Translates an absolute path for a shared folder to an absolute path which is + * within the real "shared folder" path specified in the connection settings. + * No checking is performed on the path provided, which is assumed to have + * already been normalized and validated as absolute. + * + * @param folder + * The folder containing the file whose path is being translated. + * + * @param virtual_path + * The absolute path to the file on the simulated folder, relative to the + * shared folder root. + * + * @param real_path + * The buffer in which to store the absolute path to the real file on the + * local filesystem. + */ +static void __guac_spice_folder_translate_path(guac_spice_folder* folder, + const char* virtual_path, char* real_path) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: virtual_path=\"%s\", drive_path=\"%s\"", __func__, virtual_path, folder->path); + + /* Get drive path */ + char* path = folder->path; + + int i; + + /* Start with path from settings */ + for (i=0; iclient, GUAC_LOG_DEBUG, "%s: virtual_path=\"%s\", real_path=\"%s\"", __func__, virtual_path, real_path); + +} + +guac_spice_folder* guac_spice_folder_alloc(guac_client* client, const char* folder_path, + int create_folder, int disable_download, int disable_upload) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Initializing shared folder at " + "\"%s\".", folder_path); + + /* Create folder if it does not exist */ + if (create_folder) { + guac_client_log(client, GUAC_LOG_DEBUG, + "%s: Creating folder \"%s\" if necessary.", + __func__, folder_path); + + /* Log error if directory creation fails */ + if (mkdir(folder_path, S_IRWXU) && errno != EEXIST) { + guac_client_log(client, GUAC_LOG_ERROR, + "Unable to create folder \"%s\": %s", + folder_path, strerror(errno)); + } + } + + guac_spice_folder* folder = malloc(sizeof(guac_spice_folder)); + + folder->client = client; + folder->path = strdup(folder_path); + folder->file_id_pool = guac_pool_alloc(0); + folder->open_files = 0; + folder->disable_download = disable_download; + folder->disable_upload = disable_upload; + + /* Set up Download directory and watch it. */ + if (!disable_download) { + + guac_client_log(client, GUAC_LOG_DEBUG, "%s: Setting up Download/ folder watch.", __func__); + + if (create_folder) { + guac_client_log(client, GUAC_LOG_DEBUG, "%s: Creating Download/ folder.", + __func__); + + char *download_path; + download_path = guac_strdup(folder_path); + + guac_strlcat(download_path, "/Download", GUAC_SPICE_FOLDER_MAX_PATH); + + if (mkdir(folder_path, S_IRWXU) && errno != EEXIST) { + guac_client_log(client, GUAC_LOG_ERROR, + "%s: Unable to create folder \"%s\": %s", __func__, + download_path, strerror(errno)); + } + + } + + if(pthread_create(&(folder->download_thread), NULL, guac_spice_file_download_monitor, (void*) folder)) { + guac_client_log(client, GUAC_LOG_ERROR, + "%s: Unable to create Download folder thread monitor.", __func__); + } + + } + + return folder; + +} + +void guac_spice_folder_free(guac_spice_folder* folder) { + guac_pool_free(folder->file_id_pool); + free(folder->path); + free(folder); +} + +guac_object* guac_spice_folder_alloc_object(guac_spice_folder *folder, guac_user* user) { + + /* Init folder */ + guac_object* folder_object = guac_user_alloc_object(user); + folder_object->get_handler = guac_spice_file_download_get_handler; + + /* Assign upload handler only if uploads are not disabled. */ + if (!folder->disable_upload) + folder_object->put_handler = guac_spice_file_upload_put_handler; + + folder_object->data = folder; + + /* Send filesystem to user */ + guac_protocol_send_filesystem(user->socket, folder_object, "Shared Folder"); + guac_socket_flush(user->socket); + + return folder_object; + +} + +int guac_spice_folder_append_filename(char* fullpath, const char* path, + const char* filename) { + + int i; + + /* Disallow "." as a filename */ + if (strcmp(filename, ".") == 0) + return 0; + + /* Disallow ".." as a filename */ + if (strcmp(filename, "..") == 0) + return 0; + + /* Copy path, append trailing slash */ + for (i=0; i 0 && path[i-1] != '/' && path[i-1] != '\\') + fullpath[i++] = '/'; + break; + } + + /* Copy character if not end of string */ + fullpath[i] = c; + + } + + /* Append filename */ + for (; iclient, GUAC_LOG_DEBUG, + "%s: Ignoring close for bad file_id: %i", + __func__, file_id); + return; + } + + file = &(folder->files[file_id]); + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Closed \"%s\" (file_id=%i)", + __func__, file->absolute_path, file_id); + + /* Close directory, if open */ + if (file->dir != NULL) + closedir(file->dir); + + /* Close file */ + close(file->fd); + + /* Free paths */ + free(file->absolute_path); + free(file->real_path); + + /* Free ID back to pool */ + guac_pool_free_int(folder->file_id_pool, file_id); + folder->open_files--; + +} + +int guac_spice_folder_delete(guac_spice_folder* folder, int file_id) { + + /* Get file */ + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Delete of bad file_id: %i", __func__, file_id); + return GUAC_SPICE_FOLDER_EINVAL; + } + + /* If directory, attempt removal */ + if (S_ISDIR(file->stmode)) { + if (rmdir(file->real_path)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: rmdir() failed: \"%s\"", __func__, file->real_path); + return guac_spice_folder_get_errorcode(errno); + } + } + + /* Otherwise, attempt deletion */ + else if (unlink(file->real_path)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: unlink() failed: \"%s\"", __func__, file->real_path); + return guac_spice_folder_get_errorcode(errno); + } + + return 0; + +} + +void* guac_spice_folder_expose(guac_user* user, void* data) { + + guac_spice_folder* folder = (guac_spice_folder*) data; + + guac_user_log(user, GUAC_LOG_DEBUG, "%s: Exposing folder \"%s\" to user.", __func__, folder->path); + + /* No need to expose if there is no folder or the user has left */ + if (user == NULL || folder == NULL) + return NULL; + + /* Allocate and expose folder object for user */ + return guac_spice_folder_alloc_object(folder, user); + +} + +int guac_spice_folder_get_errorcode(int err) { + + /* Translate errno codes to GUAC_SPICE_FOLDER codes */ + switch(err) { + case ENFILE: + return GUAC_SPICE_FOLDER_ENFILE; + + case ENOENT: + return GUAC_SPICE_FOLDER_ENOENT; + + case ENOTDIR: + return GUAC_SPICE_FOLDER_ENOTDIR; + + case ENOSPC: + return GUAC_SPICE_FOLDER_ENOSPC; + + case EISDIR: + return GUAC_SPICE_FOLDER_EISDIR; + + case EACCES: + return GUAC_SPICE_FOLDER_EACCES; + + case EEXIST: + return GUAC_SPICE_FOLDER_EEXIST; + + case EINVAL: + return GUAC_SPICE_FOLDER_EINVAL; + + case ENOSYS: + return GUAC_SPICE_FOLDER_ENOSYS; + + case ENOTSUP: + return GUAC_SPICE_FOLDER_ENOTSUP; + + default: + return GUAC_SPICE_FOLDER_EINVAL; + + } + +} + +guac_spice_folder_file* guac_spice_folder_get_file(guac_spice_folder* folder, + int file_id) { + + /* Validate ID */ + if (file_id < 0 || file_id >= GUAC_SPICE_FOLDER_MAX_FILES) + return NULL; + + /* Return file at given ID */ + return &(folder->files[file_id]); + +} + +int guac_spice_folder_normalize_path(const char* path, char* abs_path) { + + int path_depth = 0; + const char* path_components[GUAC_SPICE_FOLDER_MAX_PATH_DEPTH]; + + /* If original path is not absolute, normalization fails */ + if (path[0] != '/') + return 1; + + /* Create scratch copy of path excluding leading slash (we will be + * replacing path separators with null terminators and referencing those + * substrings directly as path components) */ + char path_scratch[GUAC_SPICE_FOLDER_MAX_PATH - 1]; + int length = guac_strlcpy(path_scratch, path + 1, + sizeof(path_scratch)); + + /* Fail if provided path is too long */ + if (length >= sizeof(path_scratch)) + return 1; + + /* Locate all path components within path */ + const char* current_path_component = &(path_scratch[0]); + for (int i = 0; i <= length; i++) { + + /* If current character is a path separator, parse as component */ + char c = path_scratch[i]; + if (c == '/' || c == '\0') { + + /* Terminate current component */ + path_scratch[i] = '\0'; + + /* If component refers to parent, just move up in depth */ + if (strcmp(current_path_component, "..") == 0) { + if (path_depth > 0) + path_depth--; + } + + /* Otherwise, if component not current directory, add to list */ + else if (strcmp(current_path_component, ".") != 0 + && strcmp(current_path_component, "") != 0) { + + /* Fail normalization if path is too deep */ + if (path_depth >= GUAC_SPICE_FOLDER_MAX_PATH_DEPTH) + return 1; + + path_components[path_depth++] = current_path_component; + + } + + /* Update start of next component */ + current_path_component = &(path_scratch[i+1]); + + } /* end if separator */ + + /* We do not currently support named streams */ + else if (c == ':') + return 1; + + } /* end for each character */ + + /* Add leading slash for resulting absolute path */ + abs_path[0] = '/'; + + /* Append normalized components to path, separated by slashes */ + guac_strljoin(abs_path + 1, path_components, path_depth, + "/", GUAC_SPICE_FOLDER_MAX_PATH - 1); + + return 0; + +} + +int guac_spice_folder_open(guac_spice_folder* folder, const char* path, + int flags, bool overwrite, bool directory) { + + char real_path[GUAC_SPICE_FOLDER_MAX_PATH]; + char normalized_path[GUAC_SPICE_FOLDER_MAX_PATH]; + + struct stat file_stat; + int fd; + int file_id; + guac_spice_folder_file* file; + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: path=\"%s\", flags=0x%x, overwrite=0x%x, " + "directory=0x%x", __func__, path, flags, overwrite, directory); + + /* If no files available, return too many open */ + if (folder->open_files >= GUAC_SPICE_FOLDER_MAX_FILES) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Too many open files.", + __func__, path); + return GUAC_SPICE_FOLDER_ENFILE; + } + + /* If path empty, return an error */ + if (path[0] == '\0') + return GUAC_SPICE_FOLDER_EINVAL; + + /* If path is relative, the file does not exist */ + else if (path[0] != '\\' && path[0] != '/') { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Access denied - supplied path \"%s\" is relative.", + __func__, path); + return GUAC_SPICE_FOLDER_ENOENT; + } + + /* Translate access into flags */ + if (directory) + flags |= O_DIRECTORY; + + else if (overwrite) + flags |= O_TRUNC; + + /* Normalize path, return no-such-file if invalid */ + if (guac_spice_folder_normalize_path(path, normalized_path)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Normalization of path \"%s\" failed.", __func__, path); + return GUAC_SPICE_FOLDER_ENOENT; + } + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Normalized path \"%s\" to \"%s\".", + __func__, path, normalized_path); + + /* Translate normalized path to real path */ + __guac_spice_folder_translate_path(folder, normalized_path, real_path); + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Translated path \"%s\" to \"%s\".", + __func__, normalized_path, real_path); + + /* Create directory first, if necessary */ + if (directory && (flags & O_CREAT)) { + + /* Create directory */ + if (mkdir(real_path, S_IRWXU)) { + if (errno != EEXIST || (flags & O_EXCL)) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: mkdir() failed: %s", + __func__, strerror(errno)); + return guac_spice_folder_get_errorcode(errno); + } + } + + /* Unset O_CREAT and O_EXCL as directory must exist before open() */ + flags &= ~(O_CREAT | O_EXCL); + + } + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: native open: real_path=\"%s\", flags=0x%x", + __func__, real_path, flags); + + /* Open file */ + fd = open(real_path, flags, S_IRUSR | S_IWUSR); + + /* If file open failed as we're trying to write a dir, retry as read-only */ + if (fd == -1 && errno == EISDIR) { + flags &= ~(O_WRONLY | O_RDWR); + flags |= O_RDONLY; + fd = open(real_path, flags, S_IRUSR | S_IWUSR); + } + + if (fd == -1) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: open() failed: %s", __func__, strerror(errno)); + return guac_spice_folder_get_errorcode(errno); + } + + /* Get file ID, init file */ + file_id = guac_pool_next_int(folder->file_id_pool); + file = &(folder->files[file_id]); + file->id = file_id; + file->fd = fd; + file->dir = NULL; + file->dir_pattern[0] = '\0'; + file->absolute_path = strdup(normalized_path); + file->real_path = strdup(real_path); + file->bytes_written = 0; + + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Opened \"%s\" as file_id=%i", + __func__, normalized_path, file_id); + + /* Attempt to pull file information */ + if (fstat(fd, &file_stat) == 0) { + + /* Load size and times */ + file->size = file_stat.st_size; + file->ctime = file_stat.st_ctime; + file->mtime = file_stat.st_mtime; + file->atime = file_stat.st_atime; + file->stmode = file_stat.st_mode; + + } + + /* If information cannot be retrieved, fake it */ + else { + + /* Init information to 0, lacking any alternative */ + file->size = 0; + file->ctime = 0; + file->mtime = 0; + file->atime = 0; + file->stmode = 0; + + } + + folder->open_files++; + + return file_id; + +} + +int guac_spice_folder_read(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Attempt to read from file: %s", __func__, folder->path); + + int bytes_read; + + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Read from bad file_id: %i", __func__, file_id); + return GUAC_SPICE_FOLDER_EINVAL; + } + + /* Attempt read */ + lseek(file->fd, offset, SEEK_SET); + bytes_read = read(file->fd, buffer, length); + + /* Translate errno on error */ + if (bytes_read < 0) + return guac_spice_folder_get_errorcode(errno); + + return bytes_read; + +} + +const char* guac_spice_folder_read_dir(guac_spice_folder* folder, int file_id) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Attempt to read directory: %s", __func__, folder->path); + + guac_spice_folder_file* file; + + struct dirent* result; + + /* Only read if file ID is valid */ + if (file_id < 0 || file_id >= GUAC_SPICE_FOLDER_MAX_FILES) + return NULL; + + file = &(folder->files[file_id]); + + /* Open directory if not yet open, stop if error */ + if (file->dir == NULL) { + file->dir = fdopendir(file->fd); + if (file->dir == NULL) + return NULL; + } + + /* Read next entry, stop if error or no more entries */ + if ((result = readdir(file->dir)) == NULL) + return NULL; + + /* Return filename */ + return result->d_name; + +} + +int guac_spice_folder_write(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length) { + + guac_client_log(folder->client, GUAC_LOG_DEBUG, "%s: Attempt to write file: %s", __func__, folder->path); + + int bytes_written; + + guac_spice_folder_file* file = guac_spice_folder_get_file(folder, file_id); + if (file == NULL) { + guac_client_log(folder->client, GUAC_LOG_DEBUG, + "%s: Write to bad file_id: %i", __func__, file_id); + return GUAC_SPICE_FOLDER_EINVAL; + } + + /* Attempt write */ + lseek(file->fd, offset, SEEK_SET); + bytes_written = write(file->fd, buffer, length); + + /* Translate errno on error */ + if (bytes_written < 0) + return guac_spice_folder_get_errorcode(errno); + + file->bytes_written += bytes_written; + return bytes_written; + +} + +void guac_spice_client_file_transfer_handler(SpiceMainChannel* main_channel, + SpiceFileTransferTask* task, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "File transfer handler."); + +} + +#include "file.h" \ No newline at end of file diff --git a/src/protocols/spice/channels/file.h b/src/protocols/spice/channels/file.h new file mode 100644 index 00000000..2905c753 --- /dev/null +++ b/src/protocols/spice/channels/file.h @@ -0,0 +1,461 @@ +/* + * 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_SPICE_FILE_H +#define GUAC_SPICE_FILE_H + +#include "config.h" + +#include "spice-constants.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/** + * An arbitrary file on the shared folder. + */ +typedef struct guac_spice_folder_file { + + /** + * The ID of this file. + */ + int id; + + /** + * The absolute path, including filename, of this file on the simulated filesystem. + */ + char* absolute_path; + + /** + * The real path, including filename, of this file on the local filesystem. + */ + char* real_path; + + /** + * Associated local file descriptor. + */ + int fd; + + /** + * Associated directory stream, if any. This field only applies + * if the file is being used as a directory. + */ + DIR* dir; + + /** + * The pattern the check directory contents against, if any. + */ + char dir_pattern[GUAC_SPICE_FOLDER_MAX_PATH]; + + /** + * The size of this file, in bytes. + */ + uint64_t size; + + /** + * The time this file was created, as a UNIX timestamp. + */ + uint64_t ctime; + + /** + * The time this file was last modified, as a UNIX timestamp. + */ + uint64_t mtime; + + /** + * The time this file was last accessed, as a UNIX timestamp. + */ + uint64_t atime; + + /** + * THe mode field of the file, as retrieved by a call to the stat() family + * of functions; + */ + mode_t stmode; + + /** + * The number of bytes written to the file. + */ + uint64_t bytes_written; + +} guac_spice_folder_file; + +/** + * A shared folder for the Spice protocol. + */ +typedef struct guac_spice_folder { + + /** + * The guac_client object this folder is associated with. + */ + guac_client* client; + + /** + * The path to the shared folder. + */ + char* path; + + /** + * The number of currently open files in the folder. + */ + int open_files; + + /** + * A pool of file IDs. + */ + guac_pool* file_id_pool; + + /** + * All available file structures. + */ + guac_spice_folder_file files[GUAC_SPICE_FOLDER_MAX_FILES]; + + /** + * Whether uploads from the client to the shared folder should be disabled. + */ + int disable_download; + + /** + * Whether downloads from the shared folder to the client should be disabled. + */ + int disable_upload; + + /** + * Thread which watches the Download folder and triggers the automatic + * download of files within this subfolder. + */ + pthread_t download_thread; + +} guac_spice_folder; + +/** + * Allocates a new filesystem given a root path which will be shared with the + * user and the remote server via WebDAV. + * + * @param client + * The guac_client associated with the current RDP session. + * + * @param folder_path + * The local directory to use as the root directory of the shared folder. + * + * @param create_folder + * Non-zero if the folder at the path specified should be automatically + * created if it does not yet exist, zero otherwise. + * + * @param disable_download + * Non-zero if downloads from the remote server to the local browser should + * be disabled. + * + * @param disable_upload + * Non-zero if uploads from the browser to the remote server should be + * disabled. + * + * @return + * The newly-allocated filesystem. + */ +guac_spice_folder* guac_spice_folder_alloc(guac_client* client, const char* folder_path, + int create_folder, int disable_download, int disable_upload); + +/** + * Frees the given filesystem. + * + * @param folder + * The folder to free. + */ +void guac_spice_folder_free(guac_spice_folder* folder); + +/** + * Creates and exposes a new filesystem guac_object to the given user, + * providing access to the files within the given Spice shared folder. The + * allocated guac_object must eventually be freed via guac_user_free_object(). + * + * @param folder + * The guac_spice_folder object to expose. + * + * @param user + * The user that the folder should be exposed to. + * + * @return + * A new Guacamole filesystem object, configured to use Spice for uploading + * and downloading files. + */ +guac_object* guac_spice_folder_alloc_object(guac_spice_folder* folder, guac_user* user); + +/** + * Concatenates the given filename with the given path, separating the two + * with a single forward slash. The full result must be no more than + * GUAC_SPICE_FOLDER_MAX_PATH bytes long, counting null terminator. + * + * @param fullpath + * The buffer to store the result within. This buffer must be at least + * GUAC_SPICE_FOLDER_MAX_PATH bytes long. + * + * @param path + * The path to append the filename to. + * + * @param filename + * The filename to append to the path. + * + * @return + * Non-zero if the filename is valid and was successfully appended to the + * path, zero otherwise. + */ +int guac_spice_folder_append_filename(char* fullpath, const char* path, + const char* filename); + +/** + * Given an arbitrary path, returns a pointer to the first character following + * the last path separator in the path (the basename of the path). For example, + * given "/foo/bar/baz", this function would return a pointer to "baz". + * + * @param path + * The path to determine the basename of. + * + * @return + * A pointer to the first character of the basename within the path. + */ +const char* guac_spice_folder_basename(const char* path); + +/** + * Frees the given file ID, allowing future open operations to reuse it. + * + * @param folder + * The folder containing the file to close. + * + * @param file_id + * The ID of the file to close, as returned by guac_spice_folder_open(). + */ +void guac_spice_folder_close(guac_spice_folder* folder, int file_id); + +/** + * Deletes the file with the given ID. + * + * @param folder + * The folder containing the file to delete. + * + * @param file_id + * The ID of the file to delete, as returned by guac_spice_folder_open(). + * + * @return + * Zero if deletion succeeded, or an error code if an error occurs. All + * error codes are negative values and correspond to GUAC_SPICE_FOLDER + * constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_delete(guac_spice_folder* folder, int file_id); + +/** + * Allocates a new filesystem guac_object for the given user, returning the + * resulting guac_object. This function is provided for convenience, as it is + * can be used as the callback for guac_client_foreach_user() or + * guac_client_for_owner(). Note that this guac_object will be tracked + * internally by libguac, will be provided to us in the parameters of handlers + * related to that guac_object, and will automatically be freed when the + * associated guac_user is freed, so the return value of this function can + * safely be ignored. + * + * If either the given user or the given filesystem are NULL, then this + * function has no effect. + * + * @param user + * The use to expose the filesystem to, or NULL if nothing should be + * exposed. + * + * @param data + * A pointer to the guac_spice_folder instance to expose to the given user, + * or NULL if nothing should be exposed. + * + * @return + * The guac_object allocated for the newly-exposed filesystem, or NULL if + * no filesystem object could be allocated. + */ +void* guac_spice_folder_expose(guac_user* user, void* data); + +/** + * Translates the given errno error code to a GUAC_SPICE_FOLDER error code. + * + * @param err + * The error code, as returned within errno by a system call. + * + * @return + * A GUAC_SPICE_FOLDER error code, such as GUAC_SPICE_FOLDER_ENFILE, + * GUAC_SPICE_FOLDER_ENOENT, etc. + */ +int guac_spice_folder_get_errorcode(int err); + +/** + * Returns the file having the given ID, or NULL if no such file exists. + * + * @param folder + * The folder containing the desired file. + * + * @param file_id + * The ID of the desired, as returned by guac_spice_folder_open(). + * + * @return + * The file having the given ID, or NULL is no such file exists. + */ +guac_spice_folder_file* guac_spice_folder_get_file(guac_spice_folder* folder, + int file_id); + +/** + * Given an arbitrary path, which may contain ".." and ".", creates an + * absolute path which does NOT contain ".." or ".". The given path MUST + * be absolute. + * + * @param path + * The path to normalize. + * + * @param abs_path + * The buffer to populate with the normalized path. The normalized path + * will not contain relative path components like ".." or ".". + * + * @return + * Zero if normalization succeeded, non-zero otherwise. + */ +int guac_spice_folder_normalize_path(const char* path, char* abs_path); + +/** + * Opens the given file, returning the a new file ID, or an error code less + * than zero if an error occurs. The given path MUST be absolute, and will be + * translated to be relative to the drive path of the simulated filesystem. + * + * @param folder + * The shared folder to use when opening the file. + * + * @param path + * The absolute path to the file within the simulated filesystem. + * + * @param flags + * A bitwise-OR of various standard POSIX flags to use when opening the + * file or directory. + * + * @param overwrite + * True if the file should be overwritten when opening it, otherwise false. + * + * @param directory + * True if the path specified is a directory, otherwise false. + * + * @return + * A new file ID, which will always be a positive value, or an error code + * if an error occurs. All error codes are negative values and correspond + * to GUAC_SPICE_FOLDER constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_open(guac_spice_folder* folder, const char* path, + int flags, bool overwrite, bool directory); + +/** + * Reads up to the given length of bytes from the given offset within the + * file having the given ID. Returns the number of bytes read, zero on EOF, + * and an error code if an error occurs. + * + * @param folder + * The folder containing the file from which data is to be read. + * + * @param file_id + * The ID of the file to read data from, as returned by guac_spice_folder_open(). + * + * @param offset + * The byte offset within the file to start reading from. + * + * @param buffer + * The buffer to fill with data from the file. + * + * @param length + * The maximum number of bytes to read from the file. + * + * @return + * The number of bytes actually read, zero on EOF, or an error code if an + * error occurs. All error codes are negative values and correspond to + * GUAC_SPICE_FOLDER constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_read(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length); + +/** + * Returns the next filename within the directory having the given file ID, + * or NULL if no more files. + * + * @param folder + * The foleer containing the file to read directory entries from. + * + * @param file_id + * The ID of the file to read directory entries from, as returned by + * guac_spice_folder_open(). + * + * @return + * The name of the next filename within the directory, or NULL if the last + * file in the directory has already been returned by a previous call. + */ +const char* guac_spice_folder_read_dir(guac_spice_folder* folder, int file_id); + +/** + * Writes up to the given length of bytes from the given offset within the + * file having the given ID. Returns the number of bytes written, and an + * error code if an error occurs. + * + * @param folder + * The folder containing the file to which data is to be written. + * + * @param file_id + * The ID of the file to write data to, as returned by guac_spice_folder_open(). + * + * @param offset + * The byte offset within the file to start writinging at. + * + * @param buffer + * The buffer containing the data to write. + * + * @param length + * The maximum number of bytes to write to the file. + * + * @return + * The number of bytes actually written, or an error code if an error + * occurs. All error codes are negative values and correspond to + * GUAC_SPICE_FOLDER constants, such as GUAC_SPICE_FOLDER_ENOENT. + */ +int guac_spice_folder_write(guac_spice_folder* folder, int file_id, uint64_t offset, + void* buffer, int length); + +/** + * A handler that is called when the SPICE client receives notification of + * a new file transfer task. + * + * @param main_channel + * The main channel associated with the SPICE session. + * + * @param task + * The file transfer task that triggered this function call. + * + * @param client + * The guac_client object associated with this session. + */ +void guac_spice_client_file_transfer_handler(SpiceMainChannel* main_channel, + SpiceFileTransferTask* task, guac_client* client); + +#endif /* GUAC_SPICE_FILE_H */ + diff --git a/src/protocols/spice/client.c b/src/protocols/spice/client.c new file mode 100644 index 00000000..77b12d8c --- /dev/null +++ b/src/protocols/spice/client.c @@ -0,0 +1,382 @@ +/* + * 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 "auth.h" +#include "client.h" +#include "channels/audio.h" +#include "channels/clipboard.h" +#include "channels/cursor.h" +#include "channels/display.h" +#include "channels/file.h" +#include "input.h" +#include "keyboard.h" +#include "user.h" +#include "spice.h" +#include "spice-constants.h" + +#ifdef ENABLE_COMMON_SSH +#include "common-ssh/sftp.h" +#include "common-ssh/ssh.h" +#include "common-ssh/user.h" +#endif + +#include +#include + +#include +#include +#include +#include +#include + +/** + * Handle events for the main Spice channel, taking the appropriate action + * for known events, and logging warnings for unknowns and non-fatal events. + * + * @param channel + * The channel for which to handle events. + * + * @param event + * The event that is being handled. + * + * @param client + * The guacamole_client associated with this event. + */ +static void guac_spice_client_main_channel_handler(SpiceChannel *channel, + SpiceChannelEvent event, guac_client* client) { + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_client_log(client, GUAC_LOG_DEBUG, "Received new main channel event: %u", event); + + /* Handle the various possible SPICE events. */ + switch (event) { + + /* Channel has been closed, so we abort the connection. */ + case SPICE_CHANNEL_CLOSED: + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Disconnected from Spice server."); + break; + + /* Channel has been opened - log it and do nothing else. */ + case SPICE_CHANNEL_OPENED: + guac_client_log(client, GUAC_LOG_DEBUG, "Channel opened."); + break; + + /* Error authenticating, log a warning and prompt user. */ + case SPICE_CHANNEL_ERROR_AUTH: + guac_client_log(client, GUAC_LOG_WARNING, + "Channel authentication failed."); + + /* Trigger a credential prompt. */ + if (guac_spice_get_credentials(client) + && spice_session_connect(spice_client->spice_session)) + guac_client_log(client, GUAC_LOG_DEBUG, "Session connection started."); + + else + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Failed to get credentials to connect to server."); + + break; + + /* TLS error, abort the connection with a warning. */ + case SPICE_CHANNEL_ERROR_TLS: + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "TLS failure connecting to Spice server."); + break; + + /* I/O error, abort the connection with a warning. */ + case SPICE_CHANNEL_ERROR_IO: + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "IO error communicating with Spice server."); + break; + + /* SPICE link error, abort the connection with a warning. */ + case SPICE_CHANNEL_ERROR_LINK: + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Link error communicating with Spice server."); + break; + + /* Connect error, abort the connection with a warning. */ + case SPICE_CHANNEL_ERROR_CONNECT: + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Connection error communicating with Spice server."); + break; + + /* Some other unknown event - log it and move on. */ + default: + guac_client_log(client, GUAC_LOG_WARNING, + "Unknown event received on channel."); + } + +} + +int guac_client_init(guac_client* client) { + + /* Set client args */ + client->args = GUAC_SPICE_CLIENT_ARGS; + + guac_client_log(client, GUAC_LOG_DEBUG, "Initializing Spice client."); + + /* Alloc client data */ + guac_spice_client* spice_client = calloc(1, sizeof(guac_spice_client)); + client->data = spice_client; + + guac_client_log(client, GUAC_LOG_DEBUG, "Initializing clipboard."); + + /* Init clipboard */ + spice_client->clipboard = + guac_common_clipboard_alloc(GUAC_SPICE_CLIPBOARD_MAX_LENGTH); + + + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up user handlers."); + + /* Set handlers */ + client->join_handler = guac_spice_user_join_handler; + client->leave_handler = guac_spice_user_leave_handler; + client->free_handler = guac_spice_client_free_handler; + + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up mutex locks."); + + /* Recursive attribute for locks */ + pthread_mutexattr_init(&(spice_client->attributes)); + pthread_mutexattr_settype(&(spice_client->attributes), + PTHREAD_MUTEX_RECURSIVE); + + /* Init required locks */ + pthread_rwlock_init(&(spice_client->lock), NULL); + pthread_mutex_init(&(spice_client->message_lock), &(spice_client->attributes)); + + return 0; +} + +int guac_spice_client_free_handler(guac_client* client) { + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_settings* settings = spice_client->settings; + + /* Clean up SPICE client*/ + SpiceSession* spice_session = spice_client->spice_session; + if (spice_session != NULL) { + + /* Wait for client thread to finish */ + pthread_join(spice_client->client_thread, NULL); + + /* Disconnect the session, destroying data */ + spice_session_disconnect(spice_session); + + /* Free up the glib main loop. */ + g_main_loop_unref(spice_client->spice_mainloop); + + } + +#ifdef ENABLE_COMMON_SSH + /* Free SFTP filesystem, if loaded */ + if (spice_client->sftp_filesystem) + guac_common_ssh_destroy_sftp_filesystem(spice_client->sftp_filesystem); + + /* Free SFTP session */ + if (spice_client->sftp_session) + guac_common_ssh_destroy_session(spice_client->sftp_session); + + /* Free SFTP user */ + if (spice_client->sftp_user) + guac_common_ssh_destroy_user(spice_client->sftp_user); + + guac_common_ssh_uninit(); +#endif + + /* Clean up recording, if in progress */ + if (spice_client->recording != NULL) + guac_recording_free(spice_client->recording); + + /* Free clipboard */ + if (spice_client->clipboard != NULL) + guac_common_clipboard_free(spice_client->clipboard); + + /* Free display */ + if (spice_client->display != NULL) + guac_common_display_free(spice_client->display); + + /* Free SPICE keyboard state */ + guac_spice_keyboard_free(spice_client->keyboard); + spice_client->keyboard = NULL; + + /* Free parsed settings */ + if (settings != NULL) + guac_spice_settings_free(settings); + + pthread_rwlock_destroy(&(spice_client->lock)); + pthread_mutex_destroy(&(spice_client->message_lock)); + + /* Free generic data struct */ + free(client->data); + + return 0; +} + +void guac_spice_client_channel_handler(SpiceSession *spice_session, + SpiceChannel *channel, guac_client* client) { + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_settings* settings = spice_client->settings; + int id, type; + + /* Get the channel ID and type. */ + g_object_get(channel, SPICE_PROPERTY_CHANNEL_ID, &id, NULL); + g_object_get(channel, SPICE_PROPERTY_CHANNEL_TYPE, &type, NULL); + + guac_client_log(client, GUAC_LOG_DEBUG, "New channel created: %i", id); + guac_client_log(client, GUAC_LOG_DEBUG, "New channel type: %i", type); + + /* Check if this is the main channel and register handlers. */ + if (SPICE_IS_MAIN_CHANNEL(channel)) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up main channel."); + spice_client->main_channel = SPICE_MAIN_CHANNEL(channel); + + /* Register the main channel event handler and return. */ + g_signal_connect(channel, SPICE_SIGNAL_CHANNEL_EVENT, + G_CALLBACK(guac_spice_client_main_channel_handler), client); + + /* Register clipboard handlers. */ + g_signal_connect(channel, SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION, + G_CALLBACK(guac_spice_clipboard_selection_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION_GRAB, + G_CALLBACK(guac_spice_clipboard_selection_grab_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION_RELEASE, + G_CALLBACK(guac_spice_clipboard_selection_release_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION_REQUEST, + G_CALLBACK(guac_spice_clipboard_selection_request_handler), client); + + /* Update the main display with our size. */ + if (client->__owner != NULL) + spice_main_channel_update_display(spice_client->main_channel, + GUAC_SPICE_DEFAULT_DISPLAY_ID, + 0, + 0, + client->__owner->info.optimal_width, + client->__owner->info.optimal_height, TRUE); + return; + } + + /* Check if this is the display channel and register display handlers. */ + if (SPICE_IS_DISPLAY_CHANNEL(channel) && id == 0) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up display channel."); + int width, height; + SpiceDisplayPrimary primary; + g_object_get(channel, "width", &width, "height", &height, NULL); + spice_client->spice_display = SPICE_DISPLAY_CHANNEL(channel); + spice_client->display = guac_common_display_alloc(client, width, height); + + /* Register callbacks fr the various display signals. */ + g_signal_connect(channel, SPICE_SIGNAL_DISPLAY_INVALIDATE, + G_CALLBACK(guac_spice_client_display_update), client); + g_signal_connect(channel, SPICE_SIGNAL_DISPLAY_MARK, + G_CALLBACK(guac_spice_client_display_mark), client); + g_signal_connect(channel, SPICE_SIGNAL_DISPLAY_PRIMARY_CREATE, + G_CALLBACK(guac_spice_client_display_primary_create), client); + g_signal_connect(channel, SPICE_SIGNAL_DISPLAY_PRIMARY_DESTROY, + G_CALLBACK(guac_spice_client_display_primary_destroy), client); + g_signal_connect(channel, SPICE_SIGNAL_GL_DRAW, + G_CALLBACK(guac_spice_client_display_gl_draw), client); + g_signal_connect(channel, SPICE_SIGNAL_STREAMING_MODE, + G_CALLBACK(guac_spice_client_streaming_handler), client); + + /* Attempt to get the primary display, and set it up. */ + if (spice_display_channel_get_primary(channel, 0, &primary)) { + guac_spice_client_display_primary_create( + spice_client->spice_display, primary.format, + primary.width, primary.height, primary.stride, + primary.shmid, primary.data, client); + guac_spice_client_display_mark(spice_client->spice_display, + primary.marked, client); + } + + } + + /* Check for audio playback channel and set up the channel. */ + if (SPICE_IS_PLAYBACK_CHANNEL(channel) && settings->audio_enabled) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up audio playback channel."); + spice_client->playback_channel = SPICE_PLAYBACK_CHANNEL(channel); + g_signal_connect(channel, SPICE_SIGNAL_PLAYBACK_DATA, + G_CALLBACK(guac_spice_client_audio_playback_data_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_PLAYBACK_GET_DELAY, + G_CALLBACK(guac_spice_client_audio_playback_delay_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_PLAYBACK_START, + G_CALLBACK(guac_spice_client_audio_playback_start_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_PLAYBACK_STOP, + G_CALLBACK(guac_spice_client_audio_playback_stop_handler), client); + } + + /* Check for audio recording channel and set up the channel. */ + if (SPICE_IS_RECORD_CHANNEL(channel) && settings->audio_input_enabled) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up audio record channel."); + spice_client->record_channel = SPICE_RECORD_CHANNEL(channel); + g_signal_connect(channel, SPICE_SIGNAL_RECORD_START, + G_CALLBACK(guac_spice_client_audio_record_start_handler), client); + g_signal_connect(channel, SPICE_SIGNAL_RECORD_STOP, + G_CALLBACK(guac_spice_client_audio_record_stop_handler), client); + } + + /* Check for cursor channel and set it up. */ + if (SPICE_IS_CURSOR_CHANNEL(channel)) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up cursor channel."); + spice_client->cursor_channel = SPICE_CURSOR_CHANNEL(channel); + g_signal_connect(channel, SPICE_SIGNAL_CURSOR_HIDE, + G_CALLBACK(guac_spice_cursor_hide), client); + g_signal_connect(channel, SPICE_SIGNAL_CURSOR_MOVE, + G_CALLBACK(guac_spice_cursor_move), client); + g_signal_connect(channel, SPICE_SIGNAL_CURSOR_RESET, + G_CALLBACK(guac_spice_cursor_reset), client); + g_signal_connect(channel, SPICE_SIGNAL_CURSOR_SET, + G_CALLBACK(guac_spice_cursor_set), client); + } + + /* Check if this is an inputs channel and set it up. */ + if (SPICE_IS_INPUTS_CHANNEL(channel)) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up inputs channel."); + spice_client->inputs_channel = SPICE_INPUTS_CHANNEL(channel); + + /* Register callback that sets keyboard modifiers */ + g_signal_connect(channel, SPICE_SIGNAL_INPUTS_MODIFIERS, + G_CALLBACK(guac_spice_keyboard_set_indicators), client); + } + + /* File transfer channel */ + if (SPICE_IS_WEBDAV_CHANNEL(channel)) { + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up webdav channel."); + if (settings->file_transfer + && settings->file_directory != NULL + && strcmp(settings->file_directory, "") != 0) { + } + } + + if (SPICE_IS_USBREDIR_CHANNEL(channel)) { + guac_client_log(client, GUAC_LOG_DEBUG, "USB redirection is not yet implemented."); + return; + } + + guac_client_log(client, GUAC_LOG_DEBUG, "Calling spice_channel_connect for channel %d.", id); + if (!spice_channel_connect(channel)) + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Unable to connect the channel."); + +} diff --git a/src/protocols/spice/client.h b/src/protocols/spice/client.h new file mode 100644 index 00000000..f2b01bc2 --- /dev/null +++ b/src/protocols/spice/client.h @@ -0,0 +1,73 @@ +/* + * 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_SPICE_CLIENT_H +#define GUAC_SPICE_CLIENT_H + +#include "config.h" + +#include + +#include + +/** + * The maximum duration of a frame in milliseconds. + */ +#define GUAC_SPICE_FRAME_DURATION 40 + +/** + * The amount of time to allow per message read within a frame, in + * milliseconds. If the server is silent for at least this amount of time, the + * frame will be considered finished. + */ +#define GUAC_SPICE_FRAME_TIMEOUT 0 + +/** + * The amount of time to wait for a new message from the Spice server when + * beginning a new frame. This value must be kept reasonably small such that + * a slow Spice server will not prevent external events from being handled (such + * as the stop signal from guac_client_stop()), but large enough that the + * message handling loop does not eat up CPU spinning. + */ +#define GUAC_SPICE_FRAME_START_TIMEOUT 1000000 + +/** + * The number of milliseconds to wait between connection attempts. + */ +#define GUAC_SPICE_CONNECT_INTERVAL 1000 + +/** + * The maximum number of bytes to allow within the clipboard. + */ +#define GUAC_SPICE_CLIPBOARD_MAX_LENGTH 262144 + +/** + * Handler which frees all data associated with the guac_client. + */ +guac_client_free_handler guac_spice_client_free_handler; + +/** + * Handler for new channel events. + */ +void guac_spice_client_channel_handler(SpiceSession *spice_session, + SpiceChannel *channel, guac_client* client); + +#endif /* GUAC_SPICE_CLIENT_H */ + diff --git a/src/protocols/spice/decompose.c b/src/protocols/spice/decompose.c new file mode 100644 index 00000000..5f4cd65b --- /dev/null +++ b/src/protocols/spice/decompose.c @@ -0,0 +1,176 @@ +/* + * 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 "keyboard.h" + +/** + * The X11 keysym for the dead key which types a grave (`). + */ +#define DEAD_GRAVE 0xFE50 + +/** + * The X11 keysym for the dead key which types an acute (´). Note that this is + * NOT equivalent to an apostrophe or single quote. + */ +#define DEAD_ACUTE 0xFE51 + +/** + * The X11 keysym for the dead key which types a circumflex/caret (^). + */ +#define DEAD_CIRCUMFLEX 0xFE52 + +/** + * The X11 keysym for the dead key which types a tilde (~). + */ +#define DEAD_TILDE 0xFE53 + +/** + * The X11 keysym for the dead key which types a dieresis/umlaut (¨). + */ +#define DEAD_DIERESIS 0xFE57 + +/** + * The X11 keysym for the dead key which types an abovering (˚). Note that this + * is NOT equivalent to the degree symbol. + */ +#define DEAD_ABOVERING 0xFE58 + +/** + * The decomposed form of a key that can be typed using two keypresses: a dead + * key followed by a base key. For example, on a keyboard which lacks a single + * dedicated key for doing the same, "ó" would be typed using the dead acute + * key followed by the "o" key. The dead key and base key are pressed and + * released in sequence; they are not held down. + */ +typedef struct guac_spice_decomposed_key { + + /** + * The keysym of the dead key which must first be pressed and released to + * begin typing the desired character. The dead key defines the diacritic + * which will be applied to the character typed by the base key. + */ + int dead_keysym; + + /** + * The keysym of the base key which must be pressed and released to finish + * typing the desired character. The base key defines the normal form of + * the character (the form which lacks any diacritic) to which the + * diacritic defined by the previously-pressed dead key will be applied. + */ + int base_keysym; + +} guac_spice_decomposed_key; + +/** + * A lookup table of all known decomposed forms of various keysyms. Keysyms map + * directly to entries within this table. A keysym which has no entry within + * this table does not have a defined decomposed form (or at least does not + * have a decomposed form relevant to SPICE). + */ +guac_spice_decomposed_key guac_spice_decomposed_keys[256] = { + + /* ^ */ [0x005E] = { DEAD_CIRCUMFLEX, ' ' }, + /* ` */ [0x0060] = { DEAD_GRAVE, ' ' }, + /* ~ */ [0x007E] = { DEAD_TILDE, ' ' }, + /* ¨ */ [0x00A8] = { DEAD_DIERESIS, ' ' }, + /* ´ */ [0x00B4] = { DEAD_ACUTE, ' ' }, + /* À */ [0x00C0] = { DEAD_GRAVE, 'A' }, + /* Á */ [0x00C1] = { DEAD_ACUTE, 'A' }, + /*  */ [0x00C2] = { DEAD_CIRCUMFLEX, 'A' }, + /* à */ [0x00C3] = { DEAD_TILDE, 'A' }, + /* Ä */ [0x00C4] = { DEAD_DIERESIS, 'A' }, + /* Å */ [0x00C5] = { DEAD_ABOVERING, 'A' }, + /* È */ [0x00C8] = { DEAD_GRAVE, 'E' }, + /* É */ [0x00C9] = { DEAD_ACUTE, 'E' }, + /* Ê */ [0x00CA] = { DEAD_CIRCUMFLEX, 'E' }, + /* Ë */ [0x00CB] = { DEAD_DIERESIS, 'E' }, + /* Ì */ [0x00CC] = { DEAD_GRAVE, 'I' }, + /* Í */ [0x00CD] = { DEAD_ACUTE, 'I' }, + /* Î */ [0x00CE] = { DEAD_CIRCUMFLEX, 'I' }, + /* Ï */ [0x00CF] = { DEAD_DIERESIS, 'I' }, + /* Ñ */ [0x00D1] = { DEAD_TILDE, 'N' }, + /* Ò */ [0x00D2] = { DEAD_GRAVE, 'O' }, + /* Ó */ [0x00D3] = { DEAD_ACUTE, 'O' }, + /* Ô */ [0x00D4] = { DEAD_CIRCUMFLEX, 'O' }, + /* Õ */ [0x00D5] = { DEAD_TILDE, 'O' }, + /* Ö */ [0x00D6] = { DEAD_DIERESIS, 'O' }, + /* Ù */ [0x00D9] = { DEAD_GRAVE, 'U' }, + /* Ú */ [0x00DA] = { DEAD_ACUTE, 'U' }, + /* Û */ [0x00DB] = { DEAD_CIRCUMFLEX, 'U' }, + /* Ü */ [0x00DC] = { DEAD_DIERESIS, 'U' }, + /* Ý */ [0x00DD] = { DEAD_ACUTE, 'Y' }, + /* à */ [0x00E0] = { DEAD_GRAVE, 'a' }, + /* á */ [0x00E1] = { DEAD_ACUTE, 'a' }, + /* â */ [0x00E2] = { DEAD_CIRCUMFLEX, 'a' }, + /* ã */ [0x00E3] = { DEAD_TILDE, 'a' }, + /* ä */ [0x00E4] = { DEAD_DIERESIS, 'a' }, + /* å */ [0x00E5] = { DEAD_ABOVERING, 'a' }, + /* è */ [0x00E8] = { DEAD_GRAVE, 'e' }, + /* é */ [0x00E9] = { DEAD_ACUTE, 'e' }, + /* ê */ [0x00EA] = { DEAD_CIRCUMFLEX, 'e' }, + /* ë */ [0x00EB] = { DEAD_DIERESIS, 'e' }, + /* ì */ [0x00EC] = { DEAD_GRAVE, 'i' }, + /* í */ [0x00ED] = { DEAD_ACUTE, 'i' }, + /* î */ [0x00EE] = { DEAD_CIRCUMFLEX, 'i' }, + /* ï */ [0x00EF] = { DEAD_DIERESIS, 'i' }, + /* ñ */ [0x00F1] = { DEAD_TILDE, 'n' }, + /* ò */ [0x00F2] = { DEAD_GRAVE, 'o' }, + /* ó */ [0x00F3] = { DEAD_ACUTE, 'o' }, + /* ô */ [0x00F4] = { DEAD_CIRCUMFLEX, 'o' }, + /* õ */ [0x00F5] = { DEAD_TILDE, 'o' }, + /* ö */ [0x00F6] = { DEAD_DIERESIS, 'o' }, + /* ù */ [0x00F9] = { DEAD_GRAVE, 'u' }, + /* ú */ [0x00FA] = { DEAD_ACUTE, 'u' }, + /* û */ [0x00FB] = { DEAD_CIRCUMFLEX, 'u' }, + /* ü */ [0x00FC] = { DEAD_DIERESIS, 'u' }, + /* ý */ [0x00FD] = { DEAD_ACUTE, 'y' }, + /* ÿ */ [0x00FF] = { DEAD_DIERESIS, 'y' } + +}; + +int guac_spice_decompose_keysym(guac_spice_keyboard* keyboard, int keysym) { + + /* Verify keysym is within range of lookup table */ + if (keysym < 0x00 || keysym > 0xFF) + return 1; + + /* Verify keysym is actually defined within lookup table */ + guac_spice_decomposed_key* key = &guac_spice_decomposed_keys[keysym]; + if (!key->dead_keysym) + return 1; + + /* Cannot type using decomposed keys if those keys are not defined within + * the current layout */ + if (!guac_spice_keyboard_is_defined(keyboard, key->dead_keysym) + || !guac_spice_keyboard_is_defined(keyboard, key->base_keysym)) + return 1; + + /* Press dead key */ + guac_spice_keyboard_update_keysym(keyboard, key->dead_keysym, 1, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, key->dead_keysym, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + + /* Press base key */ + guac_spice_keyboard_update_keysym(keyboard, key->base_keysym, 1, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, key->base_keysym, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + + /* Decomposed key successfully typed */ + return 0; + +} + diff --git a/src/protocols/spice/decompose.h b/src/protocols/spice/decompose.h new file mode 100644 index 00000000..f6638394 --- /dev/null +++ b/src/protocols/spice/decompose.h @@ -0,0 +1,50 @@ +/* + * 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_SPICE_DECOMPOSE_H +#define GUAC_SPICE_DECOMPOSE_H + +#include "config.h" +#include "keyboard.h" + +/** + * Attempts to type the given keysym by decomposing the associated character + * into the dead key and base key pair which would be used to type that + * character on a keyboard which lacks the necessary dedicated key. The key + * events for the dead key and base key are sent only if the keyboard layout of + * the given keyboard defines those keys. + * + * For example, the keysym for "ò" (0x00F2) would decompose into a dead grave + * (`) and the base key "o". May it rest in peace. + * + * @param keyboard + * The guac_spice_keyboard associated with the current SPICE session. + * + * @param keysym + * The keysym being pressed. + * + * @return + * Zero if the keysym was successfully decomposed and sent to the Spice + * server as a pair of key events (the dead key and base key), non-zero + * otherwise. + */ +int guac_spice_decompose_keysym(guac_spice_keyboard* keyboard, int keysym); + +#endif + diff --git a/src/protocols/spice/input.c b/src/protocols/spice/input.c new file mode 100644 index 00000000..e8f8ab54 --- /dev/null +++ b/src/protocols/spice/input.c @@ -0,0 +1,154 @@ +/* + * 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 "common/cursor.h" +#include "common/display.h" +#include "spice.h" +#include "spice-constants.h" + +#include +#include +#include + +int guac_spice_user_mouse_handler(guac_user* user, int x, int y, int mask) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Store the old button mask. */ + int curr_button_mask = spice_client->display->cursor->button_mask; + + guac_user_log(user, GUAC_LOG_TRACE, "Handling mouse event: %d, %d, 0x%08x", x, y, mask); + + /* Update current mouse location/state */ + guac_common_cursor_update(spice_client->display->cursor, user, x, y, mask); + + /* Report mouse position and button state within recording. */ + if (spice_client->recording != NULL) + guac_recording_report_mouse(spice_client->recording, x, y, mask); + + /* Send SPICE event only if finished connecting */ + if (spice_client->inputs_channel != NULL) { + spice_inputs_channel_position(spice_client->inputs_channel, x, y, GUAC_SPICE_DEFAULT_DISPLAY_ID, mask); + + /* Button state has changed, so we need to evaluate which buttons changed. */ + if (curr_button_mask != mask) { + + /* Left click. */ + if (curr_button_mask ^ GUAC_CLIENT_MOUSE_LEFT && mask & GUAC_CLIENT_MOUSE_LEFT) { + guac_user_log(user, GUAC_LOG_TRACE, "Left click!"); + spice_inputs_channel_button_press(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_LEFT, mask); + } + + /* Left release. */ + if (curr_button_mask & GUAC_CLIENT_MOUSE_LEFT && mask ^ GUAC_CLIENT_MOUSE_LEFT) { + guac_user_log(user, GUAC_LOG_TRACE, "Left release!"); + spice_inputs_channel_button_release(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_LEFT, mask); + } + + /* Middle click. */ + if (curr_button_mask ^ GUAC_CLIENT_MOUSE_MIDDLE && mask & GUAC_CLIENT_MOUSE_MIDDLE) { + guac_user_log(user, GUAC_LOG_TRACE, "Middle click!"); + spice_inputs_channel_button_press(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_MIDDLE, mask); + } + + /* Middle release. */ + if (curr_button_mask & GUAC_CLIENT_MOUSE_MIDDLE && mask ^ GUAC_CLIENT_MOUSE_MIDDLE) { + guac_user_log(user, GUAC_LOG_TRACE, "Middle release!"); + spice_inputs_channel_button_release(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_MIDDLE, mask); + } + + /* Right click. */ + if (curr_button_mask ^ GUAC_CLIENT_MOUSE_RIGHT && mask & GUAC_CLIENT_MOUSE_RIGHT) { + guac_user_log(user, GUAC_LOG_TRACE, "Right click!"); + spice_inputs_channel_button_press(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_RIGHT, mask); + } + + /* Right release. */ + if (curr_button_mask & GUAC_CLIENT_MOUSE_RIGHT && mask ^ GUAC_CLIENT_MOUSE_RIGHT) { + guac_user_log(user, GUAC_LOG_TRACE, "Right release!"); + spice_inputs_channel_button_release(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_RIGHT, mask); + } + + /* Scroll up. */ + if (curr_button_mask ^ GUAC_CLIENT_MOUSE_SCROLL_UP && mask & GUAC_CLIENT_MOUSE_SCROLL_UP) { + guac_user_log(user, GUAC_LOG_TRACE, "Scroll up!"); + spice_inputs_channel_button_press(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_UP, mask); + } + + /* Scroll up stop. */ + if (curr_button_mask & GUAC_CLIENT_MOUSE_SCROLL_UP && mask ^ GUAC_CLIENT_MOUSE_SCROLL_UP) { + guac_user_log(user, GUAC_LOG_TRACE, "Scroll up stop!"); + spice_inputs_channel_button_release(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_UP, mask); + } + + /* Scroll down. */ + if (curr_button_mask ^ GUAC_CLIENT_MOUSE_SCROLL_DOWN && mask & GUAC_CLIENT_MOUSE_SCROLL_DOWN) { + guac_user_log(user, GUAC_LOG_TRACE, "Scroll down!"); + spice_inputs_channel_button_press(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_DOWN, mask); + } + + /* Scroll down stop. */ + if (curr_button_mask & GUAC_CLIENT_MOUSE_SCROLL_DOWN && mask ^ GUAC_CLIENT_MOUSE_SCROLL_DOWN) { + guac_user_log(user, GUAC_LOG_TRACE, "Scroll down stop!"); + spice_inputs_channel_button_release(spice_client->inputs_channel, SPICE_MOUSE_BUTTON_DOWN, mask); + } + + } + + } + + return 0; +} + +int guac_spice_user_key_handler(guac_user* user, int keysym, int pressed) { + + guac_spice_client* spice_client = (guac_spice_client*) user->client->data; + int retval = 0; + + pthread_rwlock_rdlock(&(spice_client->lock)); + + guac_user_log(user, GUAC_LOG_TRACE, "Handling keypress: 0x%08x", keysym); + + /* Report key state within recording */ + if (spice_client->recording != NULL) + guac_recording_report_key(spice_client->recording, + keysym, pressed); + + /* Skip if keyboard not yet ready */ + if (spice_client->inputs_channel == NULL || spice_client->keyboard == NULL) + goto complete; + + /* Update keysym state */ + retval = guac_spice_keyboard_update_keysym(spice_client->keyboard, + keysym, pressed, GUAC_SPICE_KEY_SOURCE_CLIENT); + +complete: + pthread_rwlock_unlock(&(spice_client->lock)); + + return retval; +} + +void guac_spice_mouse_mode_update(SpiceChannel* channel, guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Updating mouse mode, not implemented."); + +} \ No newline at end of file diff --git a/src/protocols/spice/input.h b/src/protocols/spice/input.h new file mode 100644 index 00000000..81dd6ab7 --- /dev/null +++ b/src/protocols/spice/input.h @@ -0,0 +1,50 @@ +/* + * 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_SPICE_INPUT_H +#define GUAC_SPICE_INPUT_H + +#include "config.h" + +#include + +/** + * Handler for Guacamole user mouse events. + */ +guac_user_mouse_handler guac_spice_user_mouse_handler; + +/** + * Handler for Guacamole user key events. + */ +guac_user_key_handler guac_spice_user_key_handler; + +/** + * A callback that is invoked when the Spice server updates the mouse mode. + * + * @param channel + * The channel on which the update occurred. + * + * @param client + * The guac_client instance associated with this session. + */ +void guac_spice_mouse_mode_update(SpiceChannel* channel, guac_client* client); + +#endif /* GUAC_SPICE_INPUT_H */ + diff --git a/src/protocols/spice/keyboard.c b/src/protocols/spice/keyboard.c new file mode 100644 index 00000000..91dcc3fe --- /dev/null +++ b/src/protocols/spice/keyboard.c @@ -0,0 +1,652 @@ +/* + * 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 "decompose.h" +#include "keyboard.h" +#include "keymap.h" +#include "spice.h" +#include "spice-constants.h" + +#include + +#include + +/** + * Translates the given keysym into the corresponding lock flag, as would be + * required by the Spice synchronize event. If the given keysym does not + * represent a lock key, zero is returned. + * + * @param keysym + * The keysym to translate into a Spice lock flag. + * + * @return + * The Spice lock flag which corresponds to the given keysym, or zero if the + * given keysym does not represent a lock key. + */ +static int guac_spice_keyboard_lock_flag(int keysym) { + + /* Translate keysym into corresponding lock flag */ + switch (keysym) { + + /* Scroll lock */ + case GUAC_SPICE_KEYSYM_SCROLL_LOCK: + return SPICE_INPUTS_SCROLL_LOCK; + + /* Num lock */ + case GUAC_SPICE_KEYSYM_NUM_LOCK: + return SPICE_INPUTS_NUM_LOCK; + + /* Caps lock */ + case GUAC_SPICE_KEYSYM_CAPS_LOCK: + return SPICE_INPUTS_CAPS_LOCK; + + } + + /* Not a lock key */ + return 0; + +} + +/** + * Immediately sends an Spice key event having the given scancode and flags. + * + * @param spice_client + * The Spice client instance associated with the SPICE session along which the + * key event should be sent. + * + * @param scancode + * The scancode of the key to press or release via the Spice key event. + * + * @param flags + * Any Spice-specific flags required for the provided scancode to have the + * intended meaning, such as KBD_FLAGS_EXTENDED. The possible flags and + * their meanings are dictated by SPICE. KBD_FLAGS_DOWN and KBD_FLAGS_UP + * need not be specified here - they will automatically be added depending + * on the value specified for the pressed parameter. + * + * @param pressed + * Non-zero if the key is being pressed, zero if the key is being released. + */ +static void guac_spice_send_key_event(guac_spice_client* spice_client, + int scancode, int flags, int pressed) { + + /* Send actual key press or release */ + pthread_mutex_lock(&(spice_client->message_lock)); + if (pressed) + spice_inputs_channel_key_press(spice_client->inputs_channel, scancode); + else + spice_inputs_channel_key_release(spice_client->inputs_channel, scancode); + pthread_mutex_unlock(&(spice_client->message_lock)); + +} + +/** + * Immediately sends an Spice synchonize event having the given flags. A Spice + * synchronize event sets the state of remote lock keys absolutely, where a + * lock key will be active only if its corresponding flag is set in the event. + * + * @param spice_client + * The Spice client instance associated with the Spice session along which the + * synchronize event should be sent. + * + * @param modifiers + * Bitwise OR of the flags representing the lock keys which should be set, + * if any, as dictated by the Spice protocol. If no flags are set, then no + * lock keys will be active. + */ +static void guac_spice_send_synchronize_event(guac_spice_client* spice_client, + unsigned int modifiers) { + + /* Skip if inputs channel is not connected */ + if (spice_client->inputs_channel == NULL) + return; + + /* Synchronize lock key states */ + pthread_mutex_lock(&(spice_client->message_lock)); + spice_inputs_channel_set_key_locks(spice_client->inputs_channel, modifiers); + pthread_mutex_unlock(&(spice_client->message_lock)); + +} + +/** + * Given a keyboard instance and X11 keysym, returns a pointer to the + * keys_by_keysym entry that represents the key having that keysym within the + * keyboard, regardless of whether the key is currently defined. If no such key + * can exist (the keysym cannot be mapped or is out of range), NULL is + * returned. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param keysym + * The keysym of the key to lookup within the given keyboard. + * + * @return + * A pointer to the keys_by_keysym entry which represents or can represent + * the key having the given keysym, or NULL if no such keysym can be + * defined within a guac_spice_keyboard structure. + */ +static guac_spice_key** guac_spice_keyboard_map_key(guac_spice_keyboard* keyboard, + int keysym) { + + int index; + + /* Map keysyms between 0x0000 and 0xFFFF directly */ + if (keysym >= 0x0000 && keysym <= 0xFFFF) + index = keysym; + + /* Map all Unicode keysyms from U+0000 to U+FFFF */ + else if (keysym >= 0x1000000 && keysym <= 0x100FFFF) + index = 0x10000 + (keysym & 0xFFFF); + + /* All other keysyms are unmapped */ + else + return NULL; + + /* Corresponding key mapping (defined or not) has been located */ + return &(keyboard->keys_by_keysym[index]); + +} + +/** + * Returns the number of bits that are set within the given integer (the number + * of 1s in the binary expansion of the given integer). + * + * @param value + * The integer to read. + * + * @return + * The number of bits that are set within the given integer. + */ +static int guac_spice_count_bits(unsigned int value) { + + int bits = 0; + + while (value) { + bits += value & 1; + value >>= 1; + } + + return bits; + +} + +/** + * Returns an estimated cost for sending the necessary Spice events to type the + * key described by the given guac_spice_keysym_desc, given the current lock and + * modifier state of the keyboard. A higher cost value indicates that a greater + * number of events are expected to be required. + * + * Lower-cost approaches should be preferred when multiple alternatives exist + * for typing a particular key, as the lower cost implies fewer additional key + * events required to produce the expected behavior. For example, if Caps Lock + * is enabled, typing an uppercase "A" by pressing the "A" key has a lower cost + * than disabling Caps Lock and pressing Shift+A. + * + * @param keyboard + * The guac_spice_keyboard associated with the current SPICE session. + * + * @param def + * The guac_spice_keysym_desc that describes the key being pressed, as well + * as any requirements that must be satisfied for the key to be interpreted + * as expected. + * + * @return + * An arbitrary integer value which indicates the overall estimated + * complexity of typing the given key. + */ +static int guac_spice_keyboard_get_cost(guac_spice_keyboard* keyboard, + const guac_spice_keysym_desc* def) { + + unsigned int modifiers = guac_spice_keyboard_get_modifier_flags(keyboard); + + /* Each change to any key requires one event, by definition */ + int cost = 1; + + /* Each change to a lock requires roughly two key events */ + unsigned int update_locks = (def->set_modifiers & ~keyboard->modifiers) | (def->clear_modifiers & keyboard->modifiers); + cost += guac_spice_count_bits(update_locks) * 2; + + /* Each change to a modifier requires one key event */ + unsigned int update_modifiers = (def->clear_modifiers & modifiers) | (def->set_modifiers & ~modifiers); + cost += guac_spice_count_bits(update_modifiers); + + return cost; + +} + +/** + * Returns a pointer to the guac_spice_key structure representing the + * definition(s) and state of the key having the given keysym. If no such key + * is defined within the keyboard layout of the Spice server, NULL is returned. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param keysym + * The keysym of the key to lookup within the given keyboard. + * + * @return + * A pointer to the guac_spice_key structure representing the definition(s) + * and state of the key having the given keysym, or NULL if no such key is + * defined within the keyboard layout of the Spice server. + */ +static guac_spice_key* guac_spice_keyboard_get_key(guac_spice_keyboard* keyboard, + int keysym) { + + /* Verify that the key is actually defined */ + guac_spice_key** key_by_keysym = guac_spice_keyboard_map_key(keyboard, keysym); + if (key_by_keysym == NULL) + return NULL; + + return *key_by_keysym; + +} + +/** + * Given a key which may have multiple possible definitions, returns the + * definition that currently has the lowest cost, taking into account the + * current keyboard lock and modifier states. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param key + * The key whose lowest-cost possible definition should be retrieved. + * + * @return + * A pointer to the guac_spice_keysym_desc which defines the current + * lowest-cost method of typing the given key. + */ +static const guac_spice_keysym_desc* guac_spice_keyboard_get_definition(guac_spice_keyboard* keyboard, + guac_spice_key* key) { + + /* Consistently map the same entry so long as the key is held */ + if (key->pressed != NULL) + return key->pressed; + + /* Calculate cost of first definition of key (there must always be at least + * one definition) */ + const guac_spice_keysym_desc* best_def = key->definitions[0]; + int best_cost = guac_spice_keyboard_get_cost(keyboard, best_def); + + /* If further definitions exist, choose the definition with the lowest + * overall cost */ + for (int i = 1; i < key->num_definitions; i++) { + + const guac_spice_keysym_desc* def = key->definitions[i]; + int cost = guac_spice_keyboard_get_cost(keyboard, def); + + if (cost < best_cost) { + best_def = def; + best_cost = cost; + } + + } + + return best_def; + +} + +/** + * Adds the keysym/scancode mapping described by the given guac_spice_keysym_desc + * to the internal mapping of the keyboard. If insufficient space remains for + * additional keysyms, or the given keysym has already reached the maximum + * number of possible definitions, the mapping is ignored and the failure is + * logged. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param mapping + * The keysym/scancode mapping that should be added to the given keyboard. + */ +static void guac_spice_keyboard_add_mapping(guac_spice_keyboard* keyboard, + const guac_spice_keysym_desc* mapping) { + + /* Locate corresponding keysym-to-key translation entry within keyboard + * structure */ + guac_spice_key** key_by_keysym = guac_spice_keyboard_map_key(keyboard, mapping->keysym); + if (key_by_keysym == NULL) { + guac_client_log(keyboard->client, GUAC_LOG_DEBUG, "Ignoring unmappable keysym 0x%X", mapping->keysym); + return; + } + + /* If not yet pointing to a key, point keysym-to-key translation entry at + * next available storage */ + if (*key_by_keysym == NULL) { + + if (keyboard->num_keys == GUAC_SPICE_KEYBOARD_MAX_KEYSYMS) { + guac_client_log(keyboard->client, GUAC_LOG_DEBUG, "Key definition " + "for keysym 0x%X dropped: Keymap exceeds maximum " + "supported number of keysyms", + mapping->keysym); + return; + } + + *key_by_keysym = &keyboard->keys[keyboard->num_keys++]; + + } + + guac_spice_key* key = *key_by_keysym; + + /* Add new definition only if sufficient space remains */ + if (key->num_definitions == GUAC_SPICE_KEY_MAX_DEFINITIONS) { + guac_client_log(keyboard->client, GUAC_LOG_DEBUG, "Key definition " + "for keysym 0x%X dropped: Maximum number of possible " + "definitions has been reached for this keysym", + mapping->keysym); + return; + } + + /* Store new possible definition of key */ + key->definitions[key->num_definitions++] = mapping; + +} + +/** + * Loads all keysym/scancode mappings declared within the given keymap and its + * parent keymap, if any. These mappings are stored within the given + * guac_spice_keyboard structure for future use in translating keysyms to the + * scancodes required by Spice key events. + * + * @param keyboard + * The guac_spice_keyboard which should be initialized with the + * keysym/scancode mapping defined in the given keymap. + * + * @param keymap + * The keymap to use to populate the given client's keysym/scancode + * mapping. + */ +static void guac_spice_keyboard_load_keymap(guac_spice_keyboard* keyboard, + const guac_spice_keymap* keymap) { + + /* If parent exists, load parent first */ + if (keymap->parent != NULL) + guac_spice_keyboard_load_keymap(keyboard, keymap->parent); + + /* Log load */ + guac_client_log(keyboard->client, GUAC_LOG_INFO, + "Loading keymap \"%s\"", keymap->name); + + /* Copy mapping into keymap */ + const guac_spice_keysym_desc* mapping = keymap->mapping; + while (mapping->keysym != 0) { + guac_spice_keyboard_add_mapping(keyboard, mapping++); + } + +} + +guac_spice_keyboard* guac_spice_keyboard_alloc(guac_client* client, + const guac_spice_keymap* keymap) { + + guac_spice_keyboard* keyboard = calloc(1, sizeof(guac_spice_keyboard)); + keyboard->client = client; + + /* Load keymap into keyboard */ + guac_spice_keyboard_load_keymap(keyboard, keymap); + + return keyboard; + +} + +void guac_spice_keyboard_free(guac_spice_keyboard* keyboard) { + free(keyboard); +} + +int guac_spice_keyboard_is_defined(guac_spice_keyboard* keyboard, int keysym) { + + /* Return whether the mapping actually exists */ + return guac_spice_keyboard_get_key(keyboard, keysym) != NULL; + +} + +int guac_spice_keyboard_is_pressed(guac_spice_keyboard* keyboard, int keysym) { + + guac_spice_key* key = guac_spice_keyboard_get_key(keyboard, keysym); + return key != NULL && key->pressed != NULL; + +} + +unsigned int guac_spice_keyboard_get_modifier_flags(guac_spice_keyboard* keyboard) { + + unsigned int modifier_flags = 0; + + /* Shift */ + if (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_LSHIFT) + || guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_RSHIFT)) + modifier_flags |= GUAC_SPICE_KEYMAP_MODIFIER_SHIFT; + + /* Dedicated AltGr key */ + if (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_RALT) + || guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_ALTGR)) + modifier_flags |= GUAC_SPICE_KEYMAP_MODIFIER_ALTGR; + + /* AltGr via Ctrl+Alt */ + if (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_LALT) + && (guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_RCTRL) + || guac_spice_keyboard_is_pressed(keyboard, GUAC_SPICE_KEYSYM_LCTRL))) + modifier_flags |= GUAC_SPICE_KEYMAP_MODIFIER_ALTGR; + + return modifier_flags; + +} + +/** + * Presses/releases the requested key by sending one or more Spice key events, as + * defined within the keymap defining that key. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param key + * The guac_spice_keysym_desc of the key being pressed or released, as + * retrieved from the relevant keymap. + * + * @param pressed + * Zero if the key is being released, non-zero otherwise. + * + * @return + * Zero if the key was successfully pressed/released, non-zero if the key + * cannot be sent using Spice key events. + */ +static const guac_spice_keysym_desc* guac_spice_keyboard_send_defined_key(guac_spice_keyboard* keyboard, + guac_spice_key* key, int pressed) { + + guac_client* client = keyboard->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + const guac_spice_keysym_desc* keysym_desc = guac_spice_keyboard_get_definition(keyboard, key); + if (keysym_desc->scancode == 0) + return NULL; + + /* Update state of required locks and modifiers only when key is just + * now being pressed */ + if (pressed) { + guac_spice_keyboard_update_locks(keyboard, + keysym_desc->set_locks, + keysym_desc->clear_locks); + + guac_spice_keyboard_update_modifiers(keyboard, + keysym_desc->set_modifiers, + keysym_desc->clear_modifiers); + } + + /* Fire actual key event for target key */ + guac_client_log(client, GUAC_LOG_TRACE, "Firing scancode event: %08x", keysym_desc->scancode); + guac_spice_send_key_event(spice_client, keysym_desc->scancode, + keysym_desc->flags, pressed); + + return keysym_desc; + +} + +void guac_spice_keyboard_update_locks(guac_spice_keyboard* keyboard, + unsigned int set_modifiers, unsigned int clear_modifiers) { + + guac_client* client = keyboard->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Calculate updated lock flags */ + unsigned int modifiers = (keyboard->modifiers | set_modifiers) & ~clear_modifiers; + + /* Synchronize remote side only if lock flags have changed */ + if (modifiers != keyboard->modifiers) { + guac_spice_send_synchronize_event(spice_client, modifiers); + keyboard->modifiers = modifiers; + } + +} + +void guac_spice_keyboard_update_modifiers(guac_spice_keyboard* keyboard, + unsigned int set_flags, unsigned int clear_flags) { + + unsigned int modifier_flags = guac_spice_keyboard_get_modifier_flags(keyboard); + + /* Only clear modifiers that are set */ + clear_flags &= modifier_flags; + + /* Only set modifiers that are currently cleared */ + set_flags &= ~modifier_flags; + + /* Press/release Shift as needed */ + if (set_flags & GUAC_SPICE_KEYMAP_MODIFIER_SHIFT) { + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LSHIFT, 1, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + } + else if (clear_flags & GUAC_SPICE_KEYMAP_MODIFIER_SHIFT) { + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LSHIFT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_RSHIFT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + } + + /* Press/release AltGr as needed */ + if (set_flags & GUAC_SPICE_KEYMAP_MODIFIER_ALTGR) { + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_ALTGR, 1, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + } + else if (clear_flags & GUAC_SPICE_KEYMAP_MODIFIER_ALTGR) { + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_ALTGR, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LALT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_RALT, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_LCTRL, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + guac_spice_keyboard_update_keysym(keyboard, GUAC_SPICE_KEYSYM_RCTRL, 0, GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + } + +} + +int guac_spice_keyboard_update_keysym(guac_spice_keyboard* keyboard, + int keysym, int pressed, guac_spice_key_source source) { + + /* Synchronize lock keys states, if this has not yet been done */ + if (!keyboard->synchronized) { + + guac_client* client = keyboard->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Synchronize remote lock key states with local state */ + guac_spice_send_synchronize_event(spice_client, keyboard->modifiers); + keyboard->synchronized = 1; + + } + + guac_spice_key* key = guac_spice_keyboard_get_key(keyboard, keysym); + + /* Update tracking of client-side keyboard state but only for keys which + * are tracked server-side, as well (to ensure that the key count remains + * correct, even if a user sends extra unbalanced or excessive press and + * release events) */ + if (source == GUAC_SPICE_KEY_SOURCE_CLIENT && key != NULL) { + if (pressed && !key->user_pressed) { + keyboard->user_pressed_keys++; + key->user_pressed = 1; + } + else if (!pressed && key->user_pressed) { + keyboard->user_pressed_keys--; + key->user_pressed = 0; + } + } + + /* Send events and update server-side lock state only if server-side key + * state is changing (or if server-side state of this key is untracked) */ + if (key == NULL || (pressed && key->pressed == NULL) || (!pressed && key->pressed != NULL)) { + + /* Toggle locks on keydown */ + if (pressed) + keyboard->modifiers ^= guac_spice_keyboard_lock_flag(keysym); + + /* If key is known, update state and attempt to send using normal SPICE key + * events */ + const guac_spice_keysym_desc* definition = NULL; + if (key != NULL) { + definition = guac_spice_keyboard_send_defined_key(keyboard, key, pressed); + key->pressed = pressed ? definition : NULL; + } + + /* Fall back to dead keys or Unicode events if otherwise undefined inside + * current keymap (note that we only handle "pressed" here, as neither + * Unicode events nor dead keys can have a pressed/released state) */ + if (definition == NULL && pressed) { + guac_client_log(keyboard->client, GUAC_LOG_WARNING, + "Undefined key will not be sent: %d", keysym); + } + + } + + /* Reset SPICE server keyboard state (releasing any automatically + * pressed keys) once all keys have been released on the client + * side */ + if (source == GUAC_SPICE_KEY_SOURCE_CLIENT && keyboard->user_pressed_keys == 0) + guac_spice_keyboard_reset(keyboard); + + return 0; + +} + +void guac_spice_keyboard_reset(guac_spice_keyboard* keyboard) { + + /* Release all pressed keys */ + for (int i = 0; i < keyboard->num_keys; i++) { + guac_spice_key* key = &keyboard->keys[i]; + if (key->pressed != NULL) + guac_spice_keyboard_update_keysym(keyboard, key->pressed->keysym, 0, + GUAC_SPICE_KEY_SOURCE_SYNTHETIC); + } + +} + +void guac_spice_keyboard_set_indicators(SpiceChannel* channel, guac_client* client) { + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + pthread_rwlock_rdlock(&(spice_client->lock)); + + /* Skip if keyboard not yet ready */ + guac_spice_keyboard* keyboard = spice_client->keyboard; + if (keyboard == NULL) + goto complete; + + unsigned int modifiers; + g_object_get(channel, SPICE_PROPERTY_KEY_MODIFIERS, &modifiers, NULL); + + /* Update with received locks */ + guac_client_log(client, GUAC_LOG_DEBUG, "Received updated keyboard lock flags from Spice server: 0x%X", modifiers); + keyboard->modifiers = modifiers; + +complete: + pthread_rwlock_unlock(&(spice_client->lock)); + +} diff --git a/src/protocols/spice/keyboard.h b/src/protocols/spice/keyboard.h new file mode 100644 index 00000000..da84dfab --- /dev/null +++ b/src/protocols/spice/keyboard.h @@ -0,0 +1,323 @@ +/* + * 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_SPICE_KEYBOARD_H +#define GUAC_SPICE_KEYBOARD_H + +#include "keymap.h" + +#include +#include + +/** + * The maximum number of distinct keysyms that any particular keyboard may support. + */ +#define GUAC_SPICE_KEYBOARD_MAX_KEYSYMS 1024 + +/** + * The maximum number of unique modifier variations that any particular keysym + * may define. For example, on a US English keyboard, an uppercase "A" may be + * typed by pressing Shift+A with Caps Lock unset, or by pressing A with Caps + * Lock set (two variations). + */ +#define GUAC_SPICE_KEY_MAX_DEFINITIONS 4 + +/** + * All possible sources of Spice key events tracked by guac_spice_keyboard. + */ +typedef enum guac_spice_key_source { + + /** + * The key event was received directly from the Guacamole client via a + * "key" instruction. + */ + GUAC_SPICE_KEY_SOURCE_CLIENT = 0, + + /** + * The key event is being synthesized internally within the Spice support. + */ + GUAC_SPICE_KEY_SOURCE_SYNTHETIC = 1 + +} guac_spice_key_source; + +/** + * A representation of a single key within the overall local keyboard, + * including the definition of that key within the Spice server's keymap and + * whether the key is currently pressed locally. + */ +typedef struct guac_spice_key { + + /** + * All definitions of this key within the Spice server's keymap (keyboard + * layout). Each definition describes which scancode corresponds to this + * key from the perspective of the Spice server, as well as which other + * scancodes must be pressed/released for this key to have the desired + * meaning. + */ + const guac_spice_keysym_desc* definitions[GUAC_SPICE_KEY_MAX_DEFINITIONS]; + + /** + * The number of definitions within the definitions array. If this key does + * not exist within the Spice server's keymap, this will be 0. + */ + int num_definitions; + + /** + * The definition of this key that is currently pressed. If this key is not + * currently pressed, this will be NULL. + */ + const guac_spice_keysym_desc* pressed; + + /** + * Whether this key is currently pressed by the user, and is included among + * the total tracked by user_pressed_keys within guac_spice_keyboard. + */ + int user_pressed; + +} guac_spice_key; + +/** + * The current keyboard state of an Spice session. + */ +typedef struct guac_spice_keyboard { + + /** + * The guac_client associated with the Spice session whose keyboard state is + * being managed by this guac_spice_keyboard. + */ + guac_client* client; + + /** + * The local state of all known lock keys, as a bitwise OR of all Spice lock + * key flags. Legal flags are KBD_SYNC_SCROLL_LOCK, KBD_SYNC_NUM_LOCK, + * KBD_SYNC_CAPS_LOCK, and KBD_SYNC_KANA_LOCK. + */ + int modifiers; + + /** + * Whether the states of remote lock keys (Caps lock, Num lock, etc.) have + * been synchronized with local lock key states. + */ + int synchronized; + + /** + * The number of keys stored within the keys array. + */ + unsigned int num_keys; + + /** + * The local state of all keys, as well as the necessary information to + * translate received keysyms into scancodes or sequences of scancodes for + * Spice. The state of each key is updated based on received Guacamole key + * events, while the information describing the behavior and scancode + * mapping of each key is populated based on an associated keymap. + * + * Keys within this array are in arbitrary order. + */ + guac_spice_key keys[GUAC_SPICE_KEYBOARD_MAX_KEYSYMS]; + + /** + * Lookup table into the overall keys array, locating the guac_spice_key + * associated with any particular keysym. If a keysym has no corresponding + * guac_spice_key within the keys array, its entry within this lookuptable + * will be NULL. + * + * The index of the key for a given keysym is determined based on a + * simple transformation of the keysym itself. Keysyms between 0x0000 and + * 0xFFFF inclusive are mapped to 0x00000 through 0x0FFFF, while keysyms + * between 0x1000000 and 0x100FFFF inclusive (keysyms which are derived + * from Unicode) are mapped to 0x10000 through 0x1FFFF. + */ + guac_spice_key* keys_by_keysym[0x20000]; + + /** + * The total number of keys that the user of the connection is currently + * holding down. This value indicates only the client-side keyboard state. + * It DOES NOT indicate the number of keys currently pressed within the + * Spice server. + */ + int user_pressed_keys; + +} guac_spice_keyboard; + +/** + * Allocates a new guac_spice_keyboard which manages the keyboard state of the + * SPICE session associated with the given guac_client. Keyboard events will be + * dynamically translated from keysym to Spice scancode according to the + * provided keymap. The returned guac_spice_keyboard must eventually be freed + * with guac_spice_keyboard_free(). + * + * @param client + * The guac_client associated with the Spice session whose keyboard state is + * to be managed by the newly-allocated guac_spice_keyboard. + * + * @param keymap + * The keymap which should be used to translate keyboard events. + * + * @return + * A newly-allocated guac_spice_keyboard which manages the keyboard state + * for the Spice session associated given guac_client. + */ +guac_spice_keyboard* guac_spice_keyboard_alloc(guac_client* client, + const guac_spice_keymap* keymap); + +/** + * Frees all memory allocated for the given guac_spice_keyboard. The + * guac_spice_keyboard must have been previously allocated via + * guac_spice_keyboard_alloc(). + * + * @param keyboard + * The guac_spice_keyboard instance which should be freed. + */ +void guac_spice_keyboard_free(guac_spice_keyboard* keyboard); + +/** + * Returns whether the given keysym is defined for the keyboard layout + * associated with the given keyboard. + * + * @param keyboard + * The guac_spice_keyboard instance to check. + * + * @param keysym + * The keysym of the key being checked against the keyboard layout of the + * given keyboard. + * + * @return + * Non-zero if the key is explicitly defined within the keyboard layout of + * the given keyboard, zero otherwise. + */ +int guac_spice_keyboard_is_defined(guac_spice_keyboard* keyboard, int keysym); + +/** + * Returns whether the key having the given keysym is currently pressed. + * + * @param keyboard + * The guac_spice_keyboard instance to check. + * + * @param keysym + * The keysym of the key being checked. + * + * @return + * Non-zero if the key is currently pressed, zero otherwise. + */ +int guac_spice_keyboard_is_pressed(guac_spice_keyboard* keyboard, int keysym); + +/** + * Returns the local state of all known modifier keys, as a bitwise OR of the + * modifier flags used by the keymaps. Alternative methods of producing the + * effect of certain modifiers, such as holding Ctrl+Alt for AltGr when a + * dedicated AltGr key is unavailable, are taken into account. + * + * @see GUAC_SPICE_KEYMAP_MODIFIER_SHIFT + * @see GUAC_SPICE_KEYMAP_MODIFIER_ALTGR + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @return + * The local state of all known modifier keys. + */ +unsigned int guac_spice_keyboard_get_modifier_flags(guac_spice_keyboard* keyboard); + +/** + * Updates the local state of the lock keys (such as Caps lock or Num lock), + * synchronizing the remote state of those keys if it is expected to differ. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param set_modifiers + * The lock key flags which should be set. Legal flags are + * KBD_SYNC_SCROLL_LOCK, KBD_SYNC_NUM_LOCK, KBD_SYNC_CAPS_LOCK, and + * KBD_SYNC_KANA_LOCK. + * + * @param clear_modifiers + * The lock key flags which should be cleared. Legal flags are + * KBD_SYNC_SCROLL_LOCK, KBD_SYNC_NUM_LOCK, KBD_SYNC_CAPS_LOCK, and + * KBD_SYNC_KANA_LOCK. + */ +void guac_spice_keyboard_update_locks(guac_spice_keyboard* keyboard, + unsigned int set_modifiers, unsigned int clear_modifiers); + +/** + * Updates the local state of the modifier keys (such as Shift or AltGr), + * synchronizing the remote state of those keys if it is expected to differ. + * Valid modifier flags are defined by keymap.h. + * + * @see GUAC_SPICE_KEYMAP_MODIFIER_SHIFT + * @see GUAC_SPICE_KEYMAP_MODIFIER_ALTGR + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + * + * @param set_modifiers + * The modifier key flags which should be set. + * + * @param clear_modifiers + * The modifier key flags which should be cleared. + */ +void guac_spice_keyboard_update_modifiers(guac_spice_keyboard* keyboard, + unsigned int set_modifiers, unsigned int clear_modifiers); + +/** + * Updates the local state of the given keysym, sending the key events required + * to replicate that state remotely (on the Spice server). The key events sent + * will depend on the current keymap. + * + * @param keyboard + * The guac_spice_keyboard associated with the current SPICE session. + * + * @param keysym + * The keysym being pressed or released. + * + * @param pressed + * Zero if the keysym is being released, non-zero otherwise. + * + * @param source + * The source of the key event represented by this call to + * guac_spice_keyboard_update_keysym(). + * + * @return + * Zero if the keys were successfully sent, non-zero otherwise. + */ +int guac_spice_keyboard_update_keysym(guac_spice_keyboard* keyboard, + int keysym, int pressed, guac_spice_key_source source); + +/** + * Releases all currently pressed keys, sending key release events to the Spice + * server as necessary. Lock states (Caps Lock, etc.) are not affected. + * + * @param keyboard + * The guac_spice_keyboard associated with the current Spice session. + */ +void guac_spice_keyboard_reset(guac_spice_keyboard* keyboard); + +/** + * Callback which is invoked when the Spice server reports changes to keyboard + * lock status using a Server Set Keyboard Indicators PDU. + * + * @param channel + * The spiceContext associated with the current Spice session. + * + * @param client + * The guac_client object associated with the callback. + */ +void guac_spice_keyboard_set_indicators(SpiceChannel* channel, guac_client* client); + +#endif \ No newline at end of file diff --git a/src/protocols/spice/keymap.c b/src/protocols/spice/keymap.c new file mode 100644 index 00000000..51b75a78 --- /dev/null +++ b/src/protocols/spice/keymap.c @@ -0,0 +1,41 @@ +/* + * 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 "keymap.h" + +#include + +const guac_spice_keymap* guac_spice_keymap_find(const char* name) { + + /* For each keymap */ + const guac_spice_keymap** current = GUAC_SPICE_KEYMAPS; + while (*current != NULL) { + + /* If name matches, done */ + if (strcmp((*current)->name, name) == 0) + return *current; + + current++; + } + + /* Failure */ + return NULL; + +} + diff --git a/src/protocols/spice/keymap.h b/src/protocols/spice/keymap.h new file mode 100644 index 00000000..69a02df2 --- /dev/null +++ b/src/protocols/spice/keymap.h @@ -0,0 +1,202 @@ +/* + * 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_SPICE_KEYMAP_H +#define GUAC_SPICE_KEYMAP_H + +/** + * The X11 keysym for Num Lock. + */ +#define GUAC_SPICE_KEYSYM_NUM_LOCK 0xFF7F + +/** + * The X11 keysym for Scroll Lock. + */ +#define GUAC_SPICE_KEYSYM_SCROLL_LOCK 0xFF14 + +/** + * The X11 keysym for Caps Lock. + */ +#define GUAC_SPICE_KEYSYM_CAPS_LOCK 0xFFE5 + +/** + * The X11 keysym for Kana Lock. + */ +#define GUAC_SPICE_KEYSYM_KANA_LOCK 0xFF2D + +/** + * The X11 keysym for Left Shift. + */ +#define GUAC_SPICE_KEYSYM_LSHIFT 0xFFE1 + +/** + * The X11 keysym for Right Shift. + */ +#define GUAC_SPICE_KEYSYM_RSHIFT 0xFFE2 + +/** + * The X11 keysym for Left Ctrl. + */ +#define GUAC_SPICE_KEYSYM_LCTRL 0xFFE3 + +/** + * The X11 keysym for Right Ctrl. + */ +#define GUAC_SPICE_KEYSYM_RCTRL 0xFFE4 + +/** + * The X11 keysym for Left Alt. + */ +#define GUAC_SPICE_KEYSYM_LALT 0xFFE9 + +/** + * The X11 keysym for Right Alt. + */ +#define GUAC_SPICE_KEYSYM_RALT 0xFFEA + +/** + * The X11 keysym for AltGr. + */ +#define GUAC_SPICE_KEYSYM_ALTGR 0xFE03 + +/** + * Bitwise flag value representing the Shift modifier. + */ +#define GUAC_SPICE_KEYMAP_MODIFIER_SHIFT 1 + +/** + * Bitwise flag value representing the AltGr modifier. + */ +#define GUAC_SPICE_KEYMAP_MODIFIER_ALTGR 2 + +/** + * Represents a keysym-to-scancode mapping for Spice, with extra information + * about the state of prerequisite keysyms. + */ +typedef struct guac_spice_keysym_desc { + + /** + * The keysym being mapped. + */ + int keysym; + + /** + * The scancode this keysym maps to. + */ + int scancode; + + /** + * Required Spice-specific flags that must be sent along with the scancode. + */ + int flags; + + /** + * Bitwise-OR of the flags of any modifiers that must be active for the + * associated scancode to be interpreted as this keysym. + * + * If the associated keysym is pressed, and any of these modifiers are not + * currently active, Guacamole's Spice support must send additional events + * to activate these modifiers prior to sending the scancode for this + * keysym. + * + * @see GUAC_SPICE_KEYMAP_MODIFIER_SHIFT + * @see GUAC_SPICE_KEYMAP_MODIFIER_ALTGR + */ + const unsigned int set_modifiers; + + /** + * Bitwise-OR of the flags of any modifiers that must NOT be active for the + * associated scancode to be interpreted as this keysym. + * + * If the associated keysym is pressed, and any of these modifiers are + * currently active, Guacamole's Spice support must send additional events + * to deactivate these modifiers prior to sending the scancode for this + * keysym. + * + * @see GUAC_SPICE_KEYMAP_MODIFIER_SHIFT + * @see GUAC_SPICE_KEYMAP_MODIFIER_ALTGR + */ + const unsigned int clear_modifiers; + + /** + * Bitwise OR of the flags of all lock keys (ie: Caps lock, Num lock, etc.) + * which must be active for this keysym to be properly typed. Legal flags + * are KBD_SYNC_SCROLL_LOCK, KBD_SYNC_NUM_LOCK, KBD_SYNC_CAPS_LOCK, and + * KBD_SYNC_KANA_LOCK. + */ + const unsigned int set_locks; + + /** + * Bitwise OR of the flags of all lock keys (ie: Caps lock, Num lock, etc.) + * which must be inactive for this keysym to be properly typed. Legal flags + * are KBD_SYNC_SCROLL_LOCK, KBD_SYNC_NUM_LOCK, KBD_SYNC_CAPS_LOCK, and + * KBD_SYNC_KANA_LOCK. + */ + const unsigned int clear_locks; + +} guac_spice_keysym_desc; + +/** + * Hierarchical keysym mapping + */ +typedef struct guac_spice_keymap guac_spice_keymap; +struct guac_spice_keymap { + + /** + * The parent mapping this map will inherit its initial mapping from. + * Any other mapping information will add to or override the mapping + * inherited from the parent. + */ + const guac_spice_keymap* parent; + + /** + * Descriptive name of this keymap + */ + const char* name; + + /** + * Null-terminated array of scancode mappings. + */ + const guac_spice_keysym_desc* mapping; + +}; + +/** + * The name of the default keymap, which MUST exist. + */ +#define GUAC_SPICE_DEFAULT_KEYMAP "en-us-qwerty" + +/** + * NULL-terminated array of all keymaps. + */ +extern const guac_spice_keymap* GUAC_SPICE_KEYMAPS[]; + +/** + * Return the keymap having the given name, if any, or NULL otherwise. + * + * @param name + * The name of the keymap to find. + * + * @return + * The keymap having the given name, or NULL if no such keymap exists. + */ +const guac_spice_keymap* guac_spice_keymap_find(const char* name); + +#endif + diff --git a/src/protocols/spice/keymaps/base.keymap b/src/protocols/spice/keymaps/base.keymap new file mode 100644 index 00000000..5b93fab6 --- /dev/null +++ b/src/protocols/spice/keymaps/base.keymap @@ -0,0 +1,93 @@ +# +# 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. +# + +name "base" + +# Typeable characters +map 0x39 ~ " " # Space +map 0x0F ~ 0xff09 # Tab + +# Control characters +map 0x0E ~ 0xff08 # BackSpace +map 0x1C ~ 0xff0d # Return +map 0x01 ~ 0xff1b # Escape +map +ext 0x52 ~ 0xff63 # Insert +map +ext 0x53 ~ 0xffff # Delete +map +ext 0x47 ~ 0xff50 # Home +map +ext 0x4F ~ 0xff57 # End +map +ext 0x4B ~ 0xff51 # Left +map +ext 0x48 ~ 0xff52 # Up +map +ext 0x4D ~ 0xff53 # Right +map +ext 0x50 ~ 0xff54 # Down +map +ext 0x49 ~ 0xff55 # Page_Up +map +ext 0x51 ~ 0xff56 # Page_Down +map +ext 0x37 ~ 0xff61 # Print Screen + +# Locks +map 0x45 ~ 0xff7f # Num_Lock +map 0x46 ~ 0xff14 # Scroll_Lock +map 0x3A ~ 0xffe5 # Caps_Lock + +# Keypad numerals +map -shift +num 0x52 ~ 0xffb0 # KP_0 +map -shift +num 0x4F ~ 0xffb1 # KP_1 +map -shift +num 0x50 ~ 0xffb2 # KP_2 +map -shift +num 0x51 ~ 0xffb3 # KP_3 +map -shift +num 0x4B ~ 0xffb4 # KP_4 +map -shift +num 0x4C ~ 0xffb5 # KP_5 +map -shift +num 0x4D ~ 0xffb6 # KP_6 +map -shift +num 0x47 ~ 0xffb7 # KP_7 +map -shift +num 0x48 ~ 0xffb8 # KP_8 +map -shift +num 0x49 ~ 0xffb9 # KP_9 + +# Keypad operators +map 0x37 ~ 0xffaa # KP_multiply +map 0x4e ~ 0xffab # KP_add +map 0x4a ~ 0xffad # KP_subtract +map 0x53 ~ 0xffae # KP_decimal +map +ext 0x35 ~ 0xffaf # KP_divide + +# F keys +map 0x3B ~ 0xffbe # F1 +map 0x3C ~ 0xffbf # F2 +map 0x3D ~ 0xffc0 # F3 +map 0x3E ~ 0xffc1 # F4 +map 0x3F ~ 0xffc2 # F5 +map 0x40 ~ 0xffc3 # F6 +map 0x41 ~ 0xffc4 # F7 +map 0x42 ~ 0xffc5 # F8 +map 0x43 ~ 0xffc6 # F9 +map 0x44 ~ 0xffc7 # F10 +map 0x57 ~ 0xffc8 # F11 +map 0x58 ~ 0xffc9 # F12 + +# Modifiers +map 0x2A ~ 0xffe1 # Shift_L +map 0x36 ~ 0xffe2 # Shift_R +map 0x1D ~ 0xffe3 # Control_L +map +ext 0x1D ~ 0xffe4 # Control_R +map 0x38 ~ 0xffe9 # Alt_L +map +ext 0x38 ~ 0xffea # Alt_R +map +ext 0x38 ~ 0xfe03 # AltGr +map +ext 0x5B ~ 0xffe7 # Meta_L +map +ext 0x5C ~ 0xffe8 # Meta_R +map +ext 0x5B ~ 0xffeb # Super_L +map +ext 0x5C ~ 0xffec # Super_R +map +ext 0x5D ~ 0xff67 # Menu + diff --git a/src/protocols/spice/keymaps/da_dk_qwerty.keymap b/src/protocols/spice/keymaps/da_dk_qwerty.keymap new file mode 100644 index 00000000..07611e88 --- /dev/null +++ b/src/protocols/spice/keymaps/da_dk_qwerty.keymap @@ -0,0 +1,73 @@ +# +# 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. +# + +parent "base" +name "da-dk-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890+" +map -caps -altgr -shift 0x10..0x1A ~ "qwertyuiopå" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklæø'" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890+" +map +caps -altgr -shift 0x10..0x1A ~ "QWERTYUIOPÅ" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÆØ'" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x03 ~ "@" +map +altgr -shift 0x04 ~ "£" +map +altgr -shift 0x05 ~ "$" +map +altgr -shift 0x08 ~ "{" +map +altgr -shift 0x09 ~ "[" +map +altgr -shift 0x0A ~ "]" +map +altgr -shift 0x0B ~ "}" +map +altgr -shift 0x56 ~ "\" + +map +altgr -shift 0x12 ~ "€" + +map +altgr -shift 0x0D ~ "|" +map +altgr -shift 0x32 ~ "µ" + +# +# Dead keys +# + +map -altgr -shift 0x0D ~ 0xFE51 # Dead acute +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut +map -altgr +shift 0x1B ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x1B ~ 0xFE53 # Dead tilde diff --git a/src/protocols/spice/keymaps/de_ch_qwertz.keymap b/src/protocols/spice/keymaps/de_ch_qwertz.keymap new file mode 100644 index 00000000..a4ba4a1c --- /dev/null +++ b/src/protocols/spice/keymaps/de_ch_qwertz.keymap @@ -0,0 +1,67 @@ +# +# 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. +# + +parent "base" +name "de-ch-qwertz" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890'" +map -caps -altgr -shift 0x10..0x1A ~ "qwertzuiopü" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklöä$" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "YXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890'" +map +caps -altgr -shift 0x10..0x1A ~ "QWERTZUIOPÜ" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÖÄ$" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "yxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02..0x04 ~ "¦@#" +map +altgr -shift 0x07..0x09 ~ "¬|¢" +map +altgr -shift 0x1A..0x1B ~ "[]" +map +altgr -shift 0x28 ~ "{" +map +altgr -shift 0x2B ~ "}" +map +altgr -shift 0x56 ~ "\" +map +altgr -shift 0x12 ~ "€" + +# +# Dead keys +# + +map +altgr -shift 0x0C ~ 0xFE51 # Dead acute +map -altgr -shift 0x0D ~ 0xFE52 # Dead circumflex +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map +altgr -shift 0x0D ~ 0xFE53 # Dead tilde +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut diff --git a/src/protocols/spice/keymaps/de_de_qwertz.keymap b/src/protocols/spice/keymaps/de_de_qwertz.keymap new file mode 100644 index 00000000..bf7a0aaf --- /dev/null +++ b/src/protocols/spice/keymaps/de_de_qwertz.keymap @@ -0,0 +1,74 @@ +# +# 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. +# + +parent "base" +name "de-de-qwertz" + +# +# Basic keys +# + +map -caps -altgr -shift 0x02..0x0C ~ "1234567890ß" +map -caps -altgr -shift 0x10..0x1B ~ "qwertzuiopü+" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklöä#" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "YXCVBNM;:_" + +map +caps -altgr -shift 0x02..0x0C ~ "!"§$%&/()=?" +map +caps -altgr -shift 0x10..0x1B ~ "QWERTZUIOPÜ*" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÖÄ'" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "yxcvbnm,._" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x03 ~ "²" +map +altgr -shift 0x04 ~ "³" +map +altgr -shift 0x08 ~ "{" +map +altgr -shift 0x09 ~ "[" +map +altgr -shift 0x0A ~ "]" +map +altgr -shift 0x0B ~ "}" +map +altgr -shift 0x0C ~ "\" + +map +altgr -shift 0x10 ~ "@" +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x1B ~ "~" + +map +altgr -shift 0x56 ~ "|" +map +altgr -shift 0x32 ~ "µ" + +# +# Dead keys +# + +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map -altgr -shift 0x0D ~ 0xFE51 # Dead acute +map -altgr -shift 0x29 ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x0C ~ 0xFE53 # Dead tilde + diff --git a/src/protocols/spice/keymaps/en_gb_qwerty.keymap b/src/protocols/spice/keymaps/en_gb_qwerty.keymap new file mode 100644 index 00000000..c14485b2 --- /dev/null +++ b/src/protocols/spice/keymaps/en_gb_qwerty.keymap @@ -0,0 +1,79 @@ +# +# 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. +# + +parent "base" +name "en-gb-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map -caps -altgr -shift 0x10..0x1B ~ "qwertyuiop[]" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjkl;'#" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "\zxcvbnm,./" + +map -caps -altgr +shift 0x29 0x02..0x0D ~ "¬!"£$%^&*()_+" +map -caps -altgr +shift 0x10..0x1B ~ "QWERTYUIOP{}" +map -caps -altgr +shift 0x1E..0x28 0x2B ~ "ASDFGHJKL:@~" +map -caps -altgr +shift 0x56 0x2C..0x35 ~ "|ZXCVBNM<>?" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map +caps -altgr -shift 0x10..0x1B ~ "QWERTYUIOP[]" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKL;'#" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "\ZXCVBNM,./" + +map +caps -altgr +shift 0x29 0x02..0x0D ~ "¬!"£$%^&*()_+" +map +caps -altgr +shift 0x10..0x1B ~ "qwertyuiop{}" +map +caps -altgr +shift 0x1E..0x28 0x2B ~ "asdfghjkl:@~" +map +caps -altgr +shift 0x56 0x2C..0x35 ~ "|zxcvbnm<>?" + +# +# Keys requiring AltGr (some of which are affected by Caps Lock) +# + +map +altgr -shift 0x29 ~ "¦" +map +altgr -shift 0x05 ~ "€" + +map -caps +altgr -shift 0x12 ~ "é" +map -caps +altgr +shift 0x12 ~ "É" +map -caps +altgr -shift 0x16 ~ "ú" +map -caps +altgr +shift 0x16 ~ "Ú" +map -caps +altgr -shift 0x17 ~ "í" +map -caps +altgr +shift 0x17 ~ "Í" +map -caps +altgr -shift 0x18 ~ "ó" +map -caps +altgr +shift 0x18 ~ "Ó" +map -caps +altgr -shift 0x1E ~ "á" +map -caps +altgr +shift 0x1E ~ "Á" +map -caps +altgr -shift 0x2E ~ "ç" +map -caps +altgr +shift 0x2E ~ "Ç" + +map +caps +altgr +shift 0x12 ~ "é" +map +caps +altgr -shift 0x12 ~ "É" +map +caps +altgr +shift 0x16 ~ "ú" +map +caps +altgr -shift 0x16 ~ "Ú" +map +caps +altgr +shift 0x17 ~ "í" +map +caps +altgr -shift 0x17 ~ "Í" +map +caps +altgr +shift 0x18 ~ "ó" +map +caps +altgr -shift 0x18 ~ "Ó" +map +caps +altgr +shift 0x1E ~ "á" +map +caps +altgr -shift 0x1E ~ "Á" +map +caps +altgr +shift 0x2E ~ "ç" +map +caps +altgr -shift 0x2E ~ "Ç" + diff --git a/src/protocols/spice/keymaps/en_us_qwerty.keymap b/src/protocols/spice/keymaps/en_us_qwerty.keymap new file mode 100644 index 00000000..8af29e31 --- /dev/null +++ b/src/protocols/spice/keymaps/en_us_qwerty.keymap @@ -0,0 +1,42 @@ +# +# 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. +# + +parent "base" +name "en-us-qwerty" + +map -caps -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map -caps -shift 0x10..0x1B 0x2B ~ "qwertyuiop[]\" +map -caps -shift 0x1E..0x28 ~ "asdfghjkl;'" +map -caps -shift 0x2C..0x35 ~ "zxcvbnm,./" + +map -caps +shift 0x29 0x02..0x0D ~ "~!@#$%^&*()_+" +map -caps +shift 0x10..0x1B 0x2B ~ "QWERTYUIOP{}|" +map -caps +shift 0x1E..0x28 ~ "ASDFGHJKL:"" +map -caps +shift 0x2C..0x35 ~ "ZXCVBNM<>?" + +map +caps -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map +caps -shift 0x10..0x1B 0x2B ~ "QWERTYUIOP[]\" +map +caps -shift 0x1E..0x28 ~ "ASDFGHJKL;'" +map +caps -shift 0x2C..0x35 ~ "ZXCVBNM,./" + +map +caps +shift 0x29 0x02..0x0D ~ "~!@#$%^&*()_+" +map +caps +shift 0x10..0x1B 0x2B ~ "qwertyuiop{}|" +map +caps +shift 0x1E..0x28 ~ "asdfghjkl:"" +map +caps +shift 0x2C..0x35 ~ "zxcvbnm<>?" + diff --git a/src/protocols/spice/keymaps/es_es_qwerty.keymap b/src/protocols/spice/keymaps/es_es_qwerty.keymap new file mode 100644 index 00000000..fa00104f --- /dev/null +++ b/src/protocols/spice/keymaps/es_es_qwerty.keymap @@ -0,0 +1,63 @@ +# +# 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. +# + +parent "base" +name "es-es-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "º1234567890'¡" +map -caps -altgr -shift 0x10..0x19 0x1B ~ "qwertyuiop+" +map -caps -altgr -shift 0x1E..0x27 0x2B ~ "asdfghjklñç" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "º1234567890'¡" +map +caps -altgr -shift 0x10..0x19 0x1B ~ "QWERTYUIOP+" +map +caps -altgr -shift 0x1E..0x27 0x2B ~ "ASDFGHJKLÑÇ" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x29 0x02..0x04 0x07 ~ "\|@#¬" +map +altgr -shift 0x12 0x1A 0x1B ~ "€[]" +map +altgr -shift 0x28 0x2B ~ "{}" + +# +# Dead keys +# + +map -altgr -shift 0x1A ~ 0xFE50 # Dead grave +map -altgr -shift 0x28 ~ 0xFE51 # Dead acute +map -altgr +shift 0x1A ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x05 ~ 0xFE53 # Dead tilde +map -altgr +shift 0x28 ~ 0xFE57 # Dead diaeresis (umlaut) diff --git a/src/protocols/spice/keymaps/es_latam_qwerty.keymap b/src/protocols/spice/keymaps/es_latam_qwerty.keymap new file mode 100644 index 00000000..260f94d5 --- /dev/null +++ b/src/protocols/spice/keymaps/es_latam_qwerty.keymap @@ -0,0 +1,63 @@ +# +# 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. +# + +parent "base" +name "es-latam-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "|1234567890'¿" +map -caps -altgr -shift 0x10..0x19 0x1B ~ "qwertyuiop+" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklñ{}" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "|1234567890'¿" +map +caps -altgr -shift 0x10..0x19 0x1B ~ "QWERTYUIOP+" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÑ{}" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x29 0x0C ~ "¬\" +map +altgr -shift 0x10 ~ "€" +map +altgr -shift 0x28 ~ "^" + +# +# Dead keys +# + +map +altgr -shift 0x2B ~ 0xFE50 # Dead grave +map -altgr -shift 0x1A ~ 0xFE51 # Dead acute +map -altgr +shift 0x1A ~ 0xFE57 # Dead diaeresis (umlaut) +map +altgr -shift 0x1B ~ 0xFE53 # Dead tilde + diff --git a/src/protocols/spice/keymaps/failsafe.keymap b/src/protocols/spice/keymaps/failsafe.keymap new file mode 100644 index 00000000..76625ca7 --- /dev/null +++ b/src/protocols/spice/keymaps/failsafe.keymap @@ -0,0 +1,22 @@ +# +# 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. +# + +parent "base" +name "failsafe" + diff --git a/src/protocols/spice/keymaps/fr_be_azerty.keymap b/src/protocols/spice/keymaps/fr_be_azerty.keymap new file mode 100644 index 00000000..0a5b3848 --- /dev/null +++ b/src/protocols/spice/keymaps/fr_be_azerty.keymap @@ -0,0 +1,76 @@ +# +# 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. +# + +parent "base" +name "fr-be-azerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "²&é"'(§è!çà)-" +map -caps -altgr -shift 0x10..0x19 0x1B ~ "azertyuiop$" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "qsdfghjklmùµ" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "WXCVBN?./+" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "²1234567890°_" +map +caps -altgr -shift 0x10..0x19 0x1B ~ "AZERTYUIOP£" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "QSDFGHJKLM%£" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "wxcvbn,;:=" + +# +# Keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed) +# + +map +altgr -shift 0x02..0x04 ~ "|@#" +map +altgr -shift 0x0A..0x0B ~ "{}" +map +altgr -shift 0x1A..0x1B ~ "[]" + +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x56 ~ "\" +map +altgr -shift 0x07 ~ "^" + +# +# Dead keys requiring AltGr (unaffected by Caps Lock or Shift) +# + +map +altgr 0x35 ~ 0xFE53 # Dead tilde +map +altgr 0x28 ~ 0xFE51 # Dead acute +map +altgr 0x2B ~ 0xFE50 # Dead grave + +# +# Dead keys (affected by Caps Lock and Shift) +# + +map -caps -altgr -shift 0x1A ~ 0xFE52 # Dead circumflex +map -caps -altgr +shift 0x1A ~ 0xFE57 # Dead umlaut + +map +caps -altgr -shift 0x1A ~ 0xFE57 # Dead umlaut +map +caps -altgr +shift 0x1A ~ 0xFE52 # Dead circumflex + diff --git a/src/protocols/spice/keymaps/fr_ca_qwerty.keymap b/src/protocols/spice/keymaps/fr_ca_qwerty.keymap new file mode 100644 index 00000000..6f4a912b --- /dev/null +++ b/src/protocols/spice/keymaps/fr_ca_qwerty.keymap @@ -0,0 +1,54 @@ +# +# 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. +# + +parent "base" +name "fr-ca-qwerty" + +# +# Basic keys +# + +map -altgr -shift 0x29 0x02..0x0D ~ "#1234567890-=" +map -altgr -shift 0x10..0x1B 0x2B ~ "qwertyuiop^¸<" +map -altgr -shift 0x1E..0x28 ~ "asdfghjkl;`" +map -altgr -shift 0x2C..0x35 ~ "zxcvbnm,.é" + +map -altgr +shift 0x29 0x02..0x0D ~ "|!"/$%?&*()_+" +map -altgr +shift 0x10..0x1B 0x2B ~ "QWERTYUIOP^¨>" +map -altgr +shift 0x1E..0x28 ~ "ASDFGHJKL:`" +map -altgr +shift 0x2C..0x35 ~ "ZXCVBNM'.É" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x29 0x02..0x0D ~ "\±@£¢¤¬¦²³¼½¾" +map +altgr -shift 0x18..0x1B 0x2B ~ "§¶[]}" +map +altgr -shift 0x27..0x28 ~ "~{" +map +altgr -shift 0x32..0x33 0x35 ~ "µ¯´" + +# +# Combined accents +# + +map -altgr -shift 0x1A ~ 0x0302 # COMBINING CIRCUMFLEX ACCENT +map -altgr -shift 0x1B ~ 0x0327 # COMBINING CEDILLA +map -altgr +shift 0x1B ~ 0x0308 # COMBINING DIAERESIS +map -altgr -shift 0x28 ~ 0x0300 # COMBINING GRAVE ACCENT +map +altgr -shift 0x35 ~ 0x0301 # COMBINING ACUTE ACCENT diff --git a/src/protocols/spice/keymaps/fr_ch_qwertz.keymap b/src/protocols/spice/keymaps/fr_ch_qwertz.keymap new file mode 100644 index 00000000..bd413871 --- /dev/null +++ b/src/protocols/spice/keymaps/fr_ch_qwertz.keymap @@ -0,0 +1,68 @@ +# +# 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. +# + +parent "base" +name "fr-ch-qwertz" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890'" +map -caps -altgr -shift 0x10..0x1A ~ "qwertzuiopè" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjkléà$" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "YXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890'" +map +caps -altgr -shift 0x10..0x1A ~ "QWERTZUIOPè" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLéà$" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "yxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02..0x04 ~ "¦@#" +map +altgr -shift 0x07..0x09 ~ "¬|¢" +map +altgr -shift 0x1A 0x1B ~ "[]" +map +altgr -shift 0x28 0x2B ~ "{}" +map +altgr -shift 0x56 ~ "\" + +map +altgr -shift 0x12 ~ "€" + +# +# Dead keys +# + +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut +map +altgr -shift 0x0C ~ 0xFE51 # Dead acute +map -altgr -shift 0x0D ~ 0xFE52 # Dead circumflex +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map +altgr -shift 0x0D ~ 0xFE53 # Dead tilde + diff --git a/src/protocols/spice/keymaps/fr_fr_azerty.keymap b/src/protocols/spice/keymaps/fr_fr_azerty.keymap new file mode 100644 index 00000000..09101a7f --- /dev/null +++ b/src/protocols/spice/keymaps/fr_fr_azerty.keymap @@ -0,0 +1,65 @@ +# +# 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. +# + +parent "base" +name "fr-fr-azerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "²&é"'(-è_çà)=" +map -caps -altgr -shift 0x10..0x19 0x1B ~ "azertyuiop$" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "qsdfghjklmù*" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "WXCVBN?./§" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "²1234567890°+" +map +caps -altgr -shift 0x10..0x19 0x1B ~ "AZERTYUIOP£" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "QSDFGHJKLM%µ" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "wxcvbn,;:!" + +# +# Keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed) +# + +map +altgr -shift 0x03..0x0D ~ "~#{[|`\^@]}" + +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x1B ~ "¤" + +# +# Dead keys (affected by Caps Lock and Shift) +# + +map -caps -altgr -shift 0x1A ~ 0xFE52 # Dead circumflex +map -caps -altgr +shift 0x1A ~ 0xFE57 # Dead umlaut + +map +caps -altgr -shift 0x1A ~ 0xFE57 # Dead umlaut +map +caps -altgr +shift 0x1A ~ 0xFE52 # Dead circumflex + diff --git a/src/protocols/spice/keymaps/generate.pl b/src/protocols/spice/keymaps/generate.pl new file mode 100755 index 00000000..b6f1fe56 --- /dev/null +++ b/src/protocols/spice/keymaps/generate.pl @@ -0,0 +1,253 @@ +#!/usr/bin/env perl +# +# 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. +# + +# +# generate.pl +# +# Parse .keymap files, producing corresponding .c files that can be included +# into the Spice plugin source. +# + +# We need at least Perl 5.8 for Unicode's sake +use 5.008; + +sub keymap_symbol { + my $name = shift; + $name =~ s/-/_/g; + return 'guac_spice_keymap_' . $name; +} + +# +# _generated_keymaps.c +# + +my @keymaps = (); + +open OUTPUT, ">", "_generated_keymaps.c"; +print OUTPUT + '#include "config.h"' . "\n" + . '#include "keymap.h"' . "\n" + . "\n" + . '#include ' . "\n" + . '#include ' . "\n" + . "\n"; + +for my $filename (@ARGV) { + + my $content = ""; + my $parent = ""; + my $layout_name = ""; + + # Parse file + open INPUT, '<', "$filename"; + binmode INPUT, ":encoding(utf8)"; + while () { + + chomp; + + # Comments + if (m/^\s*#/) {} + + # Name + elsif ((my $name) = m/^\s*name\s+"(.*)"\s*(?:#.*)?$/) { + $layout_name = $name; + } + + # Parent map + elsif ((my $name) = m/^\s*parent\s+"(.*)"\s*(?:#.*)?$/) { + $parent = keymap_symbol($name); + } + + # Map + elsif ((my $range, my $onto) = + m/^\s*map\s+([^~]*)\s+~\s+(".*"|0x[0-9A-Fa-f]+)\s*(?:#.*)?$/) { + + my @keysyms = (); + my @scancodes = (); + + my $ext_flags = 0; + my $set_shift = 0; + my $set_altgr = 0; + my $set_caps = 0; + my $set_num = 0; + + my $clear_shift = 0; + my $clear_altgr = 0; + my $clear_caps = 0; + my $clear_num = 0; + + # Parse ranges and options + foreach $_ (split(/\s+/, $range)) { + + # Set option/modifier + if ((my $opt) = m/^\+([a-z]+)$/) { + if ($opt eq "shift") { $set_shift = 1; } + elsif ($opt eq "altgr") { $set_altgr = 1; } + elsif ($opt eq "caps") { $set_caps = 1; } + elsif ($opt eq "num") { $set_num = 1; } + elsif ($opt eq "ext") { $ext_flags = 1; } + else { + die "$filename: $.: ERROR: " + . "Invalid set option\n"; + } + } + + # Clear option/modifier + elsif ((my $opt) = m/^-([a-z]+)$/) { + if ($opt eq "shift") { $clear_shift = 1; } + elsif ($opt eq "altgr") { $clear_altgr = 1; } + elsif ($opt eq "caps") { $clear_caps = 1; } + elsif ($opt eq "num") { $clear_num = 1; } + else { + die "$filename: $.: ERROR: " + . "Invalid clear option\n"; + } + } + + # Single scancode + elsif ((my $scancode) = m/^(0x[0-9A-Fa-f]+)$/) { + $scancodes[++$#scancodes] = hex($scancode); + } + + # Range of scancodes + elsif ((my $start, my $end) = + m/^(0x[0-9A-Fa-f]+)\.\.(0x[0-9A-Fa-f]+)$/) { + for (my $i=hex($start); $i<=hex($end); $i++) { + $scancodes[++$#scancodes] = $i; + } + } + + # Invalid token + else { + die "$filename: $.: ERROR: " + . "Invalid token\n"; + } + + } + + # Parse onto + if ($onto =~ m/^0x/) { + $keysyms[0] = hex($onto); + } + else { + foreach my $char (split('', + substr($onto, 1, length($onto)-2))) { + my $codepoint = ord($char); + if ($codepoint >= 0x0100) { + $keysyms[++$#keysyms] = 0x01000000 | $codepoint; + } + else { + $keysyms[++$#keysyms] = $codepoint; + } + } + } + + # Check mapping + if ($#keysyms != $#scancodes) { + die "$filename: $.: ERROR: " + . "Keysym and scancode range lengths differ\n"; + } + + # Write keysym/scancode pairs + for (my $i=0; $i<=$#keysyms; $i++) { + + $content .= " {" + . " .keysym = " . $keysyms[$i] . "," + . " .scancode = " . $scancodes[$i]; + + # Modifiers that must be active + $content .= ", .set_modifiers = 0"; + $content .= " | GUAC_SPICE_KEYMAP_MODIFIER_SHIFT" if $set_shift; + $content .= " | GUAC_SPICE_KEYMAP_MODIFIER_ALTGR" if $set_altgr; + + # Modifiers that must be inactive + $content .= ", .clear_modifiers = 0"; + $content .= " | GUAC_SPICE_KEYMAP_MODIFIER_SHIFT" if $clear_shift; + $content .= " | GUAC_SPICE_KEYMAP_MODIFIER_ALTGR" if $clear_altgr; + + # Locks that must be set + $content .= ", .set_locks = 0"; + $content .= " | SPICE_INPUTS_NUM_LOCK" if $set_num; + $content .= " | SPICE_INPUTS_CAPS_LOCK" if $set_caps; + + # Locks that must NOT be set + $content .= ", .clear_locks = 0"; + $content .= " | SPICE_INPUTS_NUM_LOCK" if $clear_num; + $content .= " | SPICE_INPUTS_CAPS_LOCK" if $clear_caps; + + $content .= " },\n"; + + } + + } + + # Invalid token + elsif (m/\S/) { + die "$filename: $.: ERROR: " + . "Invalid token\n"; + } + + } + close INPUT; + + # Header + my $sym = keymap_symbol($layout_name); + print OUTPUT + "\n" + . '/* Autogenerated from ' . $filename . ' */' . "\n" + . 'static guac_spice_keysym_desc __' . $sym . '[] = {' . "\n" + . $content + . ' {0}' . "\n" + . '};' . "\n"; + + # Desc header + print OUTPUT "\n" + . 'static const guac_spice_keymap ' . $sym . ' = { ' . "\n"; + + # Layout name + print OUTPUT " .name = \"$layout_name\",\n"; + + # Parent layout (if any) + if ($parent) { + print OUTPUT " .parent = &$parent,\n"; + } + + # Desc footer + print OUTPUT + ' .mapping = __' . $sym . "\n" + . '};' . "\n"; + + $keymaps[++$#keymaps] = $sym; + print STDERR "Added: $layout_name\n"; + +} + +print OUTPUT "\n" + . 'const guac_spice_keymap* GUAC_SPICE_KEYMAPS[] = {' . "\n"; + +foreach my $keymap (@keymaps) { + print OUTPUT " &$keymap,\n"; +} +print OUTPUT + ' NULL' . "\n" + . '};' . "\n"; + +close OUTPUT; + diff --git a/src/protocols/spice/keymaps/hu_hu_qwertz.keymap b/src/protocols/spice/keymaps/hu_hu_qwertz.keymap new file mode 100644 index 00000000..b559cc42 --- /dev/null +++ b/src/protocols/spice/keymaps/hu_hu_qwertz.keymap @@ -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. +# + +parent "base" +name "hu-hu-qwertz" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "0123456789öüó" +map -caps -altgr -shift 0x10..0x1B ~ "qwertzuıopőú" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjkléáű" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "íyxcvbnm,.-" + +map -caps -altgr +shift 0x29 0x02..0x0D ~ "§'"+!%/=()ÖÜÓ" +map -caps -altgr +shift 0x10..0x1B ~ "QWERTZUIOPŐÚ" +map -caps -altgr +shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÉÁŰ" +map -caps -altgr +shift 0x56 0x2C..0x35 ~ "ÍYXCVBNM?:_" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "0123456789ÖÜÓ" +map +caps -altgr -shift 0x10..0x1B ~ "QWERTZUIOPŐÚ" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÉÁŰ" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "ÍYXCVBNM,.-" + +map +caps -altgr +shift 0x29 0x02..0x0D ~ "§'"+!%/=()öüó" +map +caps -altgr +shift 0x10..0x1B ~ "qwertzuiopőú" +map +caps -altgr +shift 0x1E..0x28 0x2B ~ "asdfghjkléáű" +map +caps -altgr +shift 0x56 0x2C..0x35 ~ "íyxcvbnm?:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02 ~ "~" +map +altgr -shift 0x08 ~ "`" + +map +altgr -shift 0x10 ~ "\" +map +altgr -shift 0x11 ~ "|" +map +altgr -shift 0x12 ~ "Ä" +map +altgr -shift 0x16 ~ "€" +map +altgr -shift 0x17 ~ "Í" +map +altgr -shift 0x1A ~ "÷" +map +altgr -shift 0x1B ~ "×" + +map +altgr -shift 0x1E ~ "ä" +map +altgr -shift 0x1F ~ "đ" +map +altgr -shift 0x20 ~ "Đ" +map +altgr -shift 0x21 ~ "[" +map +altgr -shift 0x22 ~ "]" +map +altgr -shift 0x24 ~ "í" +map +altgr -shift 0x25 ~ "ł" +map +altgr -shift 0x26 ~ "Ł" +map +altgr -shift 0x27 ~ "$" +map +altgr -shift 0x28 ~ "ß" +map +altgr -shift 0x2B ~ "¤" + +map +altgr -shift 0x56 ~ "<" +map +altgr -shift 0x2C ~ ">" +map +altgr -shift 0x2D ~ "#" +map +altgr -shift 0x2E ~ "&" +map +altgr -shift 0x2F ~ "@" +map +altgr -shift 0x30 ~ "{" +map +altgr -shift 0x31 ~ "}" +map +altgr -shift 0x32 ~ "<" +map +altgr -shift 0x33 ~ ";" +map +altgr -shift 0x34 ~ ">" +map +altgr -shift 0x35 ~ "*" + + +# +# Keys requiring AltGr & Shift +# + + +# +# Dead keys +# + +map +altgr -shift 0x03 ~ 0xFE5A # Dead caron +map +altgr -shift 0x04 ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x05 ~ 0xFE55 # Dead breve +map +altgr -shift 0x06 ~ 0xFE58 # Dead abovering +map +altgr -shift 0x07 ~ 0xFE5C # Dead ogonek +map +altgr -shift 0x09 ~ 0xFE56 # Dead abovedot +map +altgr -shift 0x0A ~ 0xFE51 # Dead acute +map +altgr -shift 0x0B ~ 0xFE59 # Dead doubleacute +map +altgr -shift 0x0C ~ 0xFE57 # Dead diaeresis +map +altgr -shift 0x0D ~ 0xFE5B # Dead cedilla + + +# END diff --git a/src/protocols/spice/keymaps/it_it_qwerty.keymap b/src/protocols/spice/keymaps/it_it_qwerty.keymap new file mode 100644 index 00000000..74da0bf9 --- /dev/null +++ b/src/protocols/spice/keymaps/it_it_qwerty.keymap @@ -0,0 +1,59 @@ +# +# 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. +# + +parent "base" +name "it-it-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "\1234567890'ì" +map -caps -altgr -shift 0x10..0x1B ~ "qwertyuiopè+" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklòàù" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "\1234567890'ì" +map +caps -altgr -shift 0x10..0x1B ~ "QWERTYUIOPè+" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLòàù" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x1A ~ "[" +map +altgr -shift 0x1B ~ "]" +map +altgr -shift 0x27 ~ "@" +map +altgr -shift 0x28 ~ "#" + +map +altgr +shift 0x1A ~ "{" +map +altgr +shift 0x1B ~ "}" + diff --git a/src/protocols/spice/keymaps/ja_jp_qwerty.keymap b/src/protocols/spice/keymaps/ja_jp_qwerty.keymap new file mode 100644 index 00000000..dc879fb7 --- /dev/null +++ b/src/protocols/spice/keymaps/ja_jp_qwerty.keymap @@ -0,0 +1,35 @@ +# +# 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. +# + +parent "base" +name "ja-jp-qwerty" + +map -shift 0x02..0x0D 0x7D ~ "1234567890-^\" +map -shift 0x10..0x1B ~ "qwertyuiop@[" +map -shift 0x1E..0x28 0x2B ~ "asdfghjkl;:]" +map -shift 0x2C..0x35 0x73 ~ "zxcvbnm,./\" + +map +shift 0x02..0x0A 0x0C 0x0D 0x7D ~ "!"#$%&'()=~|" +map +shift 0x10..0x1B ~ "QWERTYUIOP`{" +map +shift 0x1E..0x28 0x2B ~ "ASDFGHJKL+*}" +map +shift 0x2C..0x35 0x73 ~ "ZXCVBNM<>?_" + +map -shift 0x29 ~ 0xFF28 +map -shift 0x29 ~ 0xFF2A +map +shift 0x29 ~ 0xFF29 diff --git a/src/protocols/spice/keymaps/no_no_qwerty.keymap b/src/protocols/spice/keymaps/no_no_qwerty.keymap new file mode 100644 index 00000000..0661a58d --- /dev/null +++ b/src/protocols/spice/keymaps/no_no_qwerty.keymap @@ -0,0 +1,75 @@ +# +# 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. +# + +parent "base" +name "no-no-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0C ~ "|1234567890+" +map -caps -altgr -shift 0x10..0x1A ~ "qwertyuiopå" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjkløæ'" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0C ~ "|1234567890+" +map +caps -altgr -shift 0x10..0x1A ~ "QWERTYUIOPÅ" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLØÆ'" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnm;:_" + +map -altgr -shift 0x0D ~ "\" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x03 ~ "@" +map +altgr -shift 0x04 ~ "£" +map +altgr -shift 0x05 ~ "$" +map +altgr -shift 0x08 ~ "{" +map +altgr -shift 0x09 ~ "[" +map +altgr -shift 0x0A ~ "]" +map +altgr -shift 0x0B ~ "}" +map +altgr -shift 0x56 ~ "\" + +map +altgr -shift 0x12 ~ "€" + +map +altgr -shift 0x0D ~ "|" +map +altgr -shift 0x32 ~ "µ" + +# +# Dead keys +# + +map +altgr -shift 0x0D ~ 0xFE51 # Dead acute +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut +map -altgr +shift 0x1B ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x1B ~ 0xFE53 # Dead tilde diff --git a/src/protocols/spice/keymaps/pl_pl_qwerty.keymap b/src/protocols/spice/keymaps/pl_pl_qwerty.keymap new file mode 100644 index 00000000..1072971e --- /dev/null +++ b/src/protocols/spice/keymaps/pl_pl_qwerty.keymap @@ -0,0 +1,63 @@ +# +# 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. +# + +parent "base" +name "pl-pl-qwerty" + +map -caps -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map -caps -shift 0x10..0x1B 0x2B ~ "qwertyuiop[]\" +map -caps -shift 0x1E..0x28 ~ "asdfghjkl;'" +map -caps -shift 0x2C..0x35 ~ "zxcvbnm,./" + +map -caps +shift 0x29 0x02..0x0D ~ "~!@#$%^&*()_+" +map -caps +shift 0x10..0x1B 0x2B ~ "QWERTYUIOP{}|" +map -caps +shift 0x1E..0x28 ~ "ASDFGHJKL:"" +map -caps +shift 0x2C..0x35 ~ "ZXCVBNM<>?" + +map +caps -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map +caps -shift 0x10..0x1B 0x2B ~ "QWERTYUIOP[]\" +map +caps -shift 0x1E..0x28 ~ "ASDFGHJKL;'" +map +caps -shift 0x2C..0x35 ~ "ZXCVBNM,./" + +map +caps +shift 0x29 0x02..0x0D ~ "~!@#$%^&*()_+" +map +caps +shift 0x10..0x1B 0x2B ~ "qwertyuiop{}|" +map +caps +shift 0x1E..0x28 ~ "asdfghjkl:"" +map +caps +shift 0x2C..0x35 ~ "zxcvbnm<>?" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x16 ~ "€" + +map -caps -shift +altgr 0x12 0x18 ~ "ęó" +map -caps -shift +altgr 0x1E 0x1F 0x26 ~ "ąśł" +map -caps -shift +altgr 0x2C 0x2D 0x2E 0x31 ~ "żźćń" + +map -caps +shift +altgr 0x12 0x18 ~ "ĘÓ" +map -caps +shift +altgr 0x1E 0x1F 0x26 ~ "ĄŚŁ" +map -caps +shift +altgr 0x2C 0x2D 0x2E 0x31 ~ "ŻŹĆŃ" + +map +caps -shift +altgr 0x12 0x18 ~ "ĘÓ" +map +caps -shift +altgr 0x1E 0x1F 0x26 ~ "ĄŚŁ" +map +caps -shift +altgr 0x2C 0x2D 0x2E 0x31 ~ "ŻŹĆŃ" + +map +caps +shift +altgr 0x12 0x18 ~ "ęó" +map +caps +shift +altgr 0x1E 0x1F 0x26 ~ "ąśł" +map +caps +shift +altgr 0x2C 0x2D 0x2E 0x31 ~ "żźćń" diff --git a/src/protocols/spice/keymaps/pt_br_qwerty.keymap b/src/protocols/spice/keymaps/pt_br_qwerty.keymap new file mode 100644 index 00000000..b1d0a2ef --- /dev/null +++ b/src/protocols/spice/keymaps/pt_br_qwerty.keymap @@ -0,0 +1,66 @@ +# +# 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. +# + +parent "base" +name "pt-br-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ "'1234567890-=" +map -caps -altgr -shift 0x10..0x19 0x1B ~ "qwertyuiop[" +map -caps -altgr -shift 0x1E..0x27 0x2B ~ "asdfghjklç]" +map -caps -altgr -shift 0x56 0x2C..0x35 0x73 ~ "\zxcvbnm,.;/" + +map -caps -altgr +shift 0x29 0x02..0x06 0x08..0x0D ~ ""!@#$%&*()_+" +map -caps -altgr +shift 0x10..0x19 0x1B ~ "QWERTYUIOP{" +map -caps -altgr +shift 0x1E..0x27 0x2B ~ "ASDFGHJKLÇ}" +map -caps -altgr +shift 0x56 0x2C..0x35 0x73 ~ "|ZXCVBNM<>:?" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ "'1234567890-=" +map +caps -altgr -shift 0x10..0x19 0x1B ~ "QWERTYUIOP[" +map +caps -altgr -shift 0x1E..0x27 0x2B ~ "ASDFGHJKLÇ]" +map +caps -altgr -shift 0x56 0x2C..0x35 0x73 ~ "\ZXCVBNM,.;/" + +map +caps -altgr +shift 0x29 0x02..0x06 0x08..0x0D ~ ""!@#$%&*()_+" +map +caps -altgr +shift 0x10..0x19 0x1B ~ "qwertyuiop{" +map +caps -altgr +shift 0x1E..0x27 0x2B ~ "asdfghjklç}" +map +caps -altgr +shift 0x56 0x2C..0x35 0x73 ~ "|zxcvbnm<>:?" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02..0x07 0x0D ~ "¹²³£¢¬§" +map +altgr -shift 0x10..0x11 ~ "/?" +map +altgr -shift 0x12 ~ "°" +map +altgr -shift 0x1B ~ "ª" +map +altgr -shift 0x2B ~ "º" +map +altgr -shift 0x2E ~ "₢" + +# +# Dead keys +# + +map -altgr +shift 0x07 ~ 0xFE57 # Dead diaeresis (umlaut) +map -altgr +shift 0x1A ~ 0xFE50 # Dead grave +map -altgr -shift 0x1A ~ 0xFE51 # Dead acute +map -altgr +shift 0x28 ~ 0xFE52 # Dead circumflex +map -altgr -shift 0x28 ~ 0xFE53 # Dead tilde \ No newline at end of file diff --git a/src/protocols/spice/keymaps/sv_se_qwerty.keymap b/src/protocols/spice/keymaps/sv_se_qwerty.keymap new file mode 100644 index 00000000..9edfccb3 --- /dev/null +++ b/src/protocols/spice/keymaps/sv_se_qwerty.keymap @@ -0,0 +1,74 @@ +# +# 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. +# + +parent "base" +name "sv-se-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890+" +map -caps -altgr -shift 0x10..0x1A ~ "qwertyuiopå" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklöä'" +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -altgr -shift 0x29 0x02..0x0C ~ "§1234567890+" +map +caps -altgr -shift 0x10..0x1A ~ "QWERTYUIOPÅ" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLÖÄ'" +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnm;:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x03 ~ "@" +map +altgr -shift 0x04 ~ "£" +map +altgr -shift 0x05 ~ "$" +map +altgr -shift 0x08 ~ "{" +map +altgr -shift 0x09 ~ "[" +map +altgr -shift 0x0A ~ "]" +map +altgr -shift 0x0B ~ "}" +map +altgr -shift 0x0C ~ "\" + +map +altgr -shift 0x12 ~ "€" + +map +altgr -shift 0x56 ~ "|" +map +altgr -shift 0x32 ~ "µ" + +# +# Dead keys +# + +map -altgr -shift 0x0D ~ 0xFE51 # Dead acute +map -altgr +shift 0x0D ~ 0xFE50 # Dead grave +map -altgr -shift 0x1B ~ 0xFE57 # Dead umlaut +map -altgr +shift 0x1B ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x1B ~ 0xFE53 # Dead tilde + diff --git a/src/protocols/spice/keymaps/tr_tr_qwerty.keymap b/src/protocols/spice/keymaps/tr_tr_qwerty.keymap new file mode 100644 index 00000000..5df30005 --- /dev/null +++ b/src/protocols/spice/keymaps/tr_tr_qwerty.keymap @@ -0,0 +1,92 @@ +# +# 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. +# + +parent "base" +name "tr-tr-qwerty" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ ""1234567890*-" +map -caps -altgr -shift 0x10..0x1B ~ "qwertyuıopğü" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklşi," +map -caps -altgr -shift 0x56 0x2C..0x35 ~ "ZXCVBNMÖÇ:" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ ""1234567890*-" +map +caps -altgr -shift 0x10..0x1B ~ "QWERTYUIOPĞÜ" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKLŞİ," +map +caps -altgr -shift 0x56 0x2C..0x35 ~ "zxcvbnmöç:" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x29 0x02..0x06 ~ "<>£#$½" +map +altgr -shift 0x08..0x0D ~ "{[]}\|" + +map +altgr -shift 0x10 ~ "@" +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x14 ~ "₺" +map +altgr -shift 0x1F ~ "ß" + +map +altgr -shift 0x56 ~ "|" + + +# +# Keys requiring AltGr and vary by Shift and Caps Lock +# + +map -caps +altgr -shift 0x17 ~ "i" +map -caps +altgr -shift 0x1E ~ "æ" + +map -caps +altgr +shift 0x17 ~ "İ" +map -caps +altgr +shift 0x1E ~ "Æ" + +map +caps +altgr -shift 0x17 ~ "İ" +map +caps +altgr -shift 0x1E ~ "Æ" + +map +caps +altgr +shift 0x1E ~ "æ" +map +caps +altgr +shift 0x17 ~ "i" + +# +# Dead keys +# + +map -altgr +shift 0x04 ~ 0xFE52 # Dead circumflex + +map +altgr -shift 0x1A ~ 0xFE57 # Dead diaeresis (umlaut) +map +altgr -shift 0x1B ~ 0xFE53 # Dead tilde + +map +altgr -shift 0x27 ~ 0xFE51 # Dead acute +map +altgr -shift 0x2B ~ 0xFE50 # Dead grave + +# END diff --git a/src/protocols/spice/log.c b/src/protocols/spice/log.c new file mode 100644 index 00000000..0daf2ab0 --- /dev/null +++ b/src/protocols/spice/log.c @@ -0,0 +1,67 @@ +/* + * 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 "client.h" +#include "common/iconv.h" +#include "common/surface.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +void guac_spice_client_log_info(const char* format, ...) { + + char message[2048]; + + /* Copy log message into buffer */ + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + + /* Log to syslog */ + syslog(LOG_INFO, "%s", message); + +} + +void guac_spice_client_log_error(const char* format, ...) { + + char message[2048]; + + /* Copy log message into buffer */ + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + + /* Log to syslog */ + syslog(LOG_ERR, "%s", message); + +} + diff --git a/src/protocols/spice/log.h b/src/protocols/spice/log.h new file mode 100644 index 00000000..1019d5e2 --- /dev/null +++ b/src/protocols/spice/log.h @@ -0,0 +1,67 @@ +/* + * 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_SPICE_LOG_H +#define GUAC_SPICE_LOG_H + +#include "config.h" + +#include "client.h" +#include "common/iconv.h" +#include "common/surface.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/** + * Callback invoked by Spice when an informational message needs to be + * logged. + * + * @param format + * A printf-style format string to log. + * + * @param ... + * The values to use when filling the conversion specifiers within the + * format string. + */ +void guac_spice_client_log_info(const char* format, ...); + +/** + * Callback invoked by Spice when an error message needs to be logged. + * + * @param format + * A printf-style format string to log. + * + * @param ... + * The values to use when filling the conversion specifiers within the + * format string. + */ +void guac_spice_client_log_error(const char* format, ...); + +#endif /* GUAC_SPICE_LOG_H */ + diff --git a/src/protocols/spice/settings.c b/src/protocols/spice/settings.c new file mode 100644 index 00000000..cdb2e661 --- /dev/null +++ b/src/protocols/spice/settings.c @@ -0,0 +1,675 @@ +/* + * 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 "argv.h" +#include "client.h" +#include "common/defaults.h" +#include "settings.h" +#include "spice-defaults.h" +#include "spice-constants.h" + +#include + +#include +#include +#include +#include +#include + +/* Client plugin arguments */ +const char* GUAC_SPICE_CLIENT_ARGS[] = { + "hostname", + "port", + "tls", + "tls-verify", + "ca", + "ca-file", + "pubkey", + "proxy", + "read-only", + "encodings", + GUAC_SPICE_ARGV_USERNAME, + GUAC_SPICE_ARGV_PASSWORD, + "swap-red-blue", + "color-depth", + "cursor", + "autoretry", + "clipboard-encoding", + + "enable-audio", + "enable-audio-input", + "file-transfer", + "file-directory", + "file-transfer-ro", + "file-transfer-create-folder", + "disable-download", + "disable-upload", + "server-layout", + +#ifdef ENABLE_COMMON_SSH + "enable-sftp", + "sftp-hostname", + "sftp-host-key", + "sftp-port", + "sftp-username", + "sftp-password", + "sftp-private-key", + "sftp-passphrase", + "sftp-directory", + "sftp-root-directory", + "sftp-server-alive-interval", + "sftp-disable-download", + "sftp-disable-upload", +#endif + + "recording-path", + "recording-name", + "recording-exclude-output", + "recording-exclude-mouse", + "recording-include-keys", + "create-recording-path", + "disable-copy", + "disable-paste", + + NULL +}; + +enum SPICE_ARGS_IDX { + + /** + * The hostname of the Spice server to connect to. + */ + IDX_HOSTNAME, + + /** + * The port of the Spice server to connect to. + */ + IDX_PORT, + + /** + * Whether or not the connection to the Spice server should be made via + * TLS. + */ + IDX_TLS, + + /** + * The verification mode that should be used to validate TLS connections + * to the Spice server. + */ + IDX_TLS_VERIFY, + + /** + * One or more Base64-encoded certificates that will be used for TLS + * verification. + */ + IDX_CA, + + /** + * A path to a file containing one or more certificates that will be used + * when validating TLS connections. + */ + IDX_CA_FILE, + + /** + * The public key of the host for TLS verification. + */ + IDX_PUBKEY, + + /** + * The proxy server to connect through when connecting to the Spice server. + */ + IDX_PROXY, + + /** + * "true" if this connection should be read-only (user input should be + * dropped), "false" or blank otherwise. + */ + IDX_READ_ONLY, + + /** + * Space-separated list of encodings to use within the Spice session. If not + * specified, this will be: + * + * "zrle ultra copyrect hextile zlib corre rre raw". + */ + IDX_ENCODINGS, + + /** + * The username to send to the Spice server if authentication is requested. + */ + IDX_USERNAME, + + /** + * The password to send to the Spice server if authentication is requested. + */ + IDX_PASSWORD, + + /** + * "true" if the red and blue components of each color should be swapped, + * "false" or blank otherwise. This is mainly used for Spice servers that do + * not properly handle colors. + */ + IDX_SWAP_RED_BLUE, + + /** + * The color depth to request, in bits. + */ + IDX_COLOR_DEPTH, + + /** + * "remote" if the cursor should be rendered on the server instead of the + * client. All other values will default to local rendering. + */ + IDX_CURSOR, + + /** + * The number of connection attempts to make before giving up. By default, + * this will be 0. + */ + IDX_AUTORETRY, + + /** + * The encoding to use for clipboard data sent to the Spice server if we are + * going to be deviating from the standard (which mandates ISO 8829-1). + * Valid values are "ISO8829-1" (the only legal value with respect to the + * Spice standard), "UTF-8", "UTF-16", and "CP2252". + */ + IDX_CLIPBOARD_ENCODING, + + /** + * "true" if audio should be enabled, "false" or blank otherwise. + */ + IDX_ENABLE_AUDIO, + + /** + * "true" if audio input should be enabled, "false" or blank otherwise. + */ + IDX_ENABLE_AUDIO_INPUT, + + /** + * "true" if file transfer should be enabled, "false" or blank otherwise. + */ + IDX_FILE_TRANSFER, + + /** + * The absolute path to the directory that should be shared from the system + * running guacd to the spice server. + */ + IDX_FILE_DIRECTORY, + + /** + * Whether or not the shared directory should be read-only to the Spice + * server. + */ + IDX_FILE_TRANSFER_RO, + + /** + * Whether or not Guacamole should attempt to create the shared folder + * if it does not already exist. + */ + IDX_FILE_TRANSFER_CREATE_FOLDER, + + /** + * "true" if downloads from the remote server to Guacamole client should + * be disabled, otherwise false or blank. + */ + IDX_DISABLE_DOWNLOAD, + + /** + * "true" if uploads from Guacamole Client to the shared folder should be + * disabled, otherwise false or blank. + */ + IDX_DISABLE_UPLOAD, + + /** + * The name of the keymap chosen as the layout of the server. Legal names + * are defined within the *.keymap files in the "keymaps" directory of the + * source for Guacamole's Spice support. + */ + IDX_SERVER_LAYOUT, + +#ifdef ENABLE_COMMON_SSH + /** + * "true" if SFTP should be enabled for the Spice connection, "false" or + * blank otherwise. + */ + IDX_ENABLE_SFTP, + + /** + * The hostname of the SSH server to connect to for SFTP. If blank, the + * hostname of the Spice server will be used. + */ + IDX_SFTP_HOSTNAME, + + /** + * The public SSH host key to identify the SFTP server. + */ + IDX_SFTP_HOST_KEY, + + /** + * The port of the SSH server to connect to for SFTP. If blank, the default + * SSH port of "22" will be used. + */ + IDX_SFTP_PORT, + + /** + * The username to provide when authenticating with the SSH server for + * SFTP. + */ + IDX_SFTP_USERNAME, + + /** + * The password to provide when authenticating with the SSH server for + * SFTP (if not using a private key). + */ + IDX_SFTP_PASSWORD, + + /** + * The base64-encoded private key to use when authenticating with the SSH + * server for SFTP (if not using a password). + */ + IDX_SFTP_PRIVATE_KEY, + + /** + * The passphrase to use to decrypt the provided base64-encoded private + * key. + */ + IDX_SFTP_PASSPHRASE, + + /** + * The default location for file uploads within the SSH server. This will + * apply only to uploads which do not use the filesystem guac_object (where + * the destination directory is otherwise ambiguous). + */ + IDX_SFTP_DIRECTORY, + + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. If omitted, "/" will be used by default. + */ + IDX_SFTP_ROOT_DIRECTORY, + + /** + * The interval at which SSH keepalive messages are sent to the server for + * SFTP connections. The default is 0 (disabling keepalives), and a value + * of 1 is automatically incremented to 2 by libssh2 to avoid busy loop corner + * cases. + */ + IDX_SFTP_SERVER_ALIVE_INTERVAL, + + /** + * If set to "true", file downloads over SFTP will be blocked. If set to + * "false" or not set, file downloads will be allowed. + */ + IDX_SFTP_DISABLE_DOWNLOAD, + + /** + * If set to "true", file uploads over SFTP will be blocked. If set to + * "false" or not set, file uploads will be allowed. + */ + IDX_SFTP_DISABLE_UPLOAD, +#endif + + /** + * The full absolute path to the directory in which screen recordings + * should be written. + */ + IDX_RECORDING_PATH, + + /** + * The name that should be given to screen recordings which are written in + * the given path. + */ + IDX_RECORDING_NAME, + + /** + * Whether output which is broadcast to each connected client (graphics, + * streams, etc.) should NOT be included in the session recording. Output + * is included by default, as it is necessary for any recording which must + * later be viewable as video. + */ + IDX_RECORDING_EXCLUDE_OUTPUT, + + /** + * Whether changes to mouse state, such as position and buttons pressed or + * released, should NOT be included in the session recording. Mouse state + * is included by default, as it is necessary for the mouse cursor to be + * rendered in any resulting video. + */ + IDX_RECORDING_EXCLUDE_MOUSE, + + /** + * Whether keys pressed and released should be included in the session + * recording. Key events are NOT included by default within the recording, + * as doing so has privacy and security implications. Including key events + * may be necessary in certain auditing contexts, but should only be done + * with caution. Key events can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + IDX_RECORDING_INCLUDE_KEYS, + + /** + * Whether the specified screen recording path should automatically be + * created if it does not yet exist. + */ + IDX_CREATE_RECORDING_PATH, + + /** + * Whether outbound clipboard access should be blocked. If set to "true", + * it will not be possible to copy data from the remote desktop to the + * client using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_COPY, + + /** + * Whether inbound clipboard access should be blocked. If set to "true", it + * will not be possible to paste data from the client to the remote desktop + * using the clipboard. By default, clipboard access is not blocked. + */ + IDX_DISABLE_PASTE, + + SPICE_ARGS_COUNT +}; + +guac_spice_settings* guac_spice_parse_args(guac_user* user, + int argc, const char** argv) { + + /* Validate arg count */ + if (argc != SPICE_ARGS_COUNT) { + guac_user_log(user, GUAC_LOG_WARNING, "Incorrect number of connection " + "parameters provided: expected %i, got %i.", + SPICE_ARGS_COUNT, argc); + return NULL; + } + + guac_spice_settings* settings = calloc(1, sizeof(guac_spice_settings)); + + settings->hostname = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_HOSTNAME, SPICE_DEFAULT_HOST); + + settings->port = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_PORT, SPICE_DEFAULT_PORT); + + settings->tls = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_TLS, false); + + char* verify_mode = guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_TLS_VERIFY, NULL); + + if (verify_mode != NULL) { + if (strcmp(verify_mode, GUAC_SPICE_PARAMETER_TLS_VERIFY_PUBKEY) == 0) + settings->tls_verify = SPICE_SESSION_VERIFY_PUBKEY; + else if (strcmp(verify_mode, GUAC_SPICE_PARAMETER_TLS_VERIFY_SUBJECT) == 0) + settings->tls_verify = SPICE_SESSION_VERIFY_SUBJECT; + } + + else { + settings->tls_verify = SPICE_SESSION_VERIFY_HOSTNAME; + } + + free(verify_mode); + + settings->ca = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_CA, NULL); + + settings->ca_file = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_CA_FILE, NULL); + + settings->pubkey = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_PUBKEY, NULL); + + settings->proxy = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_PROXY, NULL); + + settings->username = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_USERNAME, NULL); + + settings->password = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_PASSWORD, NULL); + + /* Read-only mode */ + settings->read_only = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_READ_ONLY, false); + + /* Parse color depth */ + settings->color_depth = + guac_user_parse_args_int(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_COLOR_DEPTH, 0); + + /* Set encodings if specified */ + settings->encodings = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_ENCODINGS, + SPICE_DEFAULT_ENCODINGS); + + /* Parse autoretry */ + settings->retries = + guac_user_parse_args_int(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_AUTORETRY, 0); + + /* Audio enable/disable */ + settings->audio_enabled = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_ENABLE_AUDIO, false); + + /* Audio input enable/disable */ + settings->audio_input_enabled = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_ENABLE_AUDIO_INPUT, false); + + /* File transfer enable/disable */ + settings->file_transfer = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_FILE_TRANSFER, false); + + /* The directory on the guacd server to share */ + settings->file_directory = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_FILE_DIRECTORY, NULL); + + /* Whether or not the share should be read-only. */ + settings->file_transfer_ro = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_FILE_TRANSFER_RO, false); + + /* Whether or not Guacamole should attempt to create a non-existent folder. */ + settings->file_transfer_create_folder = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_FILE_TRANSFER_CREATE_FOLDER, false); + + /* Whether or not downloads (Server -> Client) should be disabled. */ + settings->disable_download = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_DISABLE_DOWNLOAD, false); + + /* Whether or not uploads (Client -> Server) should be disabled. */ + settings->disable_upload = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_DISABLE_UPLOAD, false); + + /* Pick keymap based on argument */ + settings->server_layout = NULL; + if (argv[IDX_SERVER_LAYOUT][0] != '\0') + settings->server_layout = + guac_spice_keymap_find(argv[IDX_SERVER_LAYOUT]); + + /* If no keymap requested, use default */ + if (settings->server_layout == NULL) + settings->server_layout = guac_spice_keymap_find(GUAC_SPICE_DEFAULT_KEYMAP); + + /* Set clipboard encoding if specified */ + settings->clipboard_encoding = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_CLIPBOARD_ENCODING, NULL); + +#ifdef ENABLE_COMMON_SSH + /* SFTP enable/disable */ + settings->enable_sftp = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_ENABLE_SFTP, false); + + /* Hostname for SFTP connection */ + settings->sftp_hostname = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_HOSTNAME, settings->hostname); + + /* The public SSH host key. */ + settings->sftp_host_key = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_HOST_KEY, NULL); + + /* Port for SFTP connection */ + settings->sftp_port = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_PORT, SPICE_DEFAULT_SFTP_PORT); + + /* Username for SSH/SFTP authentication */ + settings->sftp_username = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_USERNAME, ""); + + /* Password for SFTP (if not using private key) */ + settings->sftp_password = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_PASSWORD, ""); + + /* Private key for SFTP (if not using password) */ + settings->sftp_private_key = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_PRIVATE_KEY, NULL); + + /* Passphrase for decrypting the SFTP private key (if applicable */ + settings->sftp_passphrase = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_PASSPHRASE, ""); + + /* Default upload directory */ + settings->sftp_directory = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_DIRECTORY, NULL); + + /* SFTP root directory */ + settings->sftp_root_directory = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_ROOT_DIRECTORY, SPICE_DEFAULT_SFTP_ROOT); + + /* Default keepalive value */ + settings->sftp_server_alive_interval = + guac_user_parse_args_int(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_SERVER_ALIVE_INTERVAL, 0); + + settings->sftp_disable_download = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_DISABLE_DOWNLOAD, false); + + settings->sftp_disable_upload = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_SFTP_DISABLE_UPLOAD, false); +#endif + + /* Read recording path */ + settings->recording_path = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_RECORDING_PATH, NULL); + + /* Read recording name */ + settings->recording_name = + guac_user_parse_args_string(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_RECORDING_NAME, GUAC_SPICE_DEFAULT_RECORDING_NAME); + + /* Parse output exclusion flag */ + settings->recording_exclude_output = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_OUTPUT, false); + + /* Parse mouse exclusion flag */ + settings->recording_exclude_mouse = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_RECORDING_EXCLUDE_MOUSE, false); + + /* Parse key event inclusion flag */ + settings->recording_include_keys = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_RECORDING_INCLUDE_KEYS, false); + + /* Parse path creation flag */ + settings->create_recording_path = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_CREATE_RECORDING_PATH, false); + + /* Parse clipboard copy disable flag */ + settings->disable_copy = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_DISABLE_COPY, false); + + /* Parse clipboard paste disable flag */ + settings->disable_paste = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_DISABLE_PASTE, false); + + return settings; + +} + +void guac_spice_settings_free(guac_spice_settings* settings) { + + /* Free settings strings */ + free(settings->clipboard_encoding); + free(settings->encodings); + free(settings->hostname); + free(settings->password); + free(settings->recording_name); + free(settings->recording_path); + free(settings->username); + +#ifdef ENABLE_SPICE_REPEATER + /* Free Spice repeater settings */ + free(settings->dest_host); +#endif + +#ifdef ENABLE_COMMON_SSH + /* Free SFTP settings */ + free(settings->sftp_directory); + free(settings->sftp_root_directory); + free(settings->sftp_host_key); + free(settings->sftp_hostname); + free(settings->sftp_passphrase); + free(settings->sftp_password); + free(settings->sftp_port); + free(settings->sftp_private_key); + free(settings->sftp_username); +#endif + + /* Free settings structure */ + free(settings); + +} + diff --git a/src/protocols/spice/settings.h b/src/protocols/spice/settings.h new file mode 100644 index 00000000..f71ffc9b --- /dev/null +++ b/src/protocols/spice/settings.h @@ -0,0 +1,345 @@ +/* + * 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_SPICE_SETTINGS_H +#define GUAC_SPICE_SETTINGS_H + +#include "config.h" +#include "keymap.h" + +#include + +#include + +/** + * The filename to use for the screen recording, if not specified. + */ +#define GUAC_SPICE_DEFAULT_RECORDING_NAME "recording" + +/** + * Spice-specific client data. + */ +typedef struct guac_spice_settings { + + /** + * The hostname of the Spice server (or repeater) to connect to. + */ + char* hostname; + + /** + * The port of the Spice server (or repeater) to connect to. + */ + char* port; + + /** + * Whether or not TLS should be used to connect to the SPICE server. + */ + bool tls; + + /** + * The type of TLS validation that should be done for encrypted connections + * to Spice servers. + */ + SpiceSessionVerify tls_verify; + + /** + * One or more Base64-encoded certificates to use to validate TLS + * connections to the Spice server. + */ + char* ca; + + /** + * A path to a file containing one more certificates that will be used to + * validate TLS connections. + */ + char* ca_file; + + /** + * The public key of the Spice server for TLS verification. + */ + char* pubkey; + + /** + * Spice supports connecting to remote servers via a proxy server. You can + * specify the proxy server to use in this property. + */ + char* proxy; + + /** + * The username given in the arguments. + */ + char* username; + + /** + * The password given in the arguments. + */ + char* password; + + /** + * Space-separated list of encodings to use within the Spice session. + */ + char* encodings; + + /** + * The color depth to request, in bits. + */ + int color_depth; + + /** + * Whether this connection is read-only, and user input should be dropped. + */ + bool read_only; + + /** + * Whether audio is enabled. + */ + bool audio_enabled; + + /** + * Whether audio input is enabled. + */ + bool audio_input_enabled; + + /** + * If file transfer capability should be enabled. + */ + bool file_transfer; + + /** + * The directory on the server where guacd is running that should be + * shared. + */ + char* file_directory; + + /** + * If file transfer capability should be limited to read-only. + */ + bool file_transfer_ro; + + /** + * If the folder does not exist and this setting is set to True, guacd + * will attempt to create the folder. + */ + bool file_transfer_create_folder; + + /** + * True if downloads (Remote Server -> Guacamole Client) should be + * disabled. + */ + bool disable_download; + + /** + * True if uploads (Guacamole Client -> Remote Server) should be disabled. + */ + bool disable_upload; + + /** + * The keymap chosen as the layout of the server. + */ + const guac_spice_keymap* server_layout; + + /** + * The number of connection attempts to make before giving up. + */ + int retries; + + /** + * The encoding to use for clipboard data sent to the Spice server, or NULL + * to use the encoding required by the Spice standard. + */ + char* clipboard_encoding; + + /** + * Whether outbound clipboard access should be blocked. If set, it will not + * be possible to copy data from the remote desktop to the client using the + * clipboard. + */ + bool disable_copy; + + /** + * Whether inbound clipboard access should be blocked. If set, it will not + * be possible to paste data from the client to the remote desktop using + * the clipboard. + */ + bool disable_paste; + +#ifdef ENABLE_COMMON_SSH + /** + * Whether SFTP should be enabled for the Spice connection. + */ + bool enable_sftp; + + /** + * The hostname of the SSH server to connect to for SFTP. + */ + char* sftp_hostname; + + /** + * The public SSH host key. + */ + char* sftp_host_key; + + /** + * The port of the SSH server to connect to for SFTP. + */ + char* sftp_port; + + /** + * The username to provide when authenticating with the SSH server for + * SFTP. + */ + char* sftp_username; + + /** + * The password to provide when authenticating with the SSH server for + * SFTP (if not using a private key). + */ + char* sftp_password; + + /** + * The base64-encoded private key to use when authenticating with the SSH + * server for SFTP (if not using a password). + */ + char* sftp_private_key; + + /** + * The passphrase to use to decrypt the provided base64-encoded private + * key. + */ + char* sftp_passphrase; + + /** + * The default location for file uploads within the SSH server. This will + * apply only to uploads which do not use the filesystem guac_object (where + * the destination directory is otherwise ambiguous). + */ + char* sftp_directory; + + /** + * The path of the directory within the SSH server to expose as a + * filesystem guac_object. + */ + char* sftp_root_directory; + + /** + * The interval at which SSH keepalive messages are sent to the server for + * SFTP connections. The default is 0 (disabling keepalives), and a value + * of 1 is automatically increased to 2 by libssh2 to avoid busy loop corner + * cases. + */ + int sftp_server_alive_interval; + + /** + * Whether file downloads over SFTP should be blocked. If set to "true", + * the local client will not be able to download files from the SFTP server. + * If set to "false" or not set, file downloads will be allowed. + */ + bool sftp_disable_download; + + /** + * Whether file uploads over SFTP should be blocked. If set to "true", the + * local client will not be able to upload files to the SFTP server. If set + * to "false" or not set, file uploads will be allowed. + */ + bool sftp_disable_upload; +#endif + + /** + * The path in which the screen recording should be saved, if enabled. If + * no screen recording should be saved, this will be NULL. + */ + char* recording_path; + + /** + * The filename to use for the screen recording, if enabled. + */ + char* recording_name; + + /** + * Whether the screen recording path should be automatically created if it + * does not already exist. + */ + bool create_recording_path; + + /** + * Whether output which is broadcast to each connected client (graphics, + * streams, etc.) should NOT be included in the session recording. Output + * is included by default, as it is necessary for any recording which must + * later be viewable as video. + */ + bool recording_exclude_output; + + /** + * Whether changes to mouse state, such as position and buttons pressed or + * released, should NOT be included in the session recording. Mouse state + * is included by default, as it is necessary for the mouse cursor to be + * rendered in any resulting video. + */ + bool recording_exclude_mouse; + + /** + * Whether keys pressed and released should be included in the session + * recording. Key events are NOT included by default within the recording, + * as doing so has privacy and security implications. Including key events + * may be necessary in certain auditing contexts, but should only be done + * with caution. Key events can easily contain sensitive information, such + * as passwords, credit card numbers, etc. + */ + bool recording_include_keys; + +} guac_spice_settings; + +/** + * Parses all given args, storing them in a newly-allocated settings object. If + * the args fail to parse, NULL is returned. + * + * @param user + * The user who submitted the given arguments while joining the + * connection. + * + * @param argc + * The number of arguments within the argv array. + * + * @param argv + * The values of all arguments provided by the user. + * + * @return + * A newly-allocated settings object which must be freed with + * guac_spice_settings_free() when no longer needed. If the arguments fail + * to parse, NULL is returned. + */ +guac_spice_settings* guac_spice_parse_args(guac_user* user, + int argc, const char** argv); + +/** + * Frees the given guac_spice_settings object, having been previously allocated + * via guac_spice_parse_args(). + * + * @param settings + * The settings object to free. + */ +void guac_spice_settings_free(guac_spice_settings* settings); + +/** + * NULL-terminated array of accepted client args. + */ +extern const char* GUAC_SPICE_CLIENT_ARGS[]; + +#endif /* SPICE_SETTINGS_H */ diff --git a/src/protocols/spice/sftp.c b/src/protocols/spice/sftp.c new file mode 100644 index 00000000..bdf68a41 --- /dev/null +++ b/src/protocols/spice/sftp.c @@ -0,0 +1,42 @@ +/* + * 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 "common-ssh/sftp.h" +#include "sftp.h" +#include "spice.h" + +#include +#include +#include + +int guac_spice_sftp_file_handler(guac_user* user, guac_stream* stream, + char* mimetype, char* filename) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_common_ssh_sftp_filesystem* filesystem = spice_client->sftp_filesystem; + + /* Handle file upload */ + return guac_common_ssh_sftp_handle_file_stream(filesystem, user, stream, + mimetype, filename); + +} + diff --git a/src/protocols/spice/sftp.h b/src/protocols/spice/sftp.h new file mode 100644 index 00000000..d09145e6 --- /dev/null +++ b/src/protocols/spice/sftp.h @@ -0,0 +1,34 @@ +/* + * 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_SPICE_SFTP_H +#define GUAC_SPICE_SFTP_H + +#include "config.h" + +#include + +/** + * Handles an incoming stream from a Guacamole "file" instruction, saving the + * contents of that stream to the file having the given name. + */ +guac_user_file_handler guac_spice_sftp_file_handler; + +#endif /* SPICE_SFTP_H */ + diff --git a/src/protocols/spice/spice-constants.h b/src/protocols/spice/spice-constants.h new file mode 100644 index 00000000..23778c85 --- /dev/null +++ b/src/protocols/spice/spice-constants.h @@ -0,0 +1,445 @@ +/* + * 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 SPICE_CONSTANTS_H +#define SPICE_CONSTANTS_H + +/** + * The key used to store and retrieve Guacamole-related data from within the + * Spice client structure. + */ +#define GUAC_SPICE_CLIENT_KEY "GUAC_SPICE" + +/** + * The default identifier of the pimary/main display. + */ +#define GUAC_SPICE_DEFAULT_DISPLAY_ID 0 + +/** + * Error code returned when no more file IDs can be allocated. + */ +#define GUAC_SPICE_FOLDER_ENFILE -1 + +/** + * Error code returned when no such file exists. + */ +#define GUAC_SPICE_FOLDER_ENOENT -2 + +/** + * Error code returned when the operation required a directory + * but the file was not a directory. + */ +#define GUAC_SPICE_FOLDER_ENOTDIR -3 + +/** + * Error code returned when insufficient space exists to complete + * the operation. + */ +#define GUAC_SPICE_FOLDER_ENOSPC -4 + +/** + * Error code returned when the operation requires a normal file but + * a directory was given. + */ +#define GUAC_SPICE_FOLDER_EISDIR -5 + +/** + * Error code returned when permission is denied. + */ +#define GUAC_SPICE_FOLDER_EACCES -6 + +/** + * Error code returned when the operation cannot be completed because the + * file already exists. + */ +#define GUAC_SPICE_FOLDER_EEXIST -7 + +/** + * Error code returned when invalid parameters were given. + */ +#define GUAC_SPICE_FOLDER_EINVAL -8 + +/** + * Error code returned when the operation is not implemented. + */ +#define GUAC_SPICE_FOLDER_ENOSYS -9 + +/** + * Error code returned when the operation is not supported. + */ +#define GUAC_SPICE_FOLDER_ENOTSUP -10 + +/** + * The maximum number of events that can be monitored at a given time for + * the Spice shared folder Download folder monitor. + */ +#define GUAC_SPICE_FOLDER_MAX_EVENTS 256 + +/** + * The maximum length of a path in a shared folder. + */ +#define GUAC_SPICE_FOLDER_MAX_PATH 4096 + +/** + * The maximum number of open files in a shared folder. + */ +#define GUAC_SPICE_FOLDER_MAX_FILES 128 + +/** + * The maximum level of folder deptch in a shared folder. + */ +#define GUAC_SPICE_FOLDER_MAX_PATH_DEPTH 64 + +/** + * The TLS verification value from Guacamole Client that indicates that hostname + * verification should be done. + */ +#define GUAC_SPICE_PARAMETER_TLS_VERIFY_HOSTNAME "hostname" + +/** + * The TLS verification value from Guacamole Client that indicates that public + * key verification should be performed. + */ +#define GUAC_SPICE_PARAMETER_TLS_VERIFY_PUBKEY "pubkey" + +/** + * The TLS verification value from Guacamole Client that indicates that subject + * verification should be performed. + */ +#define GUAC_SPICE_PARAMETER_TLS_VERIFY_SUBJECT "subject" + +/** + * The property within a Spice client channel that indicates if the SPICE + * agent is connected. + */ +#define SPICE_PROPERTY_AGENT_CONNECTED "agent-connected" + +/** + * The Spice client property that defines CA certificates used to validate + * the TLS connection to the Spice server. + */ +#define SPICE_PROPERTY_CA "ca" + +/** + * The Spice client property that defines a path on the server running guacd + * to the file containing the certificate authority certificates to use to + * validate the TLS connection to the Spice server. + */ +#define SPICE_PROPERTY_CA_FILE "ca-file" + +/** + * The property that the Spice client uses to set the image cache size. If + * undefined a default of 0 will be used. + */ +#define SPICE_PROPERTY_CACHE_SIZE "cache-size" + +/** + * The Spice client channel property that stores the identifier of the channel. + */ +#define SPICE_PROPERTY_CHANNEL_ID "channel-id" + +/** + * THe Spice client channel property that stores the type of the channel. + */ +#define SPICE_PROPERTY_CHANNEL_TYPE "channel-type" + +/** + * Spice library property that determines whether or not the sockets are provided + * by the client. + */ +#define SPICE_PROPERTY_CLIENT_SOCKETS "client-sockets" + +/** + * The property that tells the Spice client the color depth to use when + * allocating new displays. + */ +#define SPICE_PROPERTY_COLOR_DEPTH "color-depth" + +/** + * The property that tells the Spice client to enable audio playback and + * recording. The SPICE client default is TRUE. + */ +#define SPICE_PROPERTY_ENABLE_AUDIO "enable-audio" + +/** + * Property that enables or disables USB redirection. + */ +#define SPICE_PROPERTY_ENABLE_USBREDIR "enable-usbredir" + +/** + * The property that contains the hostname, IP address, or URL of the Spice + * server that the client should attempt to connect to. + */ +#define SPICE_PROPERTY_HOST "host" + +/** + * A read-only property exposed by the Spice client library indicating the + * current state of key modifiers - such as lock keys - on the server. + */ +#define SPICE_PROPERTY_KEY_MODIFIERS "key-modifiers" + +/** + * The property that indicates the minimum latency for audio playback. + */ +#define SPICE_PROPERTY_MIN_LATENCY "min-latency" + +/** + * The property used to toggle the playback and/or record + * mute status on the Spice server. + */ +#define SPICE_PROPERTY_MUTE "mute" + +/** + * The property used to get or set the number of audio playback and/or recording + * channels that will be available between the Spice server and client. + */ +#define SPICE_PROPERTY_NUM_CHANNELS "nchannels" + +/** + * The property used to tell the Spice client the password to send on to the + * SPICE server for authentication. + */ +#define SPICE_PROPERTY_PASSWORD "password" + +/** + * The property used to set the unencrypted communication port for communicating + * with the Spice server. + */ +#define SPICE_PROPERTY_PORT "port" + +/** + * The property that the Spice client uses to set the proxy server that is used + * to connect to the Spice server. + */ +#define SPICE_PROPERTY_PROXY "proxy" + +/** + * The property used by the Spice client to tell the server that the session + * should be read-only. + */ +#define SPICE_PROPERTY_READ_ONLY "read-only" + +/** + * The property that the Spice client uses to determine a local (to guacd) + * directory that will be shared with the Spice server. + */ +#define SPICE_PROPERTY_SHARED_DIR "shared-dir" + +/** + * The property that tells the Spice client that the shared directory should be + * read-only to the Spice server and should not allow writes. + */ +#define SPICE_PROPERTY_SHARED_DIR_RO "share-dir-ro" + +/** + * The property within the Spice client that is used to set the port used for + * secure, TLS-based communication with the Spice server. + */ +#define SPICE_PROPERTY_TLS_PORT "tls-port" + +/** + * The property that is used to set the username that the Spice client will use + * to authenticate with the server. + */ +#define SPICE_PROPERTY_USERNAME "username" + +/** + * The property that tells the Spiec client whether or not to verify the + * certificate presented by the Spice server in TLS communications. + */ +#define SPICE_PROPERTY_VERIFY "verify" + +/** + * The property used to get or set the playback and/or recording volume of audio + * on the Spice server to the remote client. + */ +#define SPICE_PROPERTY_VOLUME "volume" + +/** + * The signal sent by the Spice client when a new channel is created. + */ +#define SPICE_SIGNAL_CHANNEL_NEW "channel-new" + +/** + * The signal sent by the Spice client when a channel is destroyed. + */ +#define SPICE_SIGNAL_CHANNEL_DESTROY "channel-destroy" + +/** + * The signal sent by the Spice client when an event occurs on a channel. + */ +#define SPICE_SIGNAL_CHANNEL_EVENT "channel-event" + +/** + * A signal that indicates that the cursor should be hidden from the display + * area. + */ +#define SPICE_SIGNAL_CURSOR_HIDE "cursor-hide" + +/** + * A signal that indicates a change in position of the cursor in the display + * area. + */ +#define SPICE_SIGNAL_CURSOR_MOVE "cursor-move" + +/** + * A signal that indicates the cursor should be reset to its default context. + */ +#define SPICE_SIGNAL_CURSOR_RESET "cursor-reset" + +/** + * A signal sent to modify cursor aspect and position within the display area. + */ +#define SPICE_SIGNAL_CURSOR_SET "cursor-set" + +/** + * The signal sent by the Spice client when the client is disconnected from + * the server. + */ +#define SPICE_SIGNAL_DISCONNECTED "disconnected" + +/** + * The signal sent to indicate that a region of the display should be updated. + */ +#define SPICE_SIGNAL_DISPLAY_INVALIDATE "display-invalidate" + +/** + * The signal that indicates that a display is ready to be exposed to the client. + */ +#define SPICE_SIGNAL_DISPLAY_MARK "display-mark" + +/** + * The signal indicating when the primary display data/buffer is ready. + */ +#define SPICE_SIGNAL_DISPLAY_PRIMARY_CREATE "display-primary-create" + +/** + * The signal indicating when the primary display surface should be freed and + * not made available anymore. + */ +#define SPICE_SIGNAL_DISPLAY_PRIMARY_DESTROY "display-primary-destroy" + +/** + * The signal indicating that a rectangular region of he display is updated + * and should be redrawn. + */ +#define SPICE_SIGNAL_GL_DRAW "gl-draw" + +/** + * The signal sent to indicate that the keyboard modifiers - such as lock keys - + * have changed and should be updated. + */ +#define SPICE_SIGNAL_INPUTS_MODIFIERS "inputs-modifiers" + +/** + * The signal sent by the Spice client when the connected status or capabilities + * of a channel change. + */ +#define SPICE_SIGNAL_MAIN_AGENT_UPDATE "main-agent-update" + +/** + * Signal fired by the Spice client when clipboard selection data is available. + */ +#define SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION "main-clipboard-selection" + +/** + * A signal fired by the Spice client when clipboard selection data is available + * from the guest, and of what type. + */ +#define SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION_GRAB "main-clipboard-selection-grab" + +/** + * A signal fired by the Spice client when clipboard selection data is no longer + * available from the guest. + */ +#define SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION_RELEASE "main-clipboard-selection-release" + +/** + * A signal used to request clipboard data from the client. + */ +#define SPICE_SIGNAL_MAIN_CLIPBOARD_SELECTION_REQUEST "main-clipboard-selection-request" + +/** + * A signal used to indicate that the mouse mode has changed. + */ +#define SPICE_SIGNAL_MAIN_MOUSE_UPDATE "main-mouse-update" + +/** + * A signal sent by the Spice client when the server has indicated that live + * migration has started. + */ +#define SPICE_SIGNAL_MIGRATION_STARTED "migration-started" + +/** + * The signal sent by the Spice client when a MM time discontinuity is + * detected. + */ +#define SPICE_SIGNAL_MM_TIME_RESET "mm-time-reset" + +/** + * The signal fired by the Spice client when a new file transfer task has been + * initiated. + */ +#define SPICE_SIGNAL_NEW_FILE_TRANSFER "new-file-transfer" + +/** + * The signal fired when data is available to be played on the client, which + * contains a pointer to the data to be played. + */ +#define SPICE_SIGNAL_PLAYBACK_DATA "playback-data" + +/** + * A signal sent when the server is requesting the current audio playback delay. + */ +#define SPICE_SIGNAL_PLAYBACK_GET_DELAY "playback-get-delay" + +/** + * A signal sent when the server is notifying the client that audio playback + * should begin, which also contains characteristics of that audio data. + */ +#define SPICE_SIGNAL_PLAYBACK_START "playback-start" + +/** + * A signal sent when audio playback should cease. + */ +#define SPICE_SIGNAL_PLAYBACK_STOP "playback-stop" + +/** + * A signal indicating that the Spice server would like to capture audio data + * from the client, along with the required format of that data. + */ +#define SPICE_SIGNAL_RECORD_START "record-start" + +/** + * A signal indicating that audio capture should cease. + */ +#define SPICE_SIGNAL_RECORD_STOP "record-stop" + +/** + * A signal indicating that a share folder is available. + */ +#define SPICE_SIGNAL_SHARE_FOLDER "notify::share-folder" + +/** + * The signal indicating that the Spice server has gone to streaming mode. + */ +#define SPICE_SIGNAL_STREAMING_MODE "streaming-mode" + +#endif /* SPICE_CONSTANTS_H */ + diff --git a/src/protocols/spice/spice-defaults.h b/src/protocols/spice/spice-defaults.h new file mode 100644 index 00000000..210878b5 --- /dev/null +++ b/src/protocols/spice/spice-defaults.h @@ -0,0 +1,49 @@ +/* + * 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 SPICE_DEFAULTS_H +#define SPICE_DEFAULTS_H + +/** + * The default hostname to connect to if none is specified. + */ +#define SPICE_DEFAULT_HOST "localhost" + +/** + * The default Spice port number to connect to if none is specified. + */ +#define SPICE_DEFAULT_PORT "5900" + +/** + * The default encodings to use for the Spice clipboard. + */ +#define SPICE_DEFAULT_ENCODINGS "zrle ultra copyrect hextile zlib corre rre raw" + +/** + * The default SFTP port to connect to if SFTP is enabled. + */ +#define SPICE_DEFAULT_SFTP_PORT "22" + +/** + * The default root directory to limit SFTP access to. + */ +#define SPICE_DEFAULT_SFTP_ROOT "/" + +#endif /* SPICE_DEFAULTS_H */ + diff --git a/src/protocols/spice/spice.c b/src/protocols/spice/spice.c new file mode 100644 index 00000000..4dc190eb --- /dev/null +++ b/src/protocols/spice/spice.c @@ -0,0 +1,189 @@ +/* + * 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 "auth.h" +#include "client.h" +#include "common/cursor.h" +#include "common/display.h" +#include "channels/audio.h" +#include "channels/cursor.h" +#include "channels/display.h" +#include "channels/file.h" +#include "log.h" +#include "settings.h" +#include "spice.h" +#include "spice-constants.h" + +#ifdef ENABLE_COMMON_SSH +#include "common-ssh/sftp.h" +#include "common-ssh/ssh.h" +#include "sftp.h" +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +SpiceSession* guac_spice_get_session(guac_client* client) { + + guac_client_log(client, GUAC_LOG_DEBUG, "Initializing new SPICE session."); + + /* Set up the Spice session and Guacamole client. */ + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_settings* spice_settings = spice_client->settings; + + /* Create a new Spice client. */ + SpiceSession* spice_session = spice_session_new(); + + /* Register a callback for handling new channel events. */ + g_signal_connect(spice_session, SPICE_SIGNAL_CHANNEL_NEW, + G_CALLBACK(guac_spice_client_channel_handler), client); + + /* Set hostname and port */ + g_object_set(spice_session, SPICE_PROPERTY_HOST, spice_settings->hostname, NULL); + guac_client_log(client, GUAC_LOG_DEBUG, "Connecting to host %s", + spice_settings->hostname); + if (spice_settings->tls) { + guac_client_log(client, GUAC_LOG_DEBUG, "Using TLS mode on port %s", + spice_settings->port); + g_object_set(spice_session, + SPICE_PROPERTY_TLS_PORT, spice_settings->port, + SPICE_PROPERTY_VERIFY, spice_settings->tls_verify, + NULL); + if (spice_settings->ca != NULL) + g_object_set(spice_session, SPICE_PROPERTY_CA, spice_settings->ca, NULL); + if (spice_settings->ca_file != NULL) + g_object_set(spice_session, SPICE_PROPERTY_CA_FILE, spice_settings->ca_file, NULL); + } + else { + guac_client_log(client, GUAC_LOG_DEBUG, "Using plaintext mode on port %s", + spice_settings->port); + g_object_set(spice_session, + SPICE_PROPERTY_PORT, spice_settings->port, NULL); + } + + guac_client_log(client, GUAC_LOG_DEBUG, "Setting up keyboard layout: %s", + spice_settings->server_layout); + + /* Load keymap into client */ + spice_client->keyboard = guac_spice_keyboard_alloc(client, + spice_settings->server_layout); + + /* If file transfer is enabled, set up the required properties. */ + if (spice_settings->file_transfer) { + guac_client_log(client, GUAC_LOG_DEBUG, "File transfer enabled, configuring Spice client."); + g_object_set(spice_session, SPICE_PROPERTY_SHARED_DIR, spice_settings->file_directory, NULL); + g_object_set(spice_session, SPICE_PROPERTY_SHARED_DIR_RO, spice_settings->file_transfer_ro, NULL); + spice_client->shared_folder = guac_spice_folder_alloc(client, + spice_settings->file_directory, + spice_settings->file_transfer_create_folder, + spice_settings->disable_download, + spice_settings->disable_upload + ); + guac_client_for_owner(client, guac_spice_folder_expose, + spice_client->shared_folder); + } + + else + g_object_set(spice_session, SPICE_PROPERTY_SHARED_DIR, NULL, NULL); + + /* Return the configured session. */ + return spice_session; + +} + +void* guac_spice_client_thread(void* data) { + + guac_client* client = (guac_client*) data; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_spice_settings* settings = spice_client->settings; + spice_client->spice_mainloop = g_main_loop_new(NULL, false); + + /* Attempt connection */ + guac_client_log(client, GUAC_LOG_DEBUG, "Attempting initial connection to SPICE server."); + spice_client->spice_session = guac_spice_get_session(client); + int retries_remaining = settings->retries; + + /* If unsuccessful, retry as many times as specified */ + while (spice_client->spice_session == NULL && retries_remaining > 0) { + + guac_client_log(client, GUAC_LOG_INFO, + "Connect failed. Waiting %ims before retrying...", + GUAC_SPICE_CONNECT_INTERVAL); + + /* Wait for given interval then retry */ + guac_timestamp_msleep(GUAC_SPICE_CONNECT_INTERVAL); + spice_client->spice_session = guac_spice_get_session(client); + retries_remaining--; + + } + + /* If the final connect attempt fails, return error */ + if (spice_client->spice_session == NULL) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, + "Unable to connect to SPICE server."); + return NULL; + } + + guac_socket_flush(client->socket); + + guac_client_log(client, GUAC_LOG_DEBUG, "Connection configuration finished, calling spice_session_connect."); + + if(!spice_session_connect(spice_client->spice_session)) + return NULL; + + guac_client_log(client, GUAC_LOG_DEBUG, "Session connected, entering main loop."); + + /* Handle messages from SPICE server while client is running */ + while (client->state == GUAC_CLIENT_RUNNING) { + + /* Run the main loop. */ + g_main_loop_run(spice_client->spice_mainloop); + guac_client_log(client, GUAC_LOG_DEBUG, "Finished main loop."); + + /* Wait for an error on the main channel. */ + if (spice_client->main_channel != NULL + && spice_channel_get_error(SPICE_CHANNEL(spice_client->main_channel)) != NULL) + break; + } + + guac_client_log(client, GUAC_LOG_DEBUG, "Exited main loop, cleaning up."); + + /* Kill client and finish connection */ + if (spice_client->spice_session != NULL) { + guac_client_log(client, GUAC_LOG_DEBUG, "Cleaning up SPICE session."); + spice_session_disconnect(spice_client->spice_session); + g_object_unref(spice_client->spice_session); + spice_client->spice_session = NULL; + } + + guac_client_stop(client); + guac_client_log(client, GUAC_LOG_INFO, "Internal SPICE client disconnected"); + return NULL; + +} diff --git a/src/protocols/spice/spice.h b/src/protocols/spice/spice.h new file mode 100644 index 00000000..63067749 --- /dev/null +++ b/src/protocols/spice/spice.h @@ -0,0 +1,206 @@ +/* + * 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_SPICE_H +#define GUAC_SPICE_H + +#include "config.h" + +#include "channels/file.h" +#include "common/clipboard.h" +#include "common/display.h" +#include "common/iconv.h" +#include "common/surface.h" +#include "keyboard.h" +#include "settings.h" + +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_COMMON_SSH +#include "common-ssh/sftp.h" +#include "common-ssh/ssh.h" +#include "common-ssh/user.h" +#endif + +#include + +/** + * Spice-specific client data. + */ +typedef struct guac_spice_client { + + /** + * The Spice client thread. + */ + pthread_t client_thread; + + /** + * The underlying Spice session. + */ + SpiceSession* spice_session; + + /** + * The main Spice channel. + */ + SpiceMainChannel* main_channel; + + /** + * The Spice audio playback channel. + */ + SpicePlaybackChannel* playback_channel; + + /** + * The Spice audio recording/input channel. + */ + SpiceRecordChannel* record_channel; + + /** + * The Spice channel that handles the cursor display and events. + */ + SpiceCursorChannel* cursor_channel; + + /** + * The Spice channel that handles mouse and keyboard inputs. + */ + SpiceInputsChannel* inputs_channel; + + /** + * Client settings, parsed from args. + */ + guac_spice_settings* settings; + + /** + * The current display state. + */ + guac_common_display* display; + + /** + * The Spice display channel. + */ + SpiceDisplayChannel* spice_display; + + /** + * The current state of the keyboard with respect to the RDP session. + */ + guac_spice_keyboard* keyboard; + + /** + * The glib main loop + */ + GMainLoop* spice_mainloop; + + /** + * Internal clipboard. + */ + guac_common_clipboard* clipboard; + + /** + * Shared folder. + */ + guac_spice_folder* shared_folder; + +#ifdef ENABLE_COMMON_SSH + /** + * The user and credentials used to authenticate for SFTP. + */ + guac_common_ssh_user* sftp_user; + + /** + * The SSH session used for SFTP. + */ + guac_common_ssh_session* sftp_session; + + /** + * An SFTP-based filesystem. + */ + guac_common_ssh_sftp_filesystem* sftp_filesystem; +#endif + + /** + * The in-progress session recording, or NULL if no recording is in + * progress. + */ + guac_recording* recording; + + /** + * Common attributes for locks. + */ + pthread_mutexattr_t attributes; + + /** + * Lock which is used to synchronizes access to Spice data structures + * between user input and client threads. It prevents input handlers + * from running when Spice data structures are allocated or freed + * by the client thread. + */ + pthread_rwlock_t lock; + + /** + * Lock which synchronizes the sending of each Spice message, ensuring + * attempts to send Spice messages never overlap. + */ + pthread_mutex_t message_lock; + + /** + * Audio output stream, if any. + */ + guac_audio_stream* audio_playback; + + /** + * Audio input stream, if any. + */ + guac_stream* audio_input; + +} guac_spice_client; + +/** + * Allocates a new Spice client session given the parameters stored within the + * client, returning NULL on failure. + * + * @param client + * The guac_client associated with the settings of the desired Spice + * connection. + * + * @return + * A new Spice session instance allocated and connected according to the + * parameters stored within the given client, or NULL if connecting to the + * Spice server fails. + */ +SpiceSession* guac_spice_get_session(guac_client* client); + +/** + * Spice client thread. This thread initiates the Spice connection and + * ultimately runs throughout the duration of the client, existing as a single + * instance, shared by all users. + * + * @param data + * The guac_client instance associated with the requested Spice connection. + * + * @return + * Always NULL. + */ +void* guac_spice_client_thread(void* data); + +#endif /* GUAC_SPICE_H */ + diff --git a/src/protocols/spice/user.c b/src/protocols/spice/user.c new file mode 100644 index 00000000..e5819002 --- /dev/null +++ b/src/protocols/spice/user.c @@ -0,0 +1,129 @@ +/* + * 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 "channels/audio.h" +#include "channels/clipboard.h" +#include "input.h" +#include "common/display.h" +#include "common/dot_cursor.h" +#include "common/pointer_cursor.h" +#include "user.h" +#include "sftp.h" +#include "spice.h" + +#include +#include +#include +#include +#include + +#include + +int guac_spice_user_join_handler(guac_user* user, int argc, char** argv) { + + guac_spice_client* spice_client = (guac_spice_client*) user->client->data; + + /* Parse provided arguments */ + guac_spice_settings* settings = guac_spice_parse_args(user, + argc, (const char**) argv); + + /* Fail if settings cannot be parsed */ + if (settings == NULL) { + guac_user_log(user, GUAC_LOG_INFO, + "Badly formatted client arguments."); + return 1; + } + + /* Store settings at user level */ + user->data = settings; + + /* Connect via Spice if owner */ + if (user->owner) { + + /* Store owner's settings at client level */ + spice_client->settings = settings; + + /* Start client thread */ + if (pthread_create(&spice_client->client_thread, NULL, guac_spice_client_thread, user->client)) { + guac_user_log(user, GUAC_LOG_ERROR, "Unable to start SPICE client thread."); + return 1; + } + + /* Handle inbound audio streams if audio input is enabled */ + if (settings->audio_input_enabled) + user->audio_handler = guac_spice_client_audio_record_handler; + + } + + /* If not owner, synchronize with current state */ + else { + + /* Synchronize with current display */ + guac_common_display_dup(spice_client->display, user, user->socket); + guac_socket_flush(user->socket); + + } + + /* Only handle events if not read-only */ + if (!settings->read_only) { + + /* General mouse/keyboard events */ + user->mouse_handler = guac_spice_user_mouse_handler; + user->key_handler = guac_spice_user_key_handler; + + /* Inbound (client to server) clipboard transfer */ + if (!settings->disable_paste) + user->clipboard_handler = guac_spice_clipboard_handler; + + /* Updates to connection parameters if we own the connection */ + if (user->owner) + user->argv_handler = guac_argv_handler; + +#ifdef ENABLE_COMMON_SSH + /* Set generic (non-filesystem) file upload handler */ + if (settings->enable_sftp && !settings->sftp_disable_upload) + user->file_handler = guac_spice_sftp_file_handler; +#endif + + } + + return 0; + +} + +int guac_spice_user_leave_handler(guac_user* user) { + + guac_spice_client* spice_client = (guac_spice_client*) user->client->data; + + if (spice_client->display) { + /* Update shared cursor state */ + guac_common_cursor_remove_user(spice_client->display->cursor, user); + } + + /* Free settings if not owner (owner settings will be freed with client) */ + if (!user->owner) { + guac_spice_settings* settings = (guac_spice_settings*) user->data; + guac_spice_settings_free(settings); + } + + return 0; +} + diff --git a/src/protocols/spice/user.h b/src/protocols/spice/user.h new file mode 100644 index 00000000..2b97496d --- /dev/null +++ b/src/protocols/spice/user.h @@ -0,0 +1,38 @@ +/* + * 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_SPICE_USER_H +#define GUAC_SPICE_USER_H + +#include "config.h" + +#include + +/** + * Handler for joining users. + */ +guac_user_join_handler guac_spice_user_join_handler; + +/** + * Handler for leaving users. + */ +guac_user_leave_handler guac_spice_user_leave_handler; + +#endif /* SPICE_USER_H */ +