summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am2
-rw-r--r--src/vdagent/webdav-cb.c292
-rw-r--r--src/vdagent/webdav-cb.h38
-rw-r--r--src/vdagent/x11-priv.h8
-rw-r--r--src/vdagent/x11.c86
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);