#include #include "runtime/peer_event_handler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "device_controller.h" #include "file_transfer.h" #include "localization.h" #include "filesystem_utf8.h" #include "platform.h" #include "rd_log.h" #include "runtime/gui_runtime.h" #if _WIN32 #include "desktop_transition_policy.h" #include "interactive_desktop.h" #include "service_host.h" #include "unattended_config.h" #endif namespace crossdesk { namespace { #if _WIN32 constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; bool BuildAbsoluteMousePosition(const std::vector &displays, int display_index, float normalized_x, float normalized_y, int *absolute_x_out, int *absolute_y_out) { if (absolute_x_out == nullptr || absolute_y_out == nullptr || display_index < 0 || display_index >= static_cast(displays.size())) { return false; } const DisplayInfo &display = displays[display_index]; if (display.width <= 0 || display.height <= 0) { return false; } const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f); const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f); *absolute_x_out = static_cast(clamped_x * display.width) + display.left; *absolute_y_out = static_cast(clamped_y * display.height) + display.top; return true; } void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side, const char *stage) { if (last_tick == nullptr) { return; } const uint32_t now = static_cast(SDL_GetTicks()); if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) { return; } *last_tick = now; LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path " "cannot drive the Windows password UI", side != nullptr ? side : "unknown", stage != nullptr ? stage : ""); } #endif } // namespace void PeerEventHandler::OnReceiveDataBuffer( const char *data, size_t size, const char *user_id, size_t user_id_size, const char *src_id, size_t src_id_size, void *user_data) { auto *handler = static_cast(user_data); if (!handler) return; auto callback = handler->EnterCallback(); if (!callback) return; GuiRuntime *runtime = handler ? &handler->owner_ : nullptr; if (!runtime) { return; } std::string source_id = std::string(src_id, src_id_size); // The service-owned host runs as SYSTEM. Do not expose filesystem or // clipboard operations in that security context. if (runtime->unattended_host_ && (source_id == runtime->file_label_ || source_id == runtime->file_feedback_label_ || source_id == runtime->clipboard_label_)) return; if (source_id == runtime->file_label_) { std::string remote_user_id = std::string(user_id, user_id_size); static FileReceiver receiver; // Update output directory from config std::string configured_path = runtime->config_center_->GetFileTransferSavePath(); if (!configured_path.empty()) { receiver.SetOutputDir(PathFromUtf8(configured_path)); } else if (receiver.OutputDir().empty()) { receiver = FileReceiver(); // re-init with default desktop path } receiver.SetOnSendAck([runtime, remote_user_id](const FileTransferAck &ack) -> int { const auto encoded_ack = EncodeFileTransferAck(ack); bool is_server_sending = remote_user_id.rfind("C-", 0) != 0; if (is_server_sending) { auto props = runtime->FindRemoteSession(remote_user_id); if (props) { PeerPtr *peer = props->peer_; return SendReliableDataFrame( peer, encoded_ack.data(), encoded_ack.size(), runtime->file_feedback_label_.c_str()); } } return SendReliableDataFrame( runtime->peer_, encoded_ack.data(), encoded_ack.size(), runtime->file_feedback_label_.c_str()); }); receiver.OnData(data, size); return; } else if (source_id == runtime->clipboard_label_) { if (size > 0) { std::string remote_user_id(user_id, user_id_size); auto props = runtime->FindRemoteSession(remote_user_id); if (props && !props->clipboard_allowed_.load()) { return; } runtime->clipboard_.QueueRemoteText( data, size, props ? remote_user_id : std::string{}); } return; } else if (source_id == runtime->file_feedback_label_) { runtime->transfers_.HandleAck(data, size); return; } const std::string remote_id(user_id, user_id_size); std::string json_str(data, size); const auto preview_json = json_str.find("\"deskhub_preview") != std::string::npos ? nlohmann::json::parse(json_str, nullptr, false) : nlohmann::json{}; if (preview_json.is_object() && preview_json.value("type", nlohmann::json{}) == "deskhub_preview") { if (source_id != runtime->control_data_label_ || !runtime->IsAuthorizedController(remote_id)) return; const auto subscription = PreviewSubscription::Parse(preview_json); if (!subscription) return; const auto previous = runtime->preview_subscriptions_.Get(remote_id); bool accepted = true; // Apply encoder quality to this controller only; other controllers retain // their own demand. MiniRTC owns per-peer video/audio pause state. if (!previous || subscription->IsNewerThan(*previous)) { if (subscription->fps > 0) accepted = UpdateVideoSettings( runtime->peer_, remote_id.data(), remote_id.size(), static_cast(subscription->quality), subscription->frame_rate, static_cast( subscription->EncoderPreference())) == 0; if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) != subscription->max_bitrate_bps.value_or(0))) accepted = UpdateVideoBitrateLimit( runtime->peer_, remote_id.data(), remote_id.size(), subscription->max_bitrate_bps.value_or(0)) == 0; // Rebalancing bandwidth alone must not force every device to send a // keyframe. The pause API requests its own keyframes on resume. if (accepted && (!previous || previous->width != subscription->width || previous->height != subscription->height)) accepted = UpdateVideoResolution( runtime->peer_, remote_id.data(), remote_id.size(), subscription->width, subscription->height) == 0; if (accepted) accepted = SetVideoSendPaused(runtime->peer_, remote_id.data(), remote_id.size(), subscription->fps == 0) == 0; if (accepted) accepted = SetAudioSendPaused(runtime->peer_, remote_id.data(), remote_id.size(), !subscription->audio) == 0; if (accepted && subscription->audio) runtime->devices_.StartSpeakerCapturer(); } if (accepted) runtime->preview_subscriptions_.Set(remote_id, *subscription); const auto reply = nlohmann::json{ {"type", "deskhub_preview_status"}, {"version", subscription->version}, {"request_id", subscription->request_id}, {"accepted", accepted}}.dump(); SendReliableDataFrameToPeer(runtime->peer_, reply.data(), reply.size(), runtime->control_data_label_.c_str(), remote_id.data(), remote_id.size()); return; } if (preview_json.is_object() && preview_json.value("type", nlohmann::json{}) == "deskhub_preview_status") { if (auto props = runtime->FindRemoteSession(remote_id); props && source_id == props->control_data_label_) { try { if ((preview_json.at("version") == 1 || preview_json.at("version") == 2) && preview_json.at("request_id").is_number_unsigned() && preview_json.at("request_id").get() > 0 && preview_json.at("request_id").get() <= UINT32_MAX && preview_json.at("accepted").is_boolean()) { props->preview_accepted_ = preview_json.at("accepted").get(); props->preview_ack_ = preview_json.at("request_id").get(); } } catch (...) { } } return; } RemoteAction remote_action{}; if (!remote_action.from_json(json_str)) { LOG_ERROR("Failed to parse RemoteAction JSON payload"); return; } if (remote_action.type == ControlType::video_settings_status) { if (auto props = runtime->FindRemoteSession(remote_id)) { if (source_id != props->control_data_label_) return; std::lock_guard lock(props->video_settings_mutex_); if (!props->video_settings_supported_) return; if (remote_action.vs.accepted && static_cast(remote_action.vs.request_id - props->applied_video_settings_.request_id) >= 0) { props->applied_video_settings_ = remote_action.vs; } if (remote_action.vs.request_id != props->video_settings_.request_id) return; props->video_settings_pending_ = false; props->video_settings_failed_ = !remote_action.vs.accepted; if (remote_action.vs.accepted) { props->video_settings_ = remote_action.vs; props->applied_video_settings_ = remote_action.vs; } else { const auto request_id = props->video_settings_.request_id; props->video_settings_ = props->applied_video_settings_; props->video_settings_.request_id = request_id; } } return; } if (remote_action.type == ControlType::video_settings) { if (source_id != runtime->control_data_label_ || !runtime->IsAuthorizedController(remote_id)) return; const auto& settings = remote_action.vs; remote_action.vs.accepted = UpdateVideoSettings( runtime->peer_, remote_id.data(), remote_id.size(), static_cast(settings.quality), settings.frame_rate, static_cast(settings.preference)) == 0; remote_action.type = ControlType::video_settings_status; const auto message = remote_action.to_json(); SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(), runtime->control_data_label_.c_str(), remote_id.data(), remote_id.size()); return; } if (remote_action.type == ControlType::privacy_status) { if (auto props = runtime->FindRemoteSession(remote_id)) { if (source_id != props->control_data_label_) return; std::lock_guard lock(props->privacy_status_mutex_); if (!props->privacy_status_received_ || static_cast(remote_action.ps.revision - props->privacy_status_.revision) >= 0) { props->privacy_status_ = remote_action.ps; props->privacy_status_received_ = true; props->privacy_status_tick_ = SDL_GetTicks(); if (static_cast(remote_action.ps.revision - props->privacy_request_revision_) > 0 && remote_action.ps.state != PrivacyState::starting && remote_action.ps.state != PrivacyState::stopping) props->privacy_command_pending_ = false; } } return; } if (remote_action.type == ControlType::privacy_command) { if (runtime->unattended_host_) return; if (source_id == runtime->control_data_label_) runtime->QueuePrivacyCommand(remote_id, remote_action.pc); return; } const bool operates_host = remote_action.type == ControlType::mouse || remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state || remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id || remote_action.type == ControlType::audio_capture; if (operates_host) { if (!runtime->IsAuthorizedController(remote_id)) return; #ifdef _WIN32 // KeyboardController checks the desktop immediately before each native // key injection, including state reconciliation and cleanup. Avoid a // second synchronous session query for every received keyboard packet. const bool keyboard_input = remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state; if (!keyboard_input && runtime->privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) { runtime->privacy_.SuspendForDesktop(); } #endif } if (remote_action.type == ControlType::service_status) { if (auto props = runtime->FindRemoteSession(remote_id)) { runtime->ApplyRemoteServiceStatus(*props, remote_action.ss); } return; } if (remote_action.type == ControlType::service_command) { if (runtime->privacy_.Engaged()) { runtime->privacy_.Fail("Security shortcut requested; privacy screen will turn off"); } #if _WIN32 if (remote_action.c.flag == ServiceCommandFlag::send_sas) { runtime->pending_windows_service_sas_.store(true, std::memory_order_relaxed); } else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) { if (!LockWorkStation()) { LOG_WARN("Remote lock workstation request failed, error={}", GetLastError()); } } #endif return; } if (remote_action.type == ControlType::cursor_state) { std::shared_ptr props; { std::shared_lock lock(runtime->remote_sessions_mutex_); auto props_it = runtime->remote_sessions_.find(remote_id); if (props_it != runtime->remote_sessions_.end()) { props = props_it->second; } } if (!props) { return; } std::lock_guard lock(props->remote_cursor_state_mutex_); const uint32_t previous_seq = props->remote_cursor_state_.seq; const bool is_newer = !props->remote_cursor_state_received_ || static_cast(remote_action.cs.seq - previous_seq) > 0; if (is_newer) { const bool changed = !props->remote_cursor_state_received_ || props->remote_cursor_state_.visible != remote_action.cs.visible || props->remote_cursor_state_.shape != remote_action.cs.shape || props->remote_cursor_state_.render_mode != remote_action.cs.render_mode || props->remote_cursor_state_.hidden_reason != remote_action.cs.hidden_reason; CursorState merged = remote_action.cs; if (!remote_action.cs.position_update && props->remote_cursor_state_received_) { merged.position_valid = props->remote_cursor_state_.position_valid; merged.x = props->remote_cursor_state_.x; merged.y = props->remote_cursor_state_.y; merged.display_id = props->remote_cursor_state_.display_id; } props->remote_cursor_state_ = merged; props->remote_cursor_state_received_ = true; props->cursor_presentation_.Observe(merged, SDL_GetTicks()); if (changed) { LOG_DEBUG("Received cursor state: seq={}, visible={}, shape={}, " "mode={}, reason={}", remote_action.cs.seq, remote_action.cs.visible, static_cast(remote_action.cs.shape), static_cast(remote_action.cs.render_mode), static_cast(remote_action.cs.hidden_reason)); } } return; } if (remote_action.type == ControlType::host_infomation) { bool is_client_mode = false; std::shared_ptr props; { std::shared_lock lock(runtime->remote_sessions_mutex_); auto props_it = runtime->remote_sessions_.find(remote_id); if (props_it != runtime->remote_sessions_.end()) { is_client_mode = true; props = props_it->second; } } if (is_client_mode) { if (props && source_id == props->control_data_label_) { std::lock_guard lock(props->remote_version_mutex_); props->remote_host_info_received_ = true; props->remote_platform_ = remote_action.i.platform; props->recent_platform_ = remote_action.i.platform; props->remote_app_version_ = IsValidAppVersion(remote_action.i.app_version) ? remote_action.i.app_version : ""; } // Negotiate live settings only on the reliable control stream. Old // hosts omit this capability and retain their existing video behavior. if (props && source_id == props->control_data_label_) { std::lock_guard lock(props->video_settings_mutex_); const bool first_advertisement = !props->video_settings_supported_; props->preview_subscription_supported_ = remote_action.i.supports_deskhub_preview; props->preview_resolution_supported_ = remote_action.i.supports_deskhub_resolution; props->preview_fps15_supported_ = remote_action.i.supports_deskhub_fps15; props->preview_bandwidth_supported_ = remote_action.i.supports_deskhub_bandwidth; props->video_settings_supported_ = remote_action.i.supports_video_settings; if (first_advertisement && props->video_settings_supported_) { RemoteAction settings{}; settings.type = ControlType::video_settings; ++props->video_settings_.request_id; props->video_settings_pending_ = true; props->video_settings_request_tick_ = SDL_GetTicks(); settings.vs = props->video_settings_; const auto message = settings.to_json(); SendReliableDataFrame(props->peer_, message.data(), message.size(), props->control_data_label_.c_str()); } } // client mode if (props) { std::lock_guard lock(props->remote_version_mutex_); if (props->remote_host_name_.empty()) { props->remote_host_name_ = std::string(remote_action.i.host_name, remote_action.i.host_name_size); LOG_INFO("Remote hostname: [{}]", props->remote_host_name_); for (int i = 0; i < remote_action.i.display_num; i++) { props->display_info_list_.push_back( DisplayInfo(remote_action.i.display_list[i], remote_action.i.left[i], remote_action.i.top[i], remote_action.i.right[i], remote_action.i.bottom[i])); } } props->recent_connection_dirty_ = true; } FreeRemoteAction(remote_action); } else { // server mode std::string host_name(remote_action.i.host_name, remote_action.i.host_name_size); { std::unique_lock lock(runtime->connection_status_mutex_); runtime->connection_host_names_[remote_id] = host_name; } LOG_INFO("Remote hostname: [{}]", host_name); if (source_id == runtime->control_data_label_) { runtime->SetControllerPrivacySupport( remote_id, remote_action.i.supports_privacy_screen); } FreeRemoteAction(remote_action); } } else { // remote if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) { std::lock_guard lock(runtime->remote_pointer_input_mutex_); runtime->last_remote_pointer_input_time_[remote_id] = std::chrono::steady_clock::now(); } #if _WIN32 if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) { static const bool process_elevated = IsAdministratorProcess(); std::string response; const bool delivered = DispatchDesktopInput( [&] { return PreferUserDesktopInput( IsCurrentSessionUserDesktopActive(), runtime->windows_consent_ui_.load(), runtime->local_service_available_.load(), process_elevated); }, [&] { SetLastError(ERROR_SUCCESS); const bool sent = runtime->devices_.SendMouseCommand( remote_action, runtime->selected_display_); const DWORD error = GetLastError(); const bool blocked = error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE; return DesktopInputResult{ sent, IsDesktopTransitionInputError(error) || blocked, blocked}; }, [&]() -> DesktopInputResult { int absolute_x = 0; int absolute_y = 0; if (!BuildAbsoluteMousePosition( runtime->devices_.display_info_list(), runtime->selected_display_, remote_action.m.x, remote_action.m.y, &absolute_x, &absolute_y)) { LOG_WARN( "Secure desktop mouse injection skipped, invalid display " "mapping: display_index={}, x={}, y={}", runtime->selected_display_, remote_action.m.x, remote_action.m.y); return {}; } response = runtime->devices_.SendServiceMouseCommand( absolute_x, absolute_y, remote_action.m.s, static_cast(remote_action.m.flag)); auto json = nlohmann::json::parse(response, nullptr, false); const bool sent = json.is_object() && json.value("ok", false); const std::string error = json.is_object() ? json.value("error", "") : ""; return {sent, IsDesktopInputSetupPending(error)}; }); if (!delivered && !response.empty()) { LogSecureDesktopInputBlocked( &runtime->last_local_secure_input_block_log_tick_, "local", runtime->WindowsInputStage().c_str()); LOG_WARN( "Secure desktop mouse injection failed, x={}, y={}, wheel={}, " "flag={}, response={}", remote_action.m.x, remote_action.m.y, remote_action.m.s, static_cast(remote_action.m.flag), response); } return; } #endif if (remote_action.type == ControlType::mouse) { runtime->devices_.SendMouseCommand(remote_action, runtime->selected_display_); } else if (remote_action.type == ControlType::audio_capture) { // A negotiated subscription remains authoritative if a legacy capture // command arrives after it (e.g. when unmuting an inactive window). const auto subscription = runtime->preview_subscriptions_.Get(remote_id); const bool paused = subscription ? !subscription->audio : !remote_action.a; const int ret = SetAudioSendPaused(runtime->peer_, remote_id.data(), remote_id.size(), paused); if (remote_action.a) runtime->devices_.StartSpeakerCapturer(); else if (ret != 0) // Legacy/web transports without the per-peer API retain their fallback. runtime->devices_.StopSpeakerCapturer(); } else if (remote_action.type == ControlType::keyboard) { runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action); } else if (remote_action.type == ControlType::keyboard_state) { runtime->keyboard_.ApplyRemoteState(remote_id, remote_action); } else if (remote_action.type == ControlType::display_id) { const int ret = runtime->devices_.SwitchDisplay(remote_action.d); if (ret == 0) { runtime->selected_display_ = remote_action.d; } else { LOG_WARN("Display switch skipped, invalid display_id={}", remote_action.d); } } } } } // namespace crossdesk