/* * QEMU VNC display driver -- clipboard support * * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "qemu/osdep.h" #include "vnc.h" #include "vnc-jobs.h" static uint8_t *inflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) { z_stream stream = { .next_in = in, .avail_in = in_len, .zalloc = Z_NULL, .zfree = Z_NULL, }; uint32_t out_len = 8; uint8_t *out = g_malloc(out_len); int ret; stream.next_out = out + stream.total_out; stream.avail_out = out_len - stream.total_out; ret = inflateInit(&stream); if (ret != Z_OK) { goto err; } while (stream.avail_in) { ret = inflate(&stream, Z_FINISH); switch (ret) { case Z_OK: break; case Z_STREAM_END: *size = stream.total_out; inflateEnd(&stream); return out; case Z_BUF_ERROR: out_len <<= 1; if (out_len > (1 << 20)) { goto err_end; } out = g_realloc(out, out_len); stream.next_out = out + stream.total_out; stream.avail_out = out_len - stream.total_out; break; default: goto err_end; } } err_end: inflateEnd(&stream); err: g_free(out); return NULL; } static uint8_t *deflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) { z_stream stream = { .next_in = in, .avail_in = in_len, .zalloc = Z_NULL, .zfree = Z_NULL, }; uint32_t out_len = 8; uint8_t *out = g_malloc(out_len); int ret; stream.next_out = out + stream.total_out; stream.avail_out = out_len - stream.total_out; ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION); if (ret != Z_OK) { goto err; } while (ret != Z_STREAM_END) { ret = deflate(&stream, Z_FINISH); switch (ret) { case Z_OK: case Z_STREAM_END: break; case Z_BUF_ERROR: out_len <<= 1; if (out_len > (1 << 20)) { goto err_end; } out = g_realloc(out, out_len); stream.next_out = out + stream.total_out; stream.avail_out = out_len - stream.total_out; break; default: goto err_end; } } *size = stream.total_out; deflateEnd(&stream); return out; err_end: deflateEnd(&stream); err: g_free(out); return NULL; } static void vnc_clipboard_send(VncState *vs, uint32_t count, uint32_t *dwords) { int i; vnc_lock_output(vs); vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); vnc_write_u8(vs, 0); vnc_write_u8(vs, 0); vnc_write_u8(vs, 0); vnc_write_s32(vs, -(count * sizeof(uint32_t))); /* -(message length) */ for (i = 0; i < count; i++) { vnc_write_u32(vs, dwords[i]); } vnc_unlock_output(vs); vnc_flush(vs); } static void vnc_clipboard_provide(VncState *vs, QemuClipboardInfo *info, QemuClipboardType type) { uint32_t flags = 0; g_autofree uint8_t *buf = NULL; g_autofree void *zbuf = NULL; uint32_t zsize; switch (type) { case QEMU_CLIPBOARD_TYPE_TEXT: flags |= VNC_CLIPBOARD_TEXT; break; default: return; } flags |= VNC_CLIPBOARD_PROVIDE; buf = g_malloc(info->types[type].size + 4); buf[0] = (info->types[type].size >> 24) & 0xff; buf[1] = (info->types[type].size >> 16) & 0xff; buf[2] = (info->types[type].size >> 8) & 0xff; buf[3] = (info->types[type].size >> 0) & 0xff; memcpy(buf + 4, info->types[type].data, info->types[type].size); zbuf = deflate_buffer(buf, info->types[type].size + 4, &zsize); if (!zbuf) { return; } vnc_lock_output(vs); vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); vnc_write_u8(vs, 0); vnc_write_u8(vs, 0); vnc_write_u8(vs, 0); vnc_write_s32(vs, -(sizeof(uint32_t) + zsize)); /* -(message length) */ vnc_write_u32(vs, flags); vnc_write(vs, zbuf, zsize); vnc_unlock_output(vs); vnc_flush(vs); } static void vnc_clipboard_update_info(VncState *vs, QemuClipboardInfo *info) { QemuClipboardType type; bool self_update = info->owner == &vs->cbpeer; uint32_t flags = 0; if (info != vs->cbinfo) { qemu_clipboard_info_unref(vs->cbinfo); vs->cbinfo = qemu_clipboard_info_ref(info); vs->cbpending = 0; if (!self_update) { if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { flags |= VNC_CLIPBOARD_TEXT; } flags |= VNC_CLIPBOARD_NOTIFY; vnc_clipboard_send(vs, 1, &flags); } return; } if (self_update) { return; } for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { if (vs->cbpending & (1 << type)) { vs->cbpending &= ~(1 << type); vnc_clipboard_provide(vs, info, type); } } } static void vnc_clipboard_notify(Notifier *notifier, void *data) { VncState *vs = container_of(notifier, VncState, cbpeer.notifier); QemuClipboardNotify *notify = data; switch (notify->type) { case QEMU_CLIPBOARD_UPDATE_INFO: vnc_clipboard_update_info(vs, notify->info); return; case QEMU_CLIPBOARD_RESET_SERIAL: /* ignore */ return; } } static void vnc_clipboard_request(QemuClipboardInfo *info, QemuClipboardType type) { VncState *vs = container_of(info->owner, VncState, cbpeer); uint32_t flags = 0; if (type == QEMU_CLIPBOARD_TYPE_TEXT) { flags |= VNC_CLIPBOARD_TEXT; } if (!flags) { return; } flags |= VNC_CLIPBOARD_REQUEST; vnc_clipboard_send(vs, 1, &flags); } void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data) { if (flags & VNC_CLIPBOARD_CAPS) { /* need store caps somewhere ? */ return; } if (flags & VNC_CLIPBOARD_NOTIFY) { QemuClipboardInfo *info = qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); if (flags & VNC_CLIPBOARD_TEXT) { info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; } qemu_clipboard_update(info); qemu_clipboard_info_unref(info); return; } if (flags & VNC_CLIPBOARD_PROVIDE && vs->cbinfo && vs->cbinfo->owner == &vs->cbpeer) { uint32_t size = 0; g_autofree uint8_t *buf = inflate_buffer(data, len - 4, &size); if ((flags & VNC_CLIPBOARD_TEXT) && buf && size >= 4) { uint32_t tsize = read_u32(buf, 0); uint8_t *tbuf = buf + 4; if (tsize < size) { qemu_clipboard_set_data(&vs->cbpeer, vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT, tsize, tbuf, true); } } } if (flags & VNC_CLIPBOARD_REQUEST && vs->cbinfo && vs->cbinfo->owner != &vs->cbpeer) { if ((flags & VNC_CLIPBOARD_TEXT) && vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { if (vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].data) { vnc_clipboard_provide(vs, vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); } else { vs->cbpending |= (1 << QEMU_CLIPBOARD_TYPE_TEXT); qemu_clipboard_request(vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); } } } } void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text) { QemuClipboardInfo *info = qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); qemu_clipboard_set_data(&vs->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT, len, text, true); qemu_clipboard_info_unref(info); } void vnc_server_cut_text_caps(VncState *vs) { uint32_t caps[2]; if (!vnc_has_feature(vs, VNC_FEATURE_CLIPBOARD_EXT)) { return; } caps[0] = (VNC_CLIPBOARD_PROVIDE | VNC_CLIPBOARD_NOTIFY | VNC_CLIPBOARD_REQUEST | VNC_CLIPBOARD_CAPS | VNC_CLIPBOARD_TEXT); caps[1] = 0; vnc_clipboard_send(vs, 2, caps); if (!vs->cbpeer.notifier.notify) { vs->cbpeer.name = "vnc"; vs->cbpeer.notifier.notify = vnc_clipboard_notify; vs->cbpeer.request = vnc_clipboard_request; qemu_clipboard_peer_register(&vs->cbpeer); } }