/* * 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. An 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)); }