diff options
-rw-r--r-- | Makefile.am | 2 | ||||
-rw-r--r-- | src/vdagent/webdav-cb.c | 292 | ||||
-rw-r--r-- | src/vdagent/webdav-cb.h | 38 | ||||
-rw-r--r-- | src/vdagent/x11-priv.h | 8 | ||||
-rw-r--r-- | src/vdagent/x11.c | 86 |
5 files changed, 425 insertions, 1 deletions
diff --git a/Makefile.am b/Makefile.am index 431e414..575ba52 100644 --- a/Makefile.am +++ b/Makefile.am @@ -41,6 +41,8 @@ src_spice_vdagent_SOURCES = \ src/vdagent/audio.h \ src/vdagent/clipboard.c \ src/vdagent/clipboard.h \ + src/vdagent/webdav-cb.c \ + src/vdagent/webdav-cb.h \ src/vdagent/device-info.c \ src/vdagent/device-info.h \ src/vdagent/display.c \ diff --git a/src/vdagent/webdav-cb.c b/src/vdagent/webdav-cb.c new file mode 100644 index 0000000..3cf6286 --- /dev/null +++ b/src/vdagent/webdav-cb.c @@ -0,0 +1,292 @@ +/* webdav-cb.c - common code for x11 and GTK+ backend handling webdav copy&paste + + Copyright 2020 Red Hat, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <syslog.h> + +#include "webdav-cb.h" + +/* FIXME: + * From my testing, gvfs-dav with avahi doesn't seem stable enough, + * so let's simply use the usual port 9843 when mounting the shared folder for now. + * + * This is a bit unfortunate because the port can be customized with the -p option, + * while the service name "Spice client folder" is hardcoded. + * + * Issues with gvfs-dav and avahi: + * - https://bugzilla.redhat.com/show_bug.cgi?id=1843035 + * - similar to https://bugzilla.redhat.com/show_bug.cgi?id=1773219 + * - https://gitlab.gnome.org/GNOME/gvfs/-/issues/498 + * - hence the %2520 in CLIPBOARD_WEBDAV_URI below + * - fixed recently + * - https://gitlab.gnome.org/GNOME/gvfs/-/issues/449 + */ +// #define CLIPBOARD_WEBDAV_URI "dav+sd://Spice%2520client%2520folder._webdav._tcp.local" +#define CLIPBOARD_WEBDAV_URI "dav://localhost:9843" + +typedef enum clipboard_action { + CLIPBOARD_ACTION_COPY, + CLIPBOARD_ACTION_CUT, +} clipboard_action; + +static GMount *webdav_mount; +static GVolumeMonitor *monitor; +static GCancellable *cancellable; + +static gchar *clipboard_data_to_uris(const gchar *target, const gchar *mount_uri, + const gchar *data, gsize size, GError **err) +{ + if (!data || size < 1) { + /* this is valid input */ + return NULL; + } + if (data[size-1]) { + g_set_error_literal(err, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT, + "received list of uris that is not null-terminated"); + return NULL; + } + + clipboard_action action; + if (!g_strcmp0(data, "copy")) { + action = CLIPBOARD_ACTION_COPY; + } else if (!g_strcmp0(data, "cut")) { + action = CLIPBOARD_ACTION_CUT; + } else { + g_set_error_literal(err, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "first line of uri list must specify clipboard action"); + return NULL; + } + /* skip the action line, since we're only interested in + * the actual uris from this point forward */ + gsize action_len = strlen(data) + 1; + data += action_len; + size -= action_len; + + if (size < 1) { + return NULL; + } + + GString *str = g_string_new(NULL); + const gchar *delimiter = "\n"; + gboolean end_with_delimiter = FALSE; + + /* TODO: add support for more file managers + * (and then update clipboard_format_tmpl in x11-priv.h) */ + if (!g_strcmp0(target, "text/uri-list")) { + delimiter = "\r\n"; + if (action == CLIPBOARD_ACTION_CUT) { + syslog(LOG_WARNING, "cutting is not supported with 'text/uri-list' target"); + } + } else if (!g_strcmp0(target, "text/plain;charset=utf-8")) { + /* Nautilus uses text clipboard since + * https://gitlab.gnome.org/GNOME/nautilus/commit/1f77023b5769c773dd9261e5294c0738bf6a3115 */ + end_with_delimiter = TRUE; + g_string_append(str, "x-special/nautilus-clipboard\n"); + g_string_append (str, action == CLIPBOARD_ACTION_CUT ? "cut\n" : "copy\n"); + } else if (!g_strcmp0(target, "application/x-kde-cutselection")) { + /* KDE Dolphin handles text/uri-list just fine, + * but this atom is needed to distinguish between copy and move */ + g_string_append(str, action == CLIPBOARD_ACTION_CUT ? "1" : "0"); + return g_string_free(str, FALSE); + } else if (!g_strcmp0(target, "x-special/gnome-copied-files") || + !g_strcmp0(target, "x-special/mate-copied-files")) { + /* Nautilus moved away from this approach, + * but there's a bunch of other file managers that do use it, such as: + * Nemo (Cinnamon), Thunar (Xfce), Deepin File Manager (Deepin), Xfe; Caja (Mate) */ + g_string_append(str, action == CLIPBOARD_ACTION_CUT ? "cut\n" : "copy\n"); + } else { + g_set_error(err, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "conversion to uri target %s is not supported", target); + return g_string_free(str, TRUE); + } + + for (const gchar *item = data; item < data + size; item += strlen(item) + 1) { + gchar *escaped = g_uri_escape_string(item, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE); + gchar *uri = g_build_filename(mount_uri, escaped, NULL); + g_string_append(str, uri); + g_string_append(str, delimiter); + g_free(escaped); + g_free(uri); + } + + if (!end_with_delimiter) { + g_string_truncate(str, str->len - strlen(delimiter)); + } + + return g_string_free(str, FALSE); +} + +static gchar *clipboard_webdav_mount_get_uri(GMount *mount, GError **err) +{ + GFile *root; + gchar *path, *uri; + + root = g_mount_get_root(mount); + path = g_file_get_path(root); + + if (path) { + /* gvfs-fuse is running, so we get path as follows: + * "/run/user/<UID>/gvfs/dav+sd:host=SpiceClipboard._webdav._tcp.local" + * but we still need to convert it to uri */ + uri = g_filename_to_uri(path, NULL, err); + g_free(path); + } else { + /* gvfs-fuse is not running, let's return the CLIPBOARD_WEBDAV_URI ("dav+sd://..."), + * so that at least gio apps can access the shared files */ + uri = g_file_get_uri(root); + syslog(LOG_WARNING, "gvfs-fuse doesn't seem to be running, " + "file copy functionality may be limited"); + } + g_object_unref(root); + + return uri; +} + +static void resolve_task(GTask *task, const gchar *target, GBytes *data) +{ + GError *err = NULL; + gchar *mount_uri, *uris; + + mount_uri = clipboard_webdav_mount_get_uri(webdav_mount, &err); + if (!mount_uri) { + g_task_return_error(task, err); + g_object_unref(task); + return; + } + + uris = clipboard_data_to_uris(target, mount_uri, + g_bytes_get_data(data, NULL), g_bytes_get_size(data), &err); + g_free(mount_uri); + if (err) { + g_task_return_error(task, err); + g_object_unref(task); + return; + } + + g_task_return_pointer(task, uris, g_free); + g_object_unref(task); +} + +static void unmounted_cb(GMount *mount, gpointer user_data) +{ + syslog(LOG_DEBUG, "%s unmounted", CLIPBOARD_WEBDAV_URI); + g_clear_object(&webdav_mount); +} + +static void mount_found_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + GTask *task = user_data; + GError *err = NULL; + + webdav_mount = g_file_find_enclosing_mount_finish(G_FILE(source), res, &err); + + if (err) { + g_task_return_error(task, err); + g_object_unref(task); + } + syslog(LOG_DEBUG, "mount %s found", CLIPBOARD_WEBDAV_URI); + + g_signal_connect(webdav_mount, "unmounted", G_CALLBACK(unmounted_cb), NULL); + + gchar *target = g_object_get_data(G_OBJECT(task), "target"); + GBytes *data = g_task_get_task_data(task); + resolve_task(task, target, data); +} + +static void mounted_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + GFile *f = G_FILE(source); + GTask *task = user_data; + GError *err = NULL; + + g_file_mount_enclosing_volume_finish(f, res, &err); + + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_ALREADY_MOUNTED)) { + g_clear_error(&err); + } else if (err) { + g_task_return_error(task, err); + g_object_unref(task); + return; + } + syslog(LOG_DEBUG, "%s mounted successfully", CLIPBOARD_WEBDAV_URI); + + g_file_find_enclosing_mount_async(f, G_PRIORITY_DEFAULT, cancellable, mount_found_cb, task); +} + +static void clipboard_webdav_mount_async(GTask *task) +{ + g_return_if_fail(webdav_mount == NULL); + + syslog(LOG_DEBUG, "mounting %s", CLIPBOARD_WEBDAV_URI); + + GFile *f = g_file_new_for_uri(CLIPBOARD_WEBDAV_URI); + g_file_mount_enclosing_volume(f, + G_MOUNT_MOUNT_NONE, + NULL, /* GMountOperation */ + cancellable, + mounted_cb, + task); + g_object_unref(f); +} + +gchar *clipboard_data_translate_to_uris_finish(GObject *source, + GAsyncResult *res, gsize *size, GError **err) +{ + *size = 0; + g_return_val_if_fail(g_task_is_valid(res, source), NULL); + + gchar *uris = g_task_propagate_pointer(G_TASK(res), err); + if (uris) { + *size = strlen(uris); + } + return uris; +} + +void clipboard_data_translate_to_uris_async(const gchar *target, GBytes *data, + GCancellable *cancel, GAsyncReadyCallback callback, gpointer user_data) +{ + GTask *task = g_task_new(NULL, cancel, callback, user_data); + + if (!webdav_mount) { + g_task_set_task_data(task, g_bytes_ref(data), (GDestroyNotify)g_bytes_unref); + g_object_set_data_full(G_OBJECT(task), "target", g_strdup(target), g_free); + clipboard_webdav_mount_async(task); + return; + } + + resolve_task(task, target, data); +} + +void clipboard_webdav_init() +{ + /* we listen to the "unmounted" signal, + * but without the GVolumeMonitor, the signal is not emitted, + * although the docs don't seem to mention it! + * https://gitlab.gnome.org/GNOME/gvfs/-/issues/494 */ + + monitor = g_volume_monitor_get(); + webdav_mount = NULL; + cancellable = g_cancellable_new(); +} + +void clipboard_webdav_finalize() +{ + g_cancellable_cancel(cancellable); + g_clear_object(&cancellable); + g_clear_object(&webdav_mount); + g_clear_object(&monitor); +} diff --git a/src/vdagent/webdav-cb.h b/src/vdagent/webdav-cb.h new file mode 100644 index 0000000..e6e4bed --- /dev/null +++ b/src/vdagent/webdav-cb.h @@ -0,0 +1,38 @@ +/* webdav-cb.h + + Copyright 2020 Red Hat, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <gio/gio.h> + +void clipboard_webdav_init(); +void clipboard_webdav_finalize(); + +/* converts the @data received from spice-gtk to the given @target; + * supported targets are: + * - "text/uri-list" + * - "text/plain;charset=utf-8" + * - "application/x-kde-cutselection" + * - "x-special/gnome-copied-files" + * - "x-special/mate-copied-files" + */ +void clipboard_data_translate_to_uris_async(const gchar *target, GBytes *data, + GCancellable *cancel, GAsyncReadyCallback callback, gpointer user_data); + +gchar *clipboard_data_translate_to_uris_finish(GObject *source, + GAsyncResult *res, gsize *size, GError **err); diff --git a/src/vdagent/x11-priv.h b/src/vdagent/x11-priv.h index 5fa367b..f6f7efe 100644 --- a/src/vdagent/x11-priv.h +++ b/src/vdagent/x11-priv.h @@ -9,6 +9,9 @@ #include <X11/extensions/Xrandr.h> #ifndef USE_GTK_FOR_CLIPBOARD + +#include "webdav-cb.h" + /* Macros to print a message to the logfile prefixed by the selection */ #define SELPRINTF(format, ...) \ syslog(LOG_ERR, "%s: " format, \ @@ -63,6 +66,9 @@ static const struct clipboard_format_tmpl clipboard_format_templates[] = { "image/x-MS-bmp", "image/x-win-bitmap", NULL }, }, { VD_AGENT_CLIPBOARD_IMAGE_TIFF, { "image/tiff", NULL }, }, { VD_AGENT_CLIPBOARD_IMAGE_JPG, { "image/jpeg", NULL }, }, + { VD_AGENT_CLIPBOARD_FILE_LIST, { "text/uri-list", + "text/plain;charset=utf-8", "application/x-kde-cutselection", + "x-special/gnome-copied-files", "x-special/mate-copied-files", NULL } }, }; #define clipboard_format_count (sizeof(clipboard_format_templates)/sizeof(clipboard_format_templates[0])) @@ -103,6 +109,7 @@ struct vdagent_x11 { int clipboard_owner[256]; int clipboard_type_count[256]; uint32_t clipboard_agent_types[256][256]; + Bool clipboard_has_files[256]; Atom clipboard_x11_targets[256][256]; /* Data for conversion_req which is currently being processed */ struct vdagent_x11_conversion_request *conversion_req; @@ -115,6 +122,7 @@ struct vdagent_x11 { uint8_t *selection_req_data; uint32_t selection_req_data_pos; uint32_t selection_req_data_size; + GBytes *file_list_data[256]; Atom selection_req_atom; #endif Window root_window[MAX_SCREENS]; diff --git a/src/vdagent/x11.c b/src/vdagent/x11.c index fe13c76..58b99b7 100644 --- a/src/vdagent/x11.c +++ b/src/vdagent/x11.c @@ -74,6 +74,7 @@ static void vdagent_x11_send_selection_notify(struct vdagent_x11 *x11, Atom prop, struct vdagent_x11_selection_request *request); static void vdagent_x11_set_clipboard_owner(struct vdagent_x11 *x11, uint8_t selection, int new_owner); +static void uris_ready_cb(GObject *source, GAsyncResult *res, gpointer user_data); static const char *vdagent_x11_sel_to_str(uint8_t selection) { switch (selection) { @@ -287,6 +288,8 @@ struct vdagent_x11 *vdagent_x11_create(UdscsConnection *vdagentd, /* Be a good X11 citizen and maximize the amount of data we send at once */ if (x11->max_prop_size > 262144) x11->max_prop_size = 262144; + + clipboard_webdav_init(); #endif for (i = 0; i < x11->screen_count; i++) { @@ -326,6 +329,8 @@ void vdagent_x11_destroy(struct vdagent_x11 *x11, int vdagentd_disconnected) XFree(x11->atom_name_cache[i].name); } } + + clipboard_webdav_finalize(); #endif g_hash_table_destroy(x11->guest_output_map); @@ -421,6 +426,9 @@ static void vdagent_x11_set_clipboard_owner(struct vdagent_x11 *x11, } } + x11->clipboard_has_files[selection] = False; + g_clear_pointer(&x11->file_list_data[selection], g_bytes_unref); + if (new_owner == owner_none) { /* When going from owner_guest to owner_none we need to send a clipboard release message to the client */ @@ -799,6 +807,17 @@ static uint32_t vdagent_x11_target_to_type(struct vdagent_x11 *x11, int i, j; for (i = 0; i < clipboard_format_count; i++) { + /* targets for VD_AGENT_CLIPBOARD_FILE_LIST overlap with the text targets */ + if (x11->clipboard_has_files[selection]) { + if (x11->clipboard_formats[i].type == VD_AGENT_CLIPBOARD_UTF8_TEXT) { + continue; + } + } else { + if (x11->clipboard_formats[i].type == VD_AGENT_CLIPBOARD_FILE_LIST) { + continue; + } + } + for (j = 0; j < x11->clipboard_formats[i].atom_count; j++) { if (x11->clipboard_formats[i].atoms[j] == target) { return x11->clipboard_formats[i].type; @@ -968,6 +987,11 @@ static void vdagent_x11_handle_targets_notify(struct vdagent_x11 *x11, type_count = &x11->clipboard_type_count[selection]; *type_count = 0; for (i = 0; i < clipboard_format_count; i++) { + if (x11->clipboard_formats[i].type == VD_AGENT_CLIPBOARD_FILE_LIST) { + /* we don't support file copying in this direction yet */ + continue; + } + atom = atom_lists_overlap(x11->clipboard_formats[i].atoms, atoms, x11->clipboard_formats[i].atom_count, len); if (atom) { @@ -1029,6 +1053,11 @@ static void vdagent_x11_send_targets(struct vdagent_x11 *x11, int i, j, k, target_count = 1; for (i = 0; i < x11->clipboard_type_count[selection]; i++) { + if (x11->clipboard_agent_types[selection][i] == VD_AGENT_CLIPBOARD_UTF8_TEXT && + x11->clipboard_has_files[selection]) { + continue; + } + for (j = 0; j < clipboard_format_count; j++) { if (x11->clipboard_formats[j].type != x11->clipboard_agent_types[selection][i]) @@ -1115,6 +1144,17 @@ static void vdagent_x11_handle_selection_request(struct vdagent_x11 *x11) return; } + if (type == VD_AGENT_CLIPBOARD_FILE_LIST && x11->file_list_data[selection]) { + VSELPRINTF("setting file list from cache"); + + clipboard_data_translate_to_uris_async( + vdagent_x11_get_atom_name(x11, event->xselectionrequest.target), + x11->file_list_data[selection], + NULL, uris_ready_cb, x11 + ); + return; + } + udscs_write(x11->vdagentd, VDAGENTD_CLIPBOARD_REQUEST, selection, type, NULL, 0); } @@ -1249,6 +1289,13 @@ void vdagent_x11_clipboard_grab(struct vdagent_x11 *x11, uint8_t selection, x11->selection_window, CurrentTime); vdagent_x11_set_clipboard_owner(x11, selection, owner_client); + for (int i = 0; i < x11->clipboard_type_count[selection]; i++) { + if (x11->clipboard_agent_types[selection][i] == VD_AGENT_CLIPBOARD_FILE_LIST) { + x11->clipboard_has_files[selection] = True; + break; + } + } + /* If there're pending requests for targets, ignore the returned * targets as the XSetSelectionOwner() call above made them invalid */ x11->ignore_targets_notifies[selection] = @@ -1318,6 +1365,32 @@ static void clipboard_data_send_to_requestor(struct vdagent_x11 *x11, } } +static void uris_ready_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + struct vdagent_x11 *x11 = user_data; + GError *err = NULL; + size_t size; + + char *uris = clipboard_data_translate_to_uris_finish(source, res, &size, &err); + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free(err); + return; + } + if (!x11->selection_req) { + return; + } + uint8_t selection = x11->selection_req->selection; + if (err) { + SELPRINTF("failed to translate data to uris %s", err->message); + g_error_free(err); + } + + clipboard_data_send_to_requestor(x11, selection, (uint8_t *)uris, size, True); + + /* Flush output buffers and consume any pending events */ + vdagent_x11_do_read(x11); +} + void vdagent_x11_clipboard_data(struct vdagent_x11 *x11, uint8_t selection, uint32_t type, uint8_t *data, uint32_t size) { @@ -1361,7 +1434,18 @@ void vdagent_x11_clipboard_data(struct vdagent_x11 *x11, uint8_t selection, return; } - clipboard_data_send_to_requestor(x11, selection, data, size, False); + if (type == VD_AGENT_CLIPBOARD_FILE_LIST) { + g_clear_pointer(&x11->file_list_data[selection], g_bytes_unref); + x11->file_list_data[selection] = g_bytes_new(data, size); + + clipboard_data_translate_to_uris_async( + vdagent_x11_get_atom_name(x11, event->xselectionrequest.target), + x11->file_list_data[selection], NULL, uris_ready_cb, x11 + ); + return; + } else { + clipboard_data_send_to_requestor(x11, selection, data, size, False); + } /* Flush output buffers and consume any pending events */ vdagent_x11_do_read(x11); |