/* Looking Glass - KVM FrameRelay (KVMFR) Client Copyright (C) 2017-2019 Geoffrey McRae https://looking-glass.hostfission.com 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "spice.h" #include "utils.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "messages.h" #include "rsa.h" #ifdef DEBUG_SPICE_MOUSE #define DEBUG_MOUSE(fmt, args...) DEBUG_PRINT("[M]", fmt, ##args) #else #define DEBUG_MOUSE(fmt, args...) do {} while(0) #endif #ifdef DEBUG_SPICE_KEYBOARD #define DEBUG_KEYBOARD(fmt, args...) DEBUG_PRINT("[K]", fmt, ##args) #else #define DEBUG_KEYBOARD(fmt, args...) do {} while(0) #endif #ifdef DEBUG_SPICE_CLIPBOARD #define DEBUG_CLIPBOARD(fmt, args...) DEBUG_PRINT("[C]", fmt, ##args) #else #define DEBUG_CLIPBOARD(fmt, args...) do {} while(0) #endif // we don't really need flow control because we are all local // instead do what the spice-gtk library does and provide the largest // possible number #define SPICE_AGENT_TOKENS_MAX ~0 // ============================================================================ // internal structures struct SpiceChannel { bool connected; bool ready; bool initDone; uint8_t channelType; int socket; uint32_t ackFrequency; uint32_t ackCount; uint32_t serial; LG_Lock lock; }; struct SpiceKeyboard { uint32_t modifiers; }; struct SpiceMouse { uint32_t buttonState; int sentCount; int rpos, wpos; }; union SpiceAddr { struct sockaddr addr; struct sockaddr_in in; struct sockaddr_in6 in6; struct sockaddr_un un; }; struct Spice { char password[32]; short family; union SpiceAddr addr; bool hasAgent; uint32_t serverTokens; uint32_t clientTokens; uint32_t sessionID; uint32_t channelID; struct SpiceChannel scMain; struct SpiceChannel scInputs; struct SpiceKeyboard kb; struct SpiceMouse mouse; bool cbSupported; bool cbSelection; // clipboard variables bool cbAgentGrabbed; bool cbClientGrabbed; SpiceDataType cbType; uint8_t * cbBuffer; uint32_t cbRemain; uint32_t cbSize; SpiceClipboardNotice cbNoticeFn; SpiceClipboardData cbDataFn; SpiceClipboardRelease cbReleaseFn; SpiceClipboardRequest cbRequestFn; }; // globals struct Spice spice = { .sessionID = 0, .scMain .connected = false, .scMain .channelType = SPICE_CHANNEL_MAIN, .scInputs.connected = false, .scInputs.channelType = SPICE_CHANNEL_INPUTS, }; // internal forward decls bool spice_connect_channel (struct SpiceChannel * channel); void spice_disconnect_channel(struct SpiceChannel * channel); bool spice_process_ack(struct SpiceChannel * channel); bool spice_on_common_read (struct SpiceChannel * channel, SpiceMiniDataHeader * header, bool * handled); bool spice_on_main_channel_read (); bool spice_on_inputs_channel_read(); bool spice_agent_process (uint32_t dataSize); bool spice_agent_connect (); bool spice_agent_send_caps(bool request); void spice_agent_on_clipboard(); // utility functions static uint32_t spice_type_to_agent_type(SpiceDataType type); static SpiceDataType agent_type_to_spice_type(uint32_t type); // thread safe read/write methods bool spice_write_msg (struct SpiceChannel * channel, uint32_t type, const void * buffer, const ssize_t size); bool spice_agent_write_msg (uint32_t type, const void * buffer, ssize_t size); // non thread safe read/write methods (nl = non-locking) bool spice_read_nl (const struct SpiceChannel * channel, void * buffer, const ssize_t size); ssize_t spice_write_nl (const struct SpiceChannel * channel, const void * buffer, const ssize_t size); bool spice_discard_nl (const struct SpiceChannel * channel, ssize_t size); bool spice_write_msg_nl( struct SpiceChannel * channel, uint32_t type, const void * buffer, const ssize_t size, const ssize_t extra); // ============================================================================ bool spice_connect(const char * host, const unsigned short port, const char * password) { strncpy(spice.password, password, sizeof(spice.password) - 1); memset(&spice.addr, 0, sizeof(spice.addr)); if (port == 0) { spice.family = AF_UNIX; spice.addr.un.sun_family = spice.family; strncpy(spice.addr.un.sun_path, host, sizeof(spice.addr.un.sun_path) - 1); DEBUG_INFO("Remote: %s", host); } else { spice.family = AF_INET; inet_pton(spice.family, host, &spice.addr.in.sin_addr); spice.addr.in.sin_family = spice.family; spice.addr.in.sin_port = htons(port); DEBUG_INFO("Remote: %s:%u", host, port); } spice.channelID = 0; if (!spice_connect_channel(&spice.scMain)) { DEBUG_ERROR("connect main channel failed"); return false; } return true; } // ============================================================================ void spice_disconnect() { spice_disconnect_channel(&spice.scMain ); spice_disconnect_channel(&spice.scInputs); spice.sessionID = 0; if (spice.cbBuffer) free(spice.cbBuffer); spice.cbBuffer = NULL; spice.cbRemain = 0; spice.cbSize = 0; spice.cbAgentGrabbed = false; spice.cbClientGrabbed = false; } // ============================================================================ bool spice_ready() { return spice.scMain.connected && spice.scInputs.connected; } // ============================================================================ bool spice_process() { fd_set readSet; FD_ZERO(&readSet); FD_SET(spice.scMain.socket , &readSet); FD_SET(spice.scInputs.socket, &readSet); struct timeval timeout; timeout.tv_sec = 1; timeout.tv_usec = 0; int rc = select(FD_SETSIZE, &readSet, NULL, NULL, &timeout); if (rc < 0) { DEBUG_ERROR("select failure"); return false; } for(int i = 0; i < FD_SETSIZE; ++i) if (FD_ISSET(i, &readSet)) { if (i == spice.scMain.socket) { if (spice_on_main_channel_read()) { if (spice.scMain.connected && !spice_process_ack(&spice.scMain)) { DEBUG_ERROR("failed to process ack on main channel"); return false; } continue; } else { DEBUG_ERROR("failed to perform read on main channel"); return false; } } if (spice.scInputs.connected && i == spice.scInputs.socket) { if (!spice_process_ack(&spice.scInputs)) { DEBUG_ERROR("failed to process ack on inputs channel"); return false; } if (spice_on_inputs_channel_read()) continue; else { DEBUG_ERROR("failed to perform read on inputs channel"); return false; } } } return true; } // ============================================================================ bool spice_process_ack(struct SpiceChannel * channel) { if (channel->ackFrequency == 0) return true; if (channel->ackCount++ != channel->ackFrequency) return true; channel->ackCount = 0; return spice_write_msg(channel, SPICE_MSGC_ACK, "\0", 1); } // ============================================================================ bool spice_on_common_read(struct SpiceChannel * channel, SpiceMiniDataHeader * header, bool * handled) { *handled = false; if (!spice_read_nl(channel, header, sizeof(SpiceMiniDataHeader))) { DEBUG_ERROR("read failure"); return false; } //#if 0 DEBUG_PROTO("socket: %d, type: %2u, size %6u", channel->socket, header->type, header->size); //#endif if (!channel->initDone) return true; switch(header->type) { case SPICE_MSG_MIGRATE: case SPICE_MSG_MIGRATE_DATA: { DEBUG_PROTO("SPICE_MSG_MIGRATE_DATA"); *handled = true; DEBUG_WARN("migration is not supported"); return false; } case SPICE_MSG_SET_ACK: { DEBUG_INFO("SPICE_MSG_SET_ACK"); *handled = true; SpiceMsgSetAck in; if (!spice_read_nl(channel, &in, sizeof(in))) return false; channel->ackFrequency = in.window; SpiceMsgcAckSync out; out.generation = in.generation; if (!spice_write_msg(channel, SPICE_MSGC_ACK_SYNC, &out, sizeof(out))) return false; return true; } case SPICE_MSG_PING: { DEBUG_PROTO("SPICE_MSG_PING"); *handled = true; SpiceMsgPing in; if (!spice_read_nl(channel, &in, sizeof(in))) return false; const int discard = header->size - sizeof(in); if (!spice_discard_nl(channel, discard)) { DEBUG_ERROR("failed discarding enough bytes (%d) from the ping packet", discard); return false; } else DEBUG_PROTO("discard %d", discard); SpiceMsgcPong out; out.id = in.id; out.timestamp = in.timestamp; if (!spice_write_msg(channel, SPICE_MSGC_PONG, &out, sizeof(out))) return false; return true; } case SPICE_MSG_WAIT_FOR_CHANNELS: case SPICE_MSG_DISCONNECTING : { *handled = true; DEBUG_FIXME("ignored wait-for-channels or disconnect message"); return false; } case SPICE_MSG_NOTIFY: { DEBUG_PROTO("SPICE_MSG_NOTIFY"); SpiceMsgNotify in; if (!spice_read_nl(channel, &in, sizeof(in))) return false; char msg[in.message_len+1]; if (!spice_read_nl(channel, msg, in.message_len+1)) return false; DEBUG_INFO("notify message: %s", msg); *handled = true; return true; } } return true; } // ============================================================================ bool spice_on_main_channel_read() { struct SpiceChannel *channel = &spice.scMain; SpiceMiniDataHeader header; bool handled; if (!spice_on_common_read(channel, &header, &handled)) { DEBUG_ERROR("read failure"); return false; } if (handled) return true; if (!channel->initDone) { if (header.type != SPICE_MSG_MAIN_INIT) { spice_disconnect(); DEBUG_ERROR("expected main init message but got type %u", header.type); return false; } DEBUG_PROTO("SPICE_MSG_MAIN_INIT"); channel->initDone = true; SpiceMsgMainInit msg; if (!spice_read_nl(channel, &msg, sizeof(msg))) { spice_disconnect(); return false; } spice.sessionID = msg.session_id; spice.serverTokens = msg.agent_tokens; spice.hasAgent = msg.agent_connected; if (spice.hasAgent && !spice_agent_connect()) { spice_disconnect(); DEBUG_ERROR("failed to connect to spice agent"); return false; } if (msg.current_mouse_mode != SPICE_MOUSE_MODE_CLIENT && !spice_mouse_mode(false)) { DEBUG_ERROR("failed to set mouse mode"); return false; } if (!spice_write_msg(channel, SPICE_MSGC_MAIN_ATTACH_CHANNELS, NULL, 0)) { spice_disconnect(); DEBUG_ERROR("failed to ask for channel list"); return false; } return true; } if (header.type == SPICE_MSG_MAIN_CHANNELS_LIST) { DEBUG_PROTO("SPICE_MSG_MAIN_CHANNELS_LIST"); SpiceMainChannelsList msg; if (!spice_read_nl(channel, &msg, sizeof(msg))) { DEBUG_ERROR("Failed to read channel list msg"); spice_disconnect(); return false; } // documentation doesn't state that the array is null terminated but it seems that it is SpiceChannelID channels[msg.num_of_channels]; if (!spice_read_nl(channel, &channels, msg.num_of_channels * sizeof(SpiceChannelID))) { DEBUG_ERROR("Failed to read channel list vector"); spice_disconnect(); return false; } for(int i = 0; i < msg.num_of_channels; ++i) { DEBUG_PROTO("channel %d = %u", i, channels[i].type); if (channels[i].type == SPICE_CHANNEL_INPUTS) { if (spice.scInputs.connected) { DEBUG_ERROR("inputs channel already connected"); spice_disconnect(); return false; } if (!spice_connect_channel(&spice.scInputs)) { DEBUG_ERROR("failed to connect inputs channel"); spice_disconnect(); return false; } } } return true; } if (header.type == SPICE_MSG_MAIN_AGENT_CONNECTED) { DEBUG_PROTO("SPICE_MSG_MAIN_AGENT_CONNECTED"); spice.hasAgent = true; if (!spice_agent_connect()) { DEBUG_ERROR("failed to connect to spice agent"); spice_disconnect(); return false; } return true; } if (header.type == SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS) { DEBUG_PROTO("SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS"); uint32_t num_tokens; if (!spice_read_nl(channel, &num_tokens, sizeof(num_tokens))) { DEBUG_ERROR("failed to read agent tokens"); spice_disconnect(); return false; } spice.hasAgent = true; spice.serverTokens = num_tokens; if (!spice_agent_connect()) { DEBUG_ERROR("failed to connect to spice agent"); spice_disconnect(); return false; } return true; } if (header.type == SPICE_MSG_MAIN_AGENT_DISCONNECTED) { DEBUG_PROTO("SPICE_MSG_MAIN_AGENT_DISCONNECTED"); uint32_t error; if (!spice_read_nl(channel, &error, sizeof(error))) { DEBUG_ERROR("failed to read agent disconnect error code"); spice_disconnect(); return false; } DEBUG_INFO("Spice agent disconnected, error: %u", error); spice.hasAgent = false; if (spice.cbBuffer) { free(spice.cbBuffer); spice.cbBuffer = NULL; spice.cbSize = 0; spice.cbRemain = 0; } return true; } if (header.type == SPICE_MSG_MAIN_AGENT_DATA) { DEBUG_PROTO("SPICE_MSG_MAIN_AGENT_DATA"); if (!spice.hasAgent) { DEBUG_WARN("recieved agent data when the agent is yet to be started"); spice_discard_nl(channel, header.size); return true; } if (!spice_agent_process(header.size)) { DEBUG_ERROR("failed to process spice agent message"); spice_disconnect(); return false; } return true; } if (header.type == SPICE_MSG_MAIN_AGENT_TOKEN) { DEBUG_PROTO("SPICE_MSG_MAIN_AGENT_TOKEN"); uint32_t num_tokens; if (!spice_read_nl(channel, &num_tokens, sizeof(num_tokens))) { DEBUG_ERROR("failed to read agent tokens"); spice_disconnect(); return false; } spice.serverTokens = num_tokens; return true; } DEBUG_WARN("main channel unhandled message type %u", header.type); spice_discard_nl(channel, header.size); return true; } // ============================================================================ bool spice_on_inputs_channel_read() { struct SpiceChannel *channel = &spice.scInputs; SpiceMiniDataHeader header; bool handled; if (!spice_on_common_read(channel, &header, &handled)) { DEBUG_ERROR("read failure"); return false; } if (handled) return true; switch(header.type) { case SPICE_MSG_INPUTS_INIT: { DEBUG_PROTO("SPICE_MSG_INPUTS_INIT"); if (channel->initDone) { DEBUG_ERROR("input init message already done"); return false; } channel->initDone = true; SpiceMsgInputsInit in; if (!spice_read_nl(channel, &in, sizeof(in))) return false; return true; } case SPICE_MSG_INPUTS_KEY_MODIFIERS: { DEBUG_PROTO("SPICE_MSG_INPUTS_KEY_MODIFIERS"); SpiceMsgInputsInit in; if (!spice_read_nl(channel, &in, sizeof(in))) return false; spice.kb.modifiers = in.modifiers; return true; } case SPICE_MSG_INPUTS_MOUSE_MOTION_ACK: { DEBUG_PROTO("SPICE_MSG_INPUTS_MOUSE_MOTION_ACK"); const int count = __sync_add_and_fetch(&spice.mouse.sentCount, SPICE_INPUT_MOTION_ACK_BUNCH); if (count < 0) { DEBUG_ERROR("comms failure, too many mouse motion ACKs recieved"); return false; } return true; } } DEBUG_WARN("inputs channel unhandled message type %u", header.type); spice_discard_nl(channel, header.size); return true; } // ============================================================================ bool spice_connect_channel(struct SpiceChannel * channel) { channel->initDone = false; channel->ackFrequency = 0; channel->ackCount = 0; channel->serial = 0; LG_LOCK_INIT(channel->lock); size_t addrSize; switch(spice.family) { case AF_UNIX: addrSize = sizeof(spice.addr.un); break; case AF_INET: addrSize = sizeof(spice.addr.in); break; case AF_INET6: addrSize = sizeof(spice.addr.in6); break; default: DEBUG_ERROR("Unsupported socket family"); return false; } channel->socket = socket(spice.family, SOCK_STREAM, 0); if (channel->socket == -1) return false; if (spice.family != AF_UNIX) { int flag = 1; setsockopt(channel->socket, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int)); } if (connect(channel->socket, &spice.addr.addr, addrSize) == -1) { DEBUG_ERROR("socket connect failure"); close(channel->socket); return false; } channel->connected = true; uint32_t supportCaps[COMMON_CAPS_BYTES / sizeof(uint32_t)]; uint32_t channelCaps[MAIN_CAPS_BYTES / sizeof(uint32_t)]; memset(supportCaps, 0, sizeof(supportCaps)); memset(channelCaps, 0, sizeof(channelCaps)); COMMON_SET_CAPABILITY(supportCaps, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION); COMMON_SET_CAPABILITY(supportCaps, SPICE_COMMON_CAP_AUTH_SPICE ); COMMON_SET_CAPABILITY(supportCaps, SPICE_COMMON_CAP_MINI_HEADER ); if (channel == &spice.scMain) MAIN_SET_CAPABILITY(channelCaps, SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS); SpiceLinkHeader header = { .magic = SPICE_MAGIC , .major_version = SPICE_VERSION_MAJOR, .minor_version = SPICE_VERSION_MINOR, .size = sizeof(SpiceLinkMess) + sizeof(supportCaps ) + sizeof(channelCaps ) }; SpiceLinkMess message = { .connection_id = spice.sessionID, .channel_type = channel->channelType, .channel_id = spice.channelID, .num_common_caps = sizeof(supportCaps) / sizeof(uint32_t), .num_channel_caps = sizeof(channelCaps) / sizeof(uint32_t), .caps_offset = sizeof(SpiceLinkMess) }; if ( !spice_write_nl(channel, &header , sizeof(header )) || !spice_write_nl(channel, &message , sizeof(message )) || !spice_write_nl(channel, &supportCaps, sizeof(supportCaps)) || !spice_write_nl(channel, &channelCaps, sizeof(channelCaps)) ) { DEBUG_ERROR("failed to write the initial payload"); spice_disconnect_channel(channel); return false; } if (!spice_read_nl(channel, &header, sizeof(header))) { DEBUG_ERROR("failed to read SpiceLinkHeader"); spice_disconnect_channel(channel); return false; } if (header.magic != SPICE_MAGIC || header.major_version != SPICE_VERSION_MAJOR) { DEBUG_ERROR("invalid or unsupported protocol version"); spice_disconnect_channel(channel); return false; } if (header.size < sizeof(SpiceLinkReply)) { DEBUG_ERROR("reported data size too small"); spice_disconnect_channel(channel); return false; } SpiceLinkReply reply; if (!spice_read_nl(channel, &reply, sizeof(reply))) { DEBUG_ERROR("failed to read SpiceLinkReply"); spice_disconnect_channel(channel); return false; } if (reply.error != SPICE_LINK_ERR_OK) { DEBUG_ERROR("server replied with error %u", reply.error); spice_disconnect_channel(channel); return false; } uint32_t capsCommon [reply.num_common_caps ]; uint32_t capsChannel[reply.num_channel_caps]; if ( !spice_read_nl(channel, &capsCommon , sizeof(capsCommon)) || !spice_read_nl(channel, &capsChannel, sizeof(capsChannel)) ) { DEBUG_ERROR("failed to read the capabilities"); spice_disconnect_channel(channel); return false; } SpiceLinkAuthMechanism auth; auth.auth_mechanism = SPICE_COMMON_CAP_AUTH_SPICE; if (!spice_write_nl(channel, &auth, sizeof(auth))) { DEBUG_ERROR("failed to write the auth mechanism"); spice_disconnect_channel(channel); return false; } struct spice_password pass; if (!spice_rsa_encrypt_password(reply.pub_key, spice.password, &pass)) { DEBUG_ERROR("failed to encrypt the password"); spice_disconnect_channel(channel); return false; } if (!spice_write_nl(channel, pass.data, pass.size)) { spice_rsa_free_password(&pass); DEBUG_ERROR("failed to write encrypted data"); spice_disconnect_channel(channel); return false; } spice_rsa_free_password(&pass); uint32_t linkResult; if (!spice_read_nl(channel, &linkResult, sizeof(linkResult))) { DEBUG_ERROR("failed to read SpiceLinkResult"); spice_disconnect_channel(channel); return false; } if (linkResult != SPICE_LINK_ERR_OK) { DEBUG_ERROR("connect code error %u", linkResult); spice_disconnect_channel(channel); return false; } channel->ready = true; return true; } // ============================================================================ void spice_disconnect_channel(struct SpiceChannel * channel) { if (channel->connected) { shutdown(channel->socket, SHUT_WR); char buffer[1024]; ssize_t len = 0; do len = read(channel->socket, buffer, sizeof(buffer)); while(len > 0); close(channel->socket); } channel->connected = false; LG_LOCK_FREE(channel->lock); } // ============================================================================ bool spice_agent_connect() { DEBUG_INFO("Spice agent available, sending start"); spice.clientTokens = SPICE_AGENT_TOKENS_MAX; if (!spice_write_msg(&spice.scMain, SPICE_MSGC_MAIN_AGENT_START, &spice.clientTokens, sizeof(spice.clientTokens))) { DEBUG_ERROR("failed to send agent start message"); return false; } if (!spice_agent_send_caps(true)) return false; return true; } // ============================================================================ bool spice_agent_process(uint32_t dataSize) { if (spice.cbRemain) { const uint32_t r = spice.cbRemain > dataSize ? dataSize : spice.cbRemain; if (!spice_read_nl(&spice.scMain, spice.cbBuffer + spice.cbSize, r)) { DEBUG_ERROR("failed to read the clipboard data"); free(spice.cbBuffer); spice.cbBuffer = NULL; spice.cbRemain = 0; spice.cbSize = 0; return false; } spice.cbRemain -= r; spice.cbSize += r; if (spice.cbRemain == 0) spice_agent_on_clipboard(); return true; } VDAgentMessage msg; #pragma pack(push,1) struct Selection { uint8_t selection; uint8_t reserved[3]; }; #pragma pack(pop) if (!spice_read_nl(&spice.scMain, &msg, sizeof(msg))) { DEBUG_ERROR("failed to read spice agent message"); return false; } dataSize -= sizeof(msg); if (msg.protocol != VD_AGENT_PROTOCOL) { DEBUG_ERROR("invalid or unknown spice agent protocol"); return false; } switch(msg.type) { case VD_AGENT_ANNOUNCE_CAPABILITIES: { VDAgentAnnounceCapabilities *caps = (VDAgentAnnounceCapabilities *)malloc(msg.size); memset(caps, 0, msg.size); if (!spice_read_nl(&spice.scMain, caps, msg.size)) { DEBUG_ERROR("failed to read agent message payload"); free(caps); return false; } const int capsSize = VD_AGENT_CAPS_SIZE_FROM_MSG_SIZE(msg.size); spice.cbSupported = VD_AGENT_HAS_CAPABILITY(caps->caps, capsSize, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND) || VD_AGENT_HAS_CAPABILITY(caps->caps, capsSize, VD_AGENT_CAP_CLIPBOARD_SELECTION); spice.cbSelection = VD_AGENT_HAS_CAPABILITY(caps->caps, capsSize, VD_AGENT_CAP_CLIPBOARD_SELECTION); if (spice.cbSupported) DEBUG_INFO("clipboard capability detected"); if (caps->request && !spice_agent_send_caps(false)) { free(caps); return false; } free(caps); return true; } case VD_AGENT_CLIPBOARD: case VD_AGENT_CLIPBOARD_REQUEST: case VD_AGENT_CLIPBOARD_GRAB: case VD_AGENT_CLIPBOARD_RELEASE: { uint32_t remaining = msg.size; if (spice.cbSelection) { struct Selection selection; if (!spice_read_nl(&spice.scMain, &selection, sizeof(selection))) { DEBUG_ERROR("failed to read the clipboard selection"); return false; } remaining -= sizeof(selection); dataSize -= sizeof(selection); } if (msg.type == VD_AGENT_CLIPBOARD_RELEASE) { DEBUG_CLIPBOARD("VD_AGENT_CLIPBOARD_RELEASE"); spice.cbAgentGrabbed = false; if (spice.cbReleaseFn) spice.cbReleaseFn(); return true; } if (msg.type == VD_AGENT_CLIPBOARD || msg.type == VD_AGENT_CLIPBOARD_REQUEST) { uint32_t type; if (!spice_read_nl(&spice.scMain, &type, sizeof(type))) { DEBUG_ERROR("failed to read the clipboard data type"); return false; } remaining -= sizeof(type); dataSize -= sizeof(type); if (msg.type == VD_AGENT_CLIPBOARD) { DEBUG_CLIPBOARD("VD_AGENT_CLIPBOARD"); if (spice.cbBuffer) { DEBUG_ERROR("cbBuffer was never freed"); return false; } spice.cbSize = 0; spice.cbRemain = remaining; spice.cbBuffer = (uint8_t *)malloc(remaining); const uint32_t r = remaining > dataSize ? dataSize : remaining; if (!spice_read_nl(&spice.scMain, spice.cbBuffer, r)) { DEBUG_ERROR("failed to read the clipboard data"); free(spice.cbBuffer); spice.cbBuffer = NULL; spice.cbRemain = 0; spice.cbSize = 0; return false; } spice.cbRemain -= r; spice.cbSize += r; if (spice.cbRemain == 0) spice_agent_on_clipboard(); return true; } else { DEBUG_CLIPBOARD("VD_AGENT_CLIPBOARD_REQUEST"); if (spice.cbRequestFn) spice.cbRequestFn(agent_type_to_spice_type(type)); return true; } } else { DEBUG_CLIPBOARD("VD_AGENT_CLIPBOARD_GRAB"); if (remaining == 0) return true; uint32_t *types = malloc(remaining); if (!spice_read_nl(&spice.scMain, types, remaining)) { DEBUG_ERROR("failed to read the clipboard grab types"); return false; } // there is zero documentation on the types field, it might be a bitfield // but for now we are going to assume it's not. spice.cbType = agent_type_to_spice_type(types[0]); spice.cbAgentGrabbed = true; spice.cbClientGrabbed = false; if (spice.cbSelection) { // Windows doesnt support this, so until it's needed there is no point messing with it DEBUG_ERROR("Fixme!"); return false; } if (spice.cbNoticeFn) spice.cbNoticeFn(spice.cbType); free(types); return true; } } } DEBUG_WARN("unknown agent message type %d", msg.type); spice_discard_nl(&spice.scMain, msg.size); return true; } // ============================================================================ void spice_agent_on_clipboard() { if (spice.cbDataFn) spice.cbDataFn(spice.cbType, spice.cbBuffer, spice.cbSize); free(spice.cbBuffer); spice.cbBuffer = NULL; spice.cbSize = 0; spice.cbRemain = 0; } // ============================================================================ bool spice_agent_send_caps(bool request) { const ssize_t capsSize = sizeof(VDAgentAnnounceCapabilities) + VD_AGENT_CAPS_BYTES; VDAgentAnnounceCapabilities *caps = (VDAgentAnnounceCapabilities *)malloc(capsSize); memset(caps, 0, capsSize); caps->request = request ? 1 : 0; VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND); VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_SELECTION); if (!spice_agent_write_msg(VD_AGENT_ANNOUNCE_CAPABILITIES, caps, capsSize)) { DEBUG_ERROR("failed to send agent capabilities"); free(caps); return false; } free(caps); return true; } // ============================================================================ bool spice_agent_write_msg(uint32_t type, const void * buffer, ssize_t size) { VDAgentMessage msg; msg.protocol = VD_AGENT_PROTOCOL; msg.type = type; msg.opaque = 0; msg.size = size; LG_LOCK(spice.scMain.lock); uint8_t * buf = (uint8_t *)buffer; ssize_t toWrite = size > VD_AGENT_MAX_DATA_SIZE - sizeof(msg) ? VD_AGENT_MAX_DATA_SIZE - sizeof(msg) : size; if (!spice_write_msg_nl(&spice.scMain, SPICE_MSGC_MAIN_AGENT_DATA, &msg, sizeof(msg), toWrite)) { LG_UNLOCK(spice.scMain.lock); DEBUG_ERROR("failed to write agent data header"); return false; } bool first = true; while(toWrite) { bool ok = false; if (first) { ok = spice_write_nl(&spice.scMain, buf, toWrite) == toWrite; first = false; } else { ok = spice_write_msg_nl(&spice.scMain, SPICE_MSGC_MAIN_AGENT_DATA, buf, toWrite, 0); } if (!ok) { LG_UNLOCK(spice.scMain.lock); DEBUG_ERROR("failed to write agent data payload"); return false; } size -= toWrite; buf += toWrite; toWrite = size > VD_AGENT_MAX_DATA_SIZE ? VD_AGENT_MAX_DATA_SIZE : size; } LG_UNLOCK(spice.scMain.lock); return true; } // ============================================================================ ssize_t spice_write_nl(const struct SpiceChannel * channel, const void * buffer, const ssize_t size) { if (!channel->connected) { DEBUG_ERROR("not connected"); return -1; } if (!buffer) { DEBUG_ERROR("invalid buffer argument supplied"); return -1; } ssize_t len = send(channel->socket, buffer, size, 0); if (len != size) DEBUG_WARN("incomplete write"); return len; } // ============================================================================ inline bool spice_write_msg(struct SpiceChannel * channel, uint32_t type, const void * buffer, const ssize_t size) { bool result; LG_LOCK(channel->lock); result = spice_write_msg_nl(channel, type, buffer, size, 0); LG_UNLOCK(channel->lock); return result; } // ============================================================================ bool spice_write_msg_nl(struct SpiceChannel * channel, uint32_t type, const void * buffer, const ssize_t size, const ssize_t extra) { if (!channel->ready) { DEBUG_ERROR("channel not ready"); return false; } SpiceMiniDataHeader header; ++channel->serial; header.type = type; header.size = size + extra; if (spice_write_nl(channel, &header, sizeof(header)) != sizeof(header)) { DEBUG_ERROR("failed to write message header"); return false; } if (buffer && size) { if (spice_write_nl(channel, buffer, size) != size) { DEBUG_ERROR("failed to write message body"); return false; } } return true; } // ============================================================================ bool spice_read_nl(const struct SpiceChannel * channel, void * buffer, const ssize_t size) { if (!channel->connected) { DEBUG_ERROR("not connected"); return -1; } if (!buffer) { DEBUG_ERROR("invalid buffer argument supplied"); return false; } size_t left = size; uint8_t * buf = (uint8_t *)buffer; while(left) { ssize_t len = read(channel->socket, buf, left); if (len <= 0) { if (len == 0) DEBUG_ERROR("remote end closd connection after %ld byte(s)", size - left); return false; } left -= len; buf += len; } return true; } // ============================================================================ bool spice_discard_nl(const struct SpiceChannel * channel, ssize_t size) { void *c = malloc(8192); ssize_t left = size; while(left) { size_t len = read(channel->socket, c, left > 8192 ? 8192 : left); if (len <= 0) { if (len == 0) DEBUG_ERROR("remote end closed connection after %ld byte(s)", size - left); free(c); return false; } left -= len; } free(c); return true; } // ============================================================================ bool spice_key_down(uint32_t code) { DEBUG_KEYBOARD("%u", code); if (!spice.scInputs.connected) { DEBUG_ERROR("not connected"); return false; } if (code > 0x100) code = 0xe0 | ((code - 0x100) << 8); SpiceMsgcKeyDown msg; msg.code = code; return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_KEY_DOWN, &msg, sizeof(msg)); } // ============================================================================ bool spice_key_up(uint32_t code) { DEBUG_KEYBOARD("%u", code); if (!spice.scInputs.connected) { DEBUG_ERROR("not connected"); return false; } if (code < 0x100) code |= 0x80; else code = 0x80e0 | ((code - 0x100) << 8); SpiceMsgcKeyDown msg; msg.code = code; return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_KEY_UP, &msg, sizeof(msg)); } // ============================================================================ bool spice_mouse_mode(bool server) { DEBUG_MOUSE("%s", server ? "server" : "client"); if (!spice.scMain.connected) { DEBUG_ERROR("not connected"); return false; } SpiceMsgcMainMouseModeRequest msg; msg.mouse_mode = server ? SPICE_MOUSE_MODE_SERVER : SPICE_MOUSE_MODE_CLIENT; return spice_write_msg(&spice.scMain, SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST, &msg, sizeof(msg)); } // ============================================================================ bool spice_mouse_position(uint32_t x, uint32_t y) { DEBUG_MOUSE("x=%u, y=%u", x, y); if (!spice.scInputs.connected) { DEBUG_ERROR("not connected"); return false; } SpiceMsgcMousePosition msg; msg.x = x; msg.y = y; msg.button_state = spice.mouse.buttonState; msg.display_id = 0; __sync_fetch_and_add(&spice.mouse.sentCount, 1); return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_POSITION, &msg, sizeof(msg)); } // ============================================================================ bool spice_mouse_motion(int32_t x, int32_t y) { DEBUG_MOUSE("x=%d, y=%d", x, y); if (!spice.scInputs.connected) { DEBUG_ERROR("not connected"); return false; } SpiceMsgcMouseMotion msg; msg.x = x; msg.y = y; msg.button_state = spice.mouse.buttonState; __sync_fetch_and_add(&spice.mouse.sentCount, 1); return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_MOTION, &msg, sizeof(msg)); } // ============================================================================ bool spice_mouse_press(uint32_t button) { DEBUG_MOUSE("%u", button); if (!spice.scInputs.connected) { DEBUG_ERROR("not connected"); return false; } switch(button) { case SPICE_MOUSE_BUTTON_LEFT : spice.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_LEFT ; break; case SPICE_MOUSE_BUTTON_MIDDLE: spice.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_MIDDLE; break; case SPICE_MOUSE_BUTTON_RIGHT : spice.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_RIGHT ; break; } SpiceMsgcMousePress msg; msg.button = button; msg.button_state = spice.mouse.buttonState; return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_PRESS, &msg, sizeof(msg)); } // ============================================================================ bool spice_mouse_release(uint32_t button) { DEBUG_MOUSE("%u", button); if (!spice.scInputs.connected) { DEBUG_ERROR("not connected"); return false; } switch(button) { case SPICE_MOUSE_BUTTON_LEFT : spice.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_LEFT ; break; case SPICE_MOUSE_BUTTON_MIDDLE: spice.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_MIDDLE; break; case SPICE_MOUSE_BUTTON_RIGHT : spice.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_RIGHT ; break; } SpiceMsgcMouseRelease msg; msg.button = button; msg.button_state = spice.mouse.buttonState; return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_RELEASE, &msg, sizeof(msg)); } // ============================================================================ static uint32_t spice_type_to_agent_type(SpiceDataType type) { switch(type) { case SPICE_DATA_TEXT: return VD_AGENT_CLIPBOARD_UTF8_TEXT ; break; case SPICE_DATA_PNG : return VD_AGENT_CLIPBOARD_IMAGE_PNG ; break; case SPICE_DATA_BMP : return VD_AGENT_CLIPBOARD_IMAGE_BMP ; break; case SPICE_DATA_TIFF: return VD_AGENT_CLIPBOARD_IMAGE_TIFF; break; case SPICE_DATA_JPEG: return VD_AGENT_CLIPBOARD_IMAGE_JPG ; break; default: DEBUG_ERROR("unsupported spice data type specified"); return VD_AGENT_CLIPBOARD_NONE; } } static SpiceDataType agent_type_to_spice_type(uint32_t type) { switch(type) { case VD_AGENT_CLIPBOARD_UTF8_TEXT : return SPICE_DATA_TEXT; break; case VD_AGENT_CLIPBOARD_IMAGE_PNG : return SPICE_DATA_PNG ; break; case VD_AGENT_CLIPBOARD_IMAGE_BMP : return SPICE_DATA_BMP ; break; case VD_AGENT_CLIPBOARD_IMAGE_TIFF: return SPICE_DATA_TIFF; break; case VD_AGENT_CLIPBOARD_IMAGE_JPG : return SPICE_DATA_JPEG; break; default: DEBUG_ERROR("unsupported agent data type specified"); return SPICE_DATA_NONE; } } // ============================================================================ bool spice_clipboard_request(SpiceDataType type) { VDAgentClipboardRequest req; if (!spice.cbAgentGrabbed) { DEBUG_ERROR("the agent has not grabbed any data yet"); return false; } if (type != spice.cbType) { DEBUG_ERROR("data type requested doesn't match reported data type"); return false; } req.type = spice_type_to_agent_type(type); if (!spice_agent_write_msg(VD_AGENT_CLIPBOARD_REQUEST, &req, sizeof(req))) { DEBUG_ERROR("failed to request clipboard data"); return false; } return true; } // ============================================================================ bool spice_set_clipboard_cb(SpiceClipboardNotice cbNoticeFn, SpiceClipboardData cbDataFn, SpiceClipboardRelease cbReleaseFn, SpiceClipboardRequest cbRequestFn) { if ((cbNoticeFn && !cbDataFn) || (cbDataFn && !cbNoticeFn)) { DEBUG_ERROR("clipboard callback notice and data callbacks must be specified"); return false; } spice.cbNoticeFn = cbNoticeFn; spice.cbDataFn = cbDataFn; spice.cbReleaseFn = cbReleaseFn; spice.cbRequestFn = cbRequestFn; return true; } // ============================================================================ bool spice_clipboard_grab(SpiceDataType type) { if (type == SPICE_DATA_NONE) { DEBUG_ERROR("grab type is invalid"); return false; } if (spice.cbSelection) { uint8_t req[8] = { VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD }; ((uint32_t*)req)[1] = spice_type_to_agent_type(type); if (!spice_agent_write_msg(VD_AGENT_CLIPBOARD_GRAB, req, sizeof(req))) { DEBUG_ERROR("failed to grab the clipboard"); return false; } spice.cbClientGrabbed = true; return true; } uint32_t req = spice_type_to_agent_type(type); if (!spice_agent_write_msg(VD_AGENT_CLIPBOARD_GRAB, &req, sizeof(req))) { DEBUG_ERROR("failed to grab the clipboard"); return false; } spice.cbClientGrabbed = true; return true; } // ============================================================================ bool spice_clipboard_release() { // check if if there is anything to release first if (!spice.cbClientGrabbed) return true; if (spice.cbSelection) { uint8_t req[4] = { VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD }; if (!spice_agent_write_msg(VD_AGENT_CLIPBOARD_RELEASE, req, sizeof(req))) { DEBUG_ERROR("failed to release the clipboard"); return false; } spice.cbClientGrabbed = false; return true; } if (!spice_agent_write_msg(VD_AGENT_CLIPBOARD_RELEASE, NULL, 0)) { DEBUG_ERROR("failed to release the clipboard"); return false; } spice.cbClientGrabbed = false; return true; } // ============================================================================ bool spice_clipboard_data(SpiceDataType type, uint8_t * data, size_t size) { uint8_t * buffer; size_t bufSize; if (spice.cbSelection) { bufSize = 8 + size; buffer = (uint8_t *)malloc(bufSize); buffer[0] = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; buffer[1] = buffer[2] = buffer[3] = 0; ((uint32_t*)buffer)[1] = spice_type_to_agent_type(type); memcpy(buffer + 8, data, size); } else { bufSize = 4 + size; buffer = (uint8_t *)malloc(bufSize); ((uint32_t*)buffer)[0] = spice_type_to_agent_type(type); memcpy(buffer + 4, data, size); } if (!spice_agent_write_msg(VD_AGENT_CLIPBOARD, buffer, bufSize)) { DEBUG_ERROR("failed to write the clipboard data"); free(buffer); return false; } free(buffer); return true; }