1 /*
2 * QEMU DBus display
3 *
4 * Copyright (c) 2021 Marc-André Lureau <marcandre.lureau@redhat.com>
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 #include "qemu/osdep.h"
25 #include "qemu/dbus.h"
26 #include "qemu/error-report.h"
27 #include "qemu/main-loop.h"
28 #include "qom/object_interfaces.h"
29 #include "sysemu/sysemu.h"
30 #include "qapi/error.h"
31 #include "trace.h"
32
33 #include "dbus.h"
34
35 #define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
36
37 static void
dbus_clipboard_complete_request(DBusDisplay * dpy,GDBusMethodInvocation * invocation,QemuClipboardInfo * info,QemuClipboardType type)38 dbus_clipboard_complete_request(
39 DBusDisplay *dpy,
40 GDBusMethodInvocation *invocation,
41 QemuClipboardInfo *info,
42 QemuClipboardType type)
43 {
44 GVariant *v_data = g_variant_new_from_data(
45 G_VARIANT_TYPE("ay"),
46 info->types[type].data,
47 info->types[type].size,
48 TRUE,
49 (GDestroyNotify)qemu_clipboard_info_unref,
50 qemu_clipboard_info_ref(info));
51
52 qemu_dbus_display1_clipboard_complete_request(
53 dpy->clipboard, invocation,
54 MIME_TEXT_PLAIN_UTF8, v_data);
55 }
56
57 static void
dbus_clipboard_update_info(DBusDisplay * dpy,QemuClipboardInfo * info)58 dbus_clipboard_update_info(DBusDisplay *dpy, QemuClipboardInfo *info)
59 {
60 bool self_update = info->owner == &dpy->clipboard_peer;
61 const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
62 DBusClipboardRequest *req;
63 int i = 0;
64
65 if (info->owner == NULL) {
66 if (dpy->clipboard_proxy) {
67 qemu_dbus_display1_clipboard_call_release(
68 dpy->clipboard_proxy,
69 info->selection,
70 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
71 }
72 return;
73 }
74
75 if (self_update || !info->has_serial) {
76 return;
77 }
78
79 req = &dpy->clipboard_request[info->selection];
80 if (req->invocation && info->types[req->type].data) {
81 dbus_clipboard_complete_request(dpy, req->invocation, info, req->type);
82 g_clear_object(&req->invocation);
83 g_source_remove(req->timeout_id);
84 req->timeout_id = 0;
85 return;
86 }
87
88 if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
89 mime[i++] = MIME_TEXT_PLAIN_UTF8;
90 }
91
92 if (i > 0) {
93 if (dpy->clipboard_proxy) {
94 qemu_dbus_display1_clipboard_call_grab(
95 dpy->clipboard_proxy,
96 info->selection,
97 info->serial,
98 mime,
99 G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
100 }
101 }
102 }
103
104 static void
dbus_clipboard_reset_serial(DBusDisplay * dpy)105 dbus_clipboard_reset_serial(DBusDisplay *dpy)
106 {
107 if (dpy->clipboard_proxy) {
108 qemu_dbus_display1_clipboard_call_register(
109 dpy->clipboard_proxy,
110 G_DBUS_CALL_FLAGS_NONE,
111 -1, NULL, NULL, NULL);
112 }
113 }
114
115 static void
dbus_clipboard_notify(Notifier * notifier,void * data)116 dbus_clipboard_notify(Notifier *notifier, void *data)
117 {
118 DBusDisplay *dpy =
119 container_of(notifier, DBusDisplay, clipboard_peer.notifier);
120 QemuClipboardNotify *notify = data;
121
122 switch (notify->type) {
123 case QEMU_CLIPBOARD_UPDATE_INFO:
124 dbus_clipboard_update_info(dpy, notify->info);
125 return;
126 case QEMU_CLIPBOARD_RESET_SERIAL:
127 dbus_clipboard_reset_serial(dpy);
128 return;
129 }
130 }
131
132 static void
dbus_clipboard_qemu_request(QemuClipboardInfo * info,QemuClipboardType type)133 dbus_clipboard_qemu_request(QemuClipboardInfo *info,
134 QemuClipboardType type)
135 {
136 DBusDisplay *dpy = container_of(info->owner, DBusDisplay, clipboard_peer);
137 g_autofree char *mime = NULL;
138 g_autoptr(GVariant) v_data = NULL;
139 g_autoptr(GError) err = NULL;
140 const char *data = NULL;
141 const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
142 size_t n;
143
144 trace_dbus_clipboard_qemu_request(type);
145
146 if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
147 /* unsupported atm */
148 return;
149 }
150
151 if (dpy->clipboard_proxy) {
152 if (!qemu_dbus_display1_clipboard_call_request_sync(
153 dpy->clipboard_proxy,
154 info->selection,
155 mimes,
156 G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
157 error_report("Failed to request clipboard: %s", err->message);
158 return;
159 }
160
161 if (g_strcmp0(mime, MIME_TEXT_PLAIN_UTF8)) {
162 error_report("Unsupported returned MIME: %s", mime);
163 return;
164 }
165
166 data = g_variant_get_fixed_array(v_data, &n, 1);
167 qemu_clipboard_set_data(&dpy->clipboard_peer, info, type,
168 n, data, true);
169 }
170 }
171
172 static void
dbus_clipboard_request_cancelled(DBusClipboardRequest * req)173 dbus_clipboard_request_cancelled(DBusClipboardRequest *req)
174 {
175 if (!req->invocation) {
176 return;
177 }
178
179 g_dbus_method_invocation_return_error(
180 req->invocation,
181 DBUS_DISPLAY_ERROR,
182 DBUS_DISPLAY_ERROR_FAILED,
183 "Cancelled clipboard request");
184
185 g_clear_object(&req->invocation);
186 g_source_remove(req->timeout_id);
187 req->timeout_id = 0;
188 }
189
190 static void
dbus_clipboard_unregister_proxy(DBusDisplay * dpy)191 dbus_clipboard_unregister_proxy(DBusDisplay *dpy)
192 {
193 const char *name = NULL;
194 int i;
195
196 for (i = 0; i < G_N_ELEMENTS(dpy->clipboard_request); ++i) {
197 dbus_clipboard_request_cancelled(&dpy->clipboard_request[i]);
198 }
199
200 if (!dpy->clipboard_proxy) {
201 return;
202 }
203
204 name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
205 trace_dbus_clipboard_unregister(name);
206 g_clear_object(&dpy->clipboard_proxy);
207 }
208
209 static gboolean
dbus_clipboard_register(DBusDisplay * dpy,GDBusMethodInvocation * invocation)210 dbus_clipboard_register(
211 DBusDisplay *dpy,
212 GDBusMethodInvocation *invocation)
213 {
214 g_autoptr(GError) err = NULL;
215 const char *name = NULL;
216 GDBusConnection *connection = g_dbus_method_invocation_get_connection(invocation);
217
218 if (dpy->clipboard_proxy) {
219 g_dbus_method_invocation_return_error(
220 invocation,
221 DBUS_DISPLAY_ERROR,
222 DBUS_DISPLAY_ERROR_FAILED,
223 "Clipboard peer already registered!");
224 return DBUS_METHOD_INVOCATION_HANDLED;
225 }
226
227 dpy->clipboard_proxy =
228 qemu_dbus_display1_clipboard_proxy_new_sync(
229 connection,
230 G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
231 g_dbus_method_invocation_get_sender(invocation),
232 "/org/qemu/Display1/Clipboard",
233 NULL,
234 &err);
235 if (!dpy->clipboard_proxy) {
236 g_dbus_method_invocation_return_error(
237 invocation,
238 DBUS_DISPLAY_ERROR,
239 DBUS_DISPLAY_ERROR_FAILED,
240 "Failed to setup proxy: %s", err->message);
241 return DBUS_METHOD_INVOCATION_HANDLED;
242 }
243
244 name = g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy));
245 trace_dbus_clipboard_register(name);
246
247 g_object_connect(dpy->clipboard_proxy,
248 "swapped-signal::notify::g-name-owner",
249 dbus_clipboard_unregister_proxy, dpy,
250 NULL);
251 g_object_connect(connection,
252 "swapped-signal::closed",
253 dbus_clipboard_unregister_proxy, dpy,
254 NULL);
255 qemu_clipboard_reset_serial();
256
257 qemu_dbus_display1_clipboard_complete_register(dpy->clipboard, invocation);
258 return DBUS_METHOD_INVOCATION_HANDLED;
259 }
260
261 static gboolean
dbus_clipboard_check_caller(DBusDisplay * dpy,GDBusMethodInvocation * invocation)262 dbus_clipboard_check_caller(DBusDisplay *dpy, GDBusMethodInvocation *invocation)
263 {
264 if (!dpy->clipboard_proxy ||
265 g_strcmp0(g_dbus_proxy_get_name(G_DBUS_PROXY(dpy->clipboard_proxy)),
266 g_dbus_method_invocation_get_sender(invocation))) {
267 g_dbus_method_invocation_return_error(
268 invocation,
269 DBUS_DISPLAY_ERROR,
270 DBUS_DISPLAY_ERROR_FAILED,
271 "Unregistered caller");
272 return FALSE;
273 }
274
275 return TRUE;
276 }
277
278 static gboolean
dbus_clipboard_unregister(DBusDisplay * dpy,GDBusMethodInvocation * invocation)279 dbus_clipboard_unregister(
280 DBusDisplay *dpy,
281 GDBusMethodInvocation *invocation)
282 {
283 if (!dbus_clipboard_check_caller(dpy, invocation)) {
284 return DBUS_METHOD_INVOCATION_HANDLED;
285 }
286
287 dbus_clipboard_unregister_proxy(dpy);
288
289 qemu_dbus_display1_clipboard_complete_unregister(
290 dpy->clipboard, invocation);
291
292 return DBUS_METHOD_INVOCATION_HANDLED;
293 }
294
295 static gboolean
dbus_clipboard_grab(DBusDisplay * dpy,GDBusMethodInvocation * invocation,gint arg_selection,guint arg_serial,const gchar * const * arg_mimes)296 dbus_clipboard_grab(
297 DBusDisplay *dpy,
298 GDBusMethodInvocation *invocation,
299 gint arg_selection,
300 guint arg_serial,
301 const gchar *const *arg_mimes)
302 {
303 QemuClipboardSelection s = arg_selection;
304 g_autoptr(QemuClipboardInfo) info = NULL;
305
306 if (!dbus_clipboard_check_caller(dpy, invocation)) {
307 return DBUS_METHOD_INVOCATION_HANDLED;
308 }
309
310 trace_dbus_clipboard_grab(arg_selection, arg_serial);
311
312 if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
313 g_dbus_method_invocation_return_error(
314 invocation,
315 DBUS_DISPLAY_ERROR,
316 DBUS_DISPLAY_ERROR_FAILED,
317 "Invalid clipboard selection: %d", arg_selection);
318 return DBUS_METHOD_INVOCATION_HANDLED;
319 }
320
321 info = qemu_clipboard_info_new(&dpy->clipboard_peer, s);
322 if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
323 info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
324 }
325 info->serial = arg_serial;
326 info->has_serial = true;
327 if (qemu_clipboard_check_serial(info, true)) {
328 qemu_clipboard_update(info);
329 } else {
330 trace_dbus_clipboard_grab_failed();
331 }
332
333 qemu_dbus_display1_clipboard_complete_grab(dpy->clipboard, invocation);
334 return DBUS_METHOD_INVOCATION_HANDLED;
335 }
336
337 static gboolean
dbus_clipboard_release(DBusDisplay * dpy,GDBusMethodInvocation * invocation,gint arg_selection)338 dbus_clipboard_release(
339 DBusDisplay *dpy,
340 GDBusMethodInvocation *invocation,
341 gint arg_selection)
342 {
343 if (!dbus_clipboard_check_caller(dpy, invocation)) {
344 return DBUS_METHOD_INVOCATION_HANDLED;
345 }
346
347 qemu_clipboard_peer_release(&dpy->clipboard_peer, arg_selection);
348
349 qemu_dbus_display1_clipboard_complete_release(dpy->clipboard, invocation);
350 return DBUS_METHOD_INVOCATION_HANDLED;
351 }
352
353 static gboolean
dbus_clipboard_request_timeout(gpointer user_data)354 dbus_clipboard_request_timeout(gpointer user_data)
355 {
356 dbus_clipboard_request_cancelled(user_data);
357 return G_SOURCE_REMOVE;
358 }
359
360 static gboolean
dbus_clipboard_request(DBusDisplay * dpy,GDBusMethodInvocation * invocation,gint arg_selection,const gchar * const * arg_mimes)361 dbus_clipboard_request(
362 DBusDisplay *dpy,
363 GDBusMethodInvocation *invocation,
364 gint arg_selection,
365 const gchar *const *arg_mimes)
366 {
367 QemuClipboardSelection s = arg_selection;
368 QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
369 QemuClipboardInfo *info = NULL;
370
371 if (!dbus_clipboard_check_caller(dpy, invocation)) {
372 return DBUS_METHOD_INVOCATION_HANDLED;
373 }
374
375 if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
376 g_dbus_method_invocation_return_error(
377 invocation,
378 DBUS_DISPLAY_ERROR,
379 DBUS_DISPLAY_ERROR_FAILED,
380 "Invalid clipboard selection: %d", arg_selection);
381 return DBUS_METHOD_INVOCATION_HANDLED;
382 }
383
384 if (dpy->clipboard_request[s].invocation) {
385 g_dbus_method_invocation_return_error(
386 invocation,
387 DBUS_DISPLAY_ERROR,
388 DBUS_DISPLAY_ERROR_FAILED,
389 "Pending request");
390 return DBUS_METHOD_INVOCATION_HANDLED;
391 }
392
393 info = qemu_clipboard_info(s);
394 if (!info || !info->owner || info->owner == &dpy->clipboard_peer) {
395 g_dbus_method_invocation_return_error(
396 invocation,
397 DBUS_DISPLAY_ERROR,
398 DBUS_DISPLAY_ERROR_FAILED,
399 "Empty clipboard");
400 return DBUS_METHOD_INVOCATION_HANDLED;
401 }
402
403 if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
404 !info->types[type].available) {
405 g_dbus_method_invocation_return_error(
406 invocation,
407 DBUS_DISPLAY_ERROR,
408 DBUS_DISPLAY_ERROR_FAILED,
409 "Unhandled MIME types requested");
410 return DBUS_METHOD_INVOCATION_HANDLED;
411 }
412
413 if (info->types[type].data) {
414 dbus_clipboard_complete_request(dpy, invocation, info, type);
415 } else {
416 qemu_clipboard_request(info, type);
417
418 dpy->clipboard_request[s].invocation = g_object_ref(invocation);
419 dpy->clipboard_request[s].type = type;
420 dpy->clipboard_request[s].timeout_id =
421 g_timeout_add_seconds(5, dbus_clipboard_request_timeout,
422 &dpy->clipboard_request[s]);
423 }
424
425 return DBUS_METHOD_INVOCATION_HANDLED;
426 }
427
428 void
dbus_clipboard_init(DBusDisplay * dpy)429 dbus_clipboard_init(DBusDisplay *dpy)
430 {
431 g_autoptr(GDBusObjectSkeleton) clipboard = NULL;
432
433 assert(!dpy->clipboard);
434
435 clipboard = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Clipboard");
436 dpy->clipboard = qemu_dbus_display1_clipboard_skeleton_new();
437 g_object_connect(dpy->clipboard,
438 "swapped-signal::handle-register",
439 dbus_clipboard_register, dpy,
440 "swapped-signal::handle-unregister",
441 dbus_clipboard_unregister, dpy,
442 "swapped-signal::handle-grab",
443 dbus_clipboard_grab, dpy,
444 "swapped-signal::handle-release",
445 dbus_clipboard_release, dpy,
446 "swapped-signal::handle-request",
447 dbus_clipboard_request, dpy,
448 NULL);
449
450 g_dbus_object_skeleton_add_interface(
451 G_DBUS_OBJECT_SKELETON(clipboard),
452 G_DBUS_INTERFACE_SKELETON(dpy->clipboard));
453 g_dbus_object_manager_server_export(dpy->server, clipboard);
454 dpy->clipboard_peer.name = "dbus";
455 dpy->clipboard_peer.notifier.notify = dbus_clipboard_notify;
456 dpy->clipboard_peer.request = dbus_clipboard_qemu_request;
457 qemu_clipboard_peer_register(&dpy->clipboard_peer);
458 }
459