#include #include #include #include #include #include #include namespace crossdesk { namespace { using json = nlohmann::json; void ResetHostInfo(HostInfo& info) { info.host_name[0] = '\0'; info.host_name_size = 0; info.app_version[0] = '\0'; info.platform = HostPlatform::Unknown; info.display_list = nullptr; info.display_num = 0; info.left = nullptr; info.top = nullptr; info.right = nullptr; info.bottom = nullptr; info.supports_privacy_screen = false; info.supports_video_settings = false; } bool AllocateHostDisplays(HostInfo& info, std::size_t count) { if (count == 0) return true; info.display_list = static_cast(std::calloc(count, sizeof(char*))); info.left = static_cast(std::malloc(count * sizeof(int))); info.top = static_cast(std::malloc(count * sizeof(int))); info.right = static_cast(std::malloc(count * sizeof(int))); info.bottom = static_cast(std::malloc(count * sizeof(int))); return info.display_list && info.left && info.top && info.right && info.bottom; } char* DuplicateString(const std::string& value) { char* copy = static_cast(std::malloc(value.size() + 1)); if (copy == nullptr) return nullptr; std::memcpy(copy, value.data(), value.size()); copy[value.size()] = '\0'; return copy; } } // namespace const char* HostPlatformName(HostPlatform platform) { switch (platform) { case HostPlatform::Windows: return "windows"; case HostPlatform::MacOS: return "macos"; case HostPlatform::Linux: return "linux"; case HostPlatform::IOS: return "ios"; case HostPlatform::Android: return "android"; default: return "unknown"; } } HostPlatform ParseHostPlatform(const std::string& platform) { if (platform == "windows") return HostPlatform::Windows; if (platform == "macos") return HostPlatform::MacOS; if (platform == "linux") return HostPlatform::Linux; if (platform == "ios") return HostPlatform::IOS; if (platform == "android") return HostPlatform::Android; return HostPlatform::Unknown; } RemoteAction MakeHostInformation(const std::string& host_name, const std::vector& displays, bool supports_privacy_screen, const std::string& app_version, HostPlatform platform) { RemoteAction action{}; action.type = ControlType::host_infomation; ResetHostInfo(action.i); const std::size_t count = displays.size(); if (AllocateHostDisplays(action.i, count)) { action.i.display_num = count; for (std::size_t index = 0; index < count; ++index) { const HostDisplay& display = displays[index]; action.i.display_list[index] = DuplicateString(display.name); action.i.left[index] = display.left; action.i.top[index] = display.top; action.i.right[index] = display.right; action.i.bottom[index] = display.bottom; } } else { FreeRemoteAction(action); } action.i.supports_privacy_screen = supports_privacy_screen; action.i.platform = platform; const std::size_t name_size = std::min(host_name.size(), sizeof(action.i.host_name) - 1); std::memcpy(action.i.host_name, host_name.data(), name_size); action.i.host_name[name_size] = '\0'; action.i.host_name_size = name_size; // Never advertise a truncated version as a different, valid release. if (app_version.size() < sizeof(action.i.app_version) && app_version.find('\0') == std::string::npos) { std::memcpy(action.i.app_version, app_version.c_str(), app_version.size() + 1); } return action; } std::string RemoteAction::to_json() const { return ToJson(*this); } bool RemoteAction::from_json(const std::string& json_string) { RemoteAction temporary{}; if (!FromJson(json_string, temporary)) return false; *this = temporary; return true; } std::string RemoteAction::ToJson(const RemoteAction& action) { if (action.type == ControlType::invalid) return {}; json object; object["type"] = action.type; switch (action.type) { case ControlType::mouse: object["mouse"] = {{"x", action.m.x}, {"y", action.m.y}, {"s", action.m.s}, {"flag", action.m.flag}}; break; case ControlType::keyboard: object["keyboard"] = {{"key_value", action.k.key_value}, {"scan_code", action.k.scan_code}, {"extended", action.k.extended}, {"flag", action.k.flag}}; break; case ControlType::keyboard_state: { json keys = json::array(); const std::size_t pressed_count = std::min(action.ks.pressed_count, kMaxKeyboardStateKeys); for (std::size_t index = 0; index < pressed_count; ++index) { keys.push_back( {{"key_value", action.ks.pressed_keys[index].key_value}, {"scan_code", action.ks.pressed_keys[index].scan_code}, {"extended", action.ks.pressed_keys[index].extended}}); } object["keyboard_state"] = {{"seq", action.ks.seq}, {"pressed_keys", keys}}; break; } case ControlType::cursor_state: object["cursor_state"] = {{"seq", action.cs.seq}, {"visible", action.cs.visible}, {"shape", static_cast(action.cs.shape)}, {"position_valid", action.cs.position_valid}, {"x", action.cs.x}, {"y", action.cs.y}, {"visual_offset_x", action.cs.visual_offset_x}, {"visual_offset_y", action.cs.visual_offset_y}, {"display_id", action.cs.display_id}, {"position_update", action.cs.position_update}, {"render_mode", static_cast(action.cs.render_mode)}, {"hidden_reason", static_cast(action.cs.hidden_reason)}}; break; case ControlType::video_settings: case ControlType::video_settings_status: if (!ValidVideoSettings(action.vs)) return {}; object["video_settings"] = {{"quality", action.vs.quality}, {"frame_rate", action.vs.frame_rate}, {"preference", action.vs.preference}, {"request_id", action.vs.request_id}, {"accepted", action.vs.accepted}}; break; case ControlType::audio_capture: object["audio_capture"] = action.a; break; case ControlType::display_id: object["display_id"] = action.d; break; case ControlType::service_status: object["service_status"] = {{"available", action.ss.available}, {"interactive_stage", action.ss.interactive_stage}}; break; case ControlType::service_command: object["service_command"] = {{"flag", action.c.flag}}; break; case ControlType::privacy_command: object["privacy_command"] = {{"flag", action.pc.flag}, {"block_local_input", action.pc.block_local_input}}; break; case ControlType::privacy_status: object["privacy_status"] = {{"state", action.ps.state}, {"supported", action.ps.supported}, {"input_block_supported", action.ps.input_block_supported}, {"overlay_active", action.ps.overlay_active}, {"input_blocked", action.ps.input_blocked}, {"remote_paused", action.ps.remote_paused}, {"revision", action.ps.revision}, {"reason", action.ps.reason}}; break; case ControlType::host_infomation: { json displays = json::array(); for (std::size_t index = 0; index < action.i.display_num; ++index) { displays.push_back( {{"name", action.i.display_list ? action.i.display_list[index] : ""}, {"left", action.i.left ? action.i.left[index] : 0}, {"top", action.i.top ? action.i.top[index] : 0}, {"right", action.i.right ? action.i.right[index] : 0}, {"bottom", action.i.bottom ? action.i.bottom[index] : 0}}); } object["host_info"] = { {"host_name", action.i.host_name}, {"app_version", action.i.app_version}, {"platform", HostPlatformName(action.i.platform)}, {"display_num", action.i.display_num}, {"displays", displays}, {"supports_privacy_screen", action.i.supports_privacy_screen}, {"supports_video_settings", action.i.supports_video_settings}}; break; } case ControlType::invalid: default: return {}; } return object.dump(); } bool RemoteAction::FromJson(const std::string& json_string, RemoteAction& output) { bool owns_host_info = false; try { const json object = json::parse(json_string); output.type = static_cast(object.at("type").get()); switch (output.type) { case ControlType::mouse: output.m.x = object.at("mouse").at("x").get(); output.m.y = object.at("mouse").at("y").get(); output.m.s = object.at("mouse").at("s").get(); output.m.flag = static_cast( object.at("mouse").at("flag").get()); break; case ControlType::keyboard: output.k.key_value = object.at("keyboard").at("key_value").get(); output.k.scan_code = object.at("keyboard").value("scan_code", 0u); output.k.extended = object.at("keyboard").value("extended", false); output.k.flag = static_cast( object.at("keyboard").at("flag").get()); break; case ControlType::keyboard_state: { const auto& keyboard_state_object = object.at("keyboard_state"); output.ks.seq = keyboard_state_object.value("seq", 0u); output.ks.pressed_count = 0; const auto keys = keyboard_state_object.value("pressed_keys", json::array()); if (!keys.is_array()) break; const std::size_t count = std::min(keys.size(), kMaxKeyboardStateKeys); for (std::size_t index = 0; index < count; ++index) { output.ks.pressed_keys[index].key_value = keys[index].at("key_value").get(); output.ks.pressed_keys[index].scan_code = keys[index].value("scan_code", 0u); output.ks.pressed_keys[index].extended = keys[index].value("extended", false); } output.ks.pressed_count = count; break; } case ControlType::cursor_state: { const auto& cursor_state_object = object.at("cursor_state"); const int shape = cursor_state_object.at("shape").get(); if (shape < static_cast(RemoteCursorShape::default_cursor) || shape > static_cast(RemoteCursorShape::nwse_resize)) { return false; } output.cs.seq = cursor_state_object.at("seq").get(); output.cs.visible = cursor_state_object.at("visible").get(); output.cs.shape = static_cast(shape); const int mode = cursor_state_object.value("render_mode", 0); const int reason = cursor_state_object.value("hidden_reason", 0); // Unknown extensions from a newer peer fall back to visible/shape. output.cs.render_mode = mode >= 0 && mode <= static_cast(CursorRenderMode::unknown) ? static_cast(mode) : CursorRenderMode::legacy; output.cs.hidden_reason = reason >= 0 && reason <= static_cast( CursorHiddenReason::secure_desktop_pending) ? static_cast(reason) : CursorHiddenReason::unspecified; output.cs.position_valid = cursor_state_object.value("position_valid", false); output.cs.x = cursor_state_object.value("x", 0.5f); output.cs.y = cursor_state_object.value("y", 0.5f); output.cs.visual_offset_x = cursor_state_object.value("visual_offset_x", 0.0f); output.cs.visual_offset_y = cursor_state_object.value("visual_offset_y", 0.0f); output.cs.display_id = cursor_state_object.value("display_id", -1); // Cursor state messages predating position-only echo suppression // always carried an authoritative position update. output.cs.position_update = cursor_state_object.value("position_update", true); if (!std::isfinite(output.cs.x) || !std::isfinite(output.cs.y) || !std::isfinite(output.cs.visual_offset_x) || !std::isfinite(output.cs.visual_offset_y)) { return false; } output.cs.x = std::clamp(output.cs.x, 0.0f, 1.0f); output.cs.y = std::clamp(output.cs.y, 0.0f, 1.0f); output.cs.visual_offset_x = std::clamp(output.cs.visual_offset_x, -1.0f, 1.0f); output.cs.visual_offset_y = std::clamp(output.cs.visual_offset_y, -1.0f, 1.0f); break; } case ControlType::video_settings: case ControlType::video_settings_status: { const auto& settings = object.at("video_settings"); for (const auto* field : {"quality", "frame_rate", "preference"}) { if (!settings.at(field).is_number_integer()) return false; const auto value = settings.at(field).get(); if (value < 0 || value > 60) return false; } output.vs = {settings.at("quality").get(), settings.at("frame_rate").get(), settings.at("preference").get()}; if (!ValidVideoSettings(output.vs)) return false; const auto& request_id = settings.at("request_id"); if (!request_id.is_number_integer() || request_id.get() < 0 || request_id.get() > UINT32_MAX) return false; output.vs.request_id = request_id.get(); output.vs.accepted = settings.value("accepted", false); break; } case ControlType::audio_capture: output.a = object.at("audio_capture").get(); break; case ControlType::display_id: output.d = object.at("display_id").get(); break; case ControlType::service_status: { const auto& service_status_object = object.at("service_status"); output.ss.available = service_status_object.value("available", false); const std::string stage = service_status_object.value( "interactive_stage", std::string()); std::strncpy(output.ss.interactive_stage, stage.c_str(), sizeof(output.ss.interactive_stage) - 1); output.ss.interactive_stage[sizeof(output.ss.interactive_stage) - 1] = '\0'; break; } case ControlType::service_command: output.c.flag = static_cast( object.at("service_command").at("flag").get()); break; case ControlType::privacy_command: { const auto& command = object.at("privacy_command"); const int flag = command.at("flag").get(); if (flag < 0 || flag > 2) return false; output.pc.flag = static_cast(flag); output.pc.block_local_input = command.at("block_local_input").get(); break; } case ControlType::privacy_status: { const auto& status = object.at("privacy_status"); const int state = status.at("state").get(); const auto reason = status.at("reason").get(); if (state < 0 || state > 5 || reason.size() >= sizeof(output.ps.reason) || reason.find('\0') != std::string::npos) return false; output.ps.state = static_cast(state); output.ps.supported = status.at("supported").get(); output.ps.input_block_supported = status.at("input_block_supported").get(); output.ps.overlay_active = status.at("overlay_active").get(); output.ps.input_blocked = status.at("input_blocked").get(); output.ps.remote_paused = status.at("remote_paused").get(); output.ps.revision = status.at("revision").get(); std::memcpy(output.ps.reason, reason.c_str(), reason.size() + 1); // Never render a contradictory success from a malformed peer. if (output.ps.state == PrivacyState::on && (!output.ps.supported || !output.ps.overlay_active || output.ps.remote_paused)) return false; break; } case ControlType::host_infomation: { ResetHostInfo(output.i); owns_host_info = true; const auto& host_info_object = object.at("host_info"); const auto platform = host_info_object.find("platform"); if (platform != host_info_object.end() && platform->is_string()) { output.i.platform = ParseHostPlatform(platform->get_ref()); } const auto app_version = host_info_object.find("app_version"); if (app_version != host_info_object.end() && app_version->is_string()) { const auto& version = app_version->get_ref(); if (version.size() < sizeof(output.i.app_version) && version.find('\0') == std::string::npos) { std::memcpy(output.i.app_version, version.c_str(), version.size() + 1); } } const auto privacy_support = host_info_object.find("supports_privacy_screen"); output.i.supports_privacy_screen = privacy_support != host_info_object.end() && privacy_support->is_boolean() && privacy_support->get(); output.i.supports_video_settings = host_info_object.value("supports_video_settings", false); const std::string host_name = host_info_object.at("host_name").get(); std::strncpy(output.i.host_name, host_name.c_str(), sizeof(output.i.host_name) - 1); output.i.host_name[sizeof(output.i.host_name) - 1] = '\0'; output.i.host_name_size = std::strlen(output.i.host_name); const auto& displays = host_info_object.at("displays"); if (!displays.is_array()) return false; output.i.display_num = host_info_object.at("display_num").get(); if (output.i.display_num != displays.size()) return false; if (!AllocateHostDisplays(output.i, output.i.display_num)) { FreeRemoteAction(output); return false; } for (std::size_t index = 0; index < output.i.display_num; ++index) { const std::string name = displays[index].at("name").get(); output.i.display_list[index] = static_cast(std::malloc(name.size() + 1)); if (!output.i.display_list[index]) { FreeRemoteAction(output); return false; } std::memcpy(output.i.display_list[index], name.c_str(), name.size() + 1); output.i.left[index] = displays[index].at("left").get(); output.i.top[index] = displays[index].at("top").get(); output.i.right[index] = displays[index].at("right").get(); output.i.bottom[index] = displays[index].at("bottom").get(); } break; } default: return false; } return true; } catch (const std::exception& exception) { if (owns_host_info) { FreeRemoteAction(output); } std::fprintf(stderr, "Failed to parse RemoteAction JSON: %s\n", exception.what()); return false; } } void FreeRemoteAction(RemoteAction& action) { if (action.type != ControlType::host_infomation) return; if (action.i.display_list) { for (std::size_t index = 0; index < action.i.display_num; ++index) { std::free(action.i.display_list[index]); } } std::free(action.i.display_list); std::free(action.i.left); std::free(action.i.top); std::free(action.i.right); std::free(action.i.bottom); ResetHostInfo(action.i); } } // namespace crossdesk