From e40c7fca98a0b4cfa4e9f950ffaf7e3d3f67ec51 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 15 Sep 2026 13:04:53 +0800 Subject: [PATCH] [fix] disable privacy screen for legacy and web clients --- .../src/gui/runtime/connection_runtime.cpp | 1 + apps/desktop/src/gui/runtime/gui_runtime.h | 3 + .../src/gui/runtime/peer_data_callbacks.cpp | 4 ++ .../src/gui/runtime/peer_event_handler.cpp | 28 ++++----- .../src/gui/runtime/privacy_runtime.cpp | 46 ++++++++++++-- .../src/gui/runtime/privacy_session_policy.h | 61 +++++++++++++++++++ .../src/gui/runtime/remote_action_codec.cpp | 1 + libs/wire/include/remote_action.h | 2 + libs/wire/src/remote_action.cpp | 10 ++- 9 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 apps/desktop/src/gui/runtime/privacy_session_policy.h diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index a2046040..63cb8385 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -146,6 +146,7 @@ void GuiRuntime::HandleServerControllerDisconnected( std::unique_lock lock(connection_status_mutex_); connection_status_.erase(remote_id); connection_host_names_.erase(remote_id); + privacy_sessions_.Disconnected(remote_id); for (const auto& [id, status] : connection_status_) { if (status != ConnectionStatus::Connected) { continue; diff --git a/apps/desktop/src/gui/runtime/gui_runtime.h b/apps/desktop/src/gui/runtime/gui_runtime.h index 09ed85a0..51e5812c 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.h +++ b/apps/desktop/src/gui/runtime/gui_runtime.h @@ -20,6 +20,7 @@ #include "privacy_controller.h" #include "runtime/gui_state.h" #include "runtime/peer_event_handler.h" +#include "runtime/privacy_session_policy.h" namespace crossdesk { @@ -63,6 +64,7 @@ class GuiRuntime : protected gui_detail::GuiState { void HandleWindowsServiceIntegration(); void HandlePrivacy(); bool IsAuthorizedController(const std::string& remote_id); + void SetControllerPrivacySupport(const std::string& remote_id, bool supported); void QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command); void CloseRemoteSession(std::shared_ptr props); @@ -94,6 +96,7 @@ class GuiRuntime : protected gui_detail::GuiState { #endif PrivacyController privacy_; + PrivacySessionPolicy privacy_sessions_; std::mutex privacy_commands_mutex_; std::deque> privacy_commands_; uint32_t last_privacy_revision_ = 0; diff --git a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp index 0aaea420..ec0e1a25 100644 --- a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp +++ b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp @@ -290,6 +290,10 @@ void PeerEventHandler::OnReceiveDataBuffer( 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); + } remote_action_codec::Free(remote_action); } } else { diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index 1df37626..ad8cb791 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -264,7 +264,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, props->remote_cursor_state_received_ = false; } { - RemoteAction remote_action; + RemoteAction remote_action{}; + remote_action.i.supports_privacy_screen = true; remote_action.i.display_num = runtime->devices_.display_info_list().size(); remote_action.i.display_list = @@ -405,20 +406,16 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, runtime->show_connection_status_window_ = true; { std::unique_lock lock(runtime->connection_status_mutex_); -#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) - if (status == ConnectionStatus::Connected && - runtime->config_center_->IsEnablePrivacyScreen() && - std::none_of(runtime->connection_status_.begin(), - runtime->connection_status_.end(), [](const auto& entry) { - return entry.second == ConnectionStatus::Connected; - })) { - // Request optional privacy without delaying connection or capture. - // Only the first controller takes over local input. A later join must - // not release an existing controller's keys or re-enable privacy that - // the current session explicitly turned off. - runtime->privacy_.EnableOnConnection(); + if (status == ConnectionStatus::Connected) { + // Wait for explicit desktop capability before enabling privacy or + // sending privacy messages. Legacy and web controllers never opt in. + runtime->privacy_sessions_.Connected( + remote_id, runtime->config_center_->IsEnablePrivacyScreen()); + if (!runtime->privacy_sessions_.CanEnable() && + runtime->privacy_.Engaged()) { + runtime->privacy_.Disable(); + } } -#endif runtime->connection_status_[remote_id] = status; } @@ -428,7 +425,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, runtime->last_windows_service_status_tick_ = 0; #endif { - RemoteAction remote_action; + RemoteAction remote_action{}; + remote_action.i.supports_privacy_screen = true; remote_action.i.display_num = runtime->devices_.display_info_list().size(); remote_action.i.display_list = diff --git a/apps/desktop/src/gui/runtime/privacy_runtime.cpp b/apps/desktop/src/gui/runtime/privacy_runtime.cpp index 0083eb13..9a8644c7 100644 --- a/apps/desktop/src/gui/runtime/privacy_runtime.cpp +++ b/apps/desktop/src/gui/runtime/privacy_runtime.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */ +#include #include #include "rd_log.h" @@ -20,9 +21,29 @@ bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) { found->second == ConnectionStatus::Connected; } +void GuiRuntime::SetControllerPrivacySupport(const std::string& remote_id, + bool supported) { + std::unique_lock lock(connection_status_mutex_); + const auto found = connection_status_.find(remote_id); + if (found == connection_status_.end() || + found->second != ConnectionStatus::Connected) return; + + const bool automatic_enable = + privacy_sessions_.SetSupported(remote_id, supported); + if (!privacy_sessions_.CanEnable()) { + if (privacy_.Engaged()) privacy_.Disable(); + } else if (automatic_enable && config_center_->IsEnablePrivacyScreen()) { + privacy_.EnableOnConnection(); + } +} + void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command) { - if (!IsAuthorizedController(remote_id)) return; + std::shared_lock session_lock(connection_status_mutex_); + const auto found = connection_status_.find(remote_id); + if (found == connection_status_.end() || + found->second != ConnectionStatus::Connected || + !privacy_sessions_.Supports(remote_id)) return; std::lock_guard lock(privacy_commands_mutex_); if (privacy_commands_.size() < 32) privacy_commands_.emplace_back(remote_id, command); @@ -35,15 +56,22 @@ void GuiRuntime::HandlePrivacy() { commands.swap(privacy_commands_); } for (const auto& [remote_id, command] : commands) { - if (!IsAuthorizedController(remote_id)) continue; + // Keep admission and activation under the connection lock so an unknown + // controller cannot join between the capability check and enabling. + std::shared_lock lock(connection_status_mutex_); + const auto found = connection_status_.find(remote_id); + if (found == connection_status_.end() || + found->second != ConnectionStatus::Connected || + !privacy_sessions_.Supports(remote_id)) continue; if (command.flag == PrivacyCommandFlag::enable) { - privacy_.Enable(command.block_local_input); + if (privacy_sessions_.CanEnable()) + privacy_.Enable(command.block_local_input); } else if (command.flag == PrivacyCommandFlag::disable) { privacy_.Disable(); } last_privacy_status_tick_ = 0; } - const auto status = privacy_.Snapshot(); + auto status = privacy_.Snapshot(); const uint64_t now = SDL_GetTicks(); if (!peer_ || (last_privacy_status_tick_ != 0 && status.revision == last_privacy_revision_ && @@ -53,7 +81,15 @@ void GuiRuntime::HandlePrivacy() { { std::shared_lock lock(connection_status_mutex_); for (const auto& [id, state] : connection_status_) - if (state == ConnectionStatus::Connected) controllers.push_back(id); + if (state == ConnectionStatus::Connected && privacy_sessions_.Supports(id)) + controllers.push_back(id); + if (!privacy_sessions_.CanEnable()) { + status.state = PrivacyState::unsupported; + status.supported = false; + status.input_block_supported = false; + std::strcpy(status.reason, + "A connected controller does not support privacy screen"); + } } RemoteAction action{}; action.type = ControlType::privacy_status; diff --git a/apps/desktop/src/gui/runtime/privacy_session_policy.h b/apps/desktop/src/gui/runtime/privacy_session_policy.h new file mode 100644 index 00000000..6acdaa97 --- /dev/null +++ b/apps/desktop/src/gui/runtime/privacy_session_policy.h @@ -0,0 +1,61 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-15 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _PRIVACY_SESSION_POLICY_H_ +#define _PRIVACY_SESSION_POLICY_H_ + +#include +#include +#include + +namespace crossdesk { + +// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the shared +// desktop, so every connected controller must explicitly support it. +class PrivacySessionPolicy { + public: + void Connected(const std::string& remote_id, bool automatic_privacy) { + const auto [entry, inserted] = controllers_.try_emplace(remote_id, false); + if (inserted && controllers_.size() == 1 && automatic_privacy) { + automatic_controller_ = remote_id; + } + } + + void Disconnected(const std::string& remote_id) { + controllers_.erase(remote_id); + if (automatic_controller_ == remote_id) automatic_controller_.clear(); + } + + // Consume automatic activation once, on the first controller's host info. + // Repeated metadata or a later join must not undo an explicit privacy exit. + bool SetSupported(const std::string& remote_id, bool supported) { + const auto entry = controllers_.find(remote_id); + if (entry == controllers_.end()) return false; + entry->second = supported && remote_id.find("web") == std::string::npos; + if (automatic_controller_ != remote_id) return false; + automatic_controller_.clear(); + return CanEnable(); + } + + bool Supports(const std::string& remote_id) const { + const auto entry = controllers_.find(remote_id); + return entry != controllers_.end() && entry->second; + } + + bool CanEnable() const { + return !controllers_.empty() && + std::all_of(controllers_.begin(), controllers_.end(), + [](const auto& entry) { return entry.second; }); + } + + private: + std::unordered_map controllers_; + std::string automatic_controller_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/runtime/remote_action_codec.cpp b/apps/desktop/src/gui/runtime/remote_action_codec.cpp index e9dea988..16cc6fb9 100644 --- a/apps/desktop/src/gui/runtime/remote_action_codec.cpp +++ b/apps/desktop/src/gui/runtime/remote_action_codec.cpp @@ -55,6 +55,7 @@ bool Deserialize(const char *data, size_t size, RemoteAction &out) { out.type = static_cast(data[offset++]); if (out.type == ControlType::host_infomation) { + out.i.supports_privacy_screen = false; size_t name_len; if (!read(&name_len, sizeof(size_t)) || name_len >= sizeof(out.i.host_name)) { diff --git a/libs/wire/include/remote_action.h b/libs/wire/include/remote_action.h index 7a26d35f..395d0e71 100644 --- a/libs/wire/include/remote_action.h +++ b/libs/wire/include/remote_action.h @@ -100,6 +100,8 @@ struct HostInfo { int* top; int* right; int* bottom; + // Optional capability; absent in legacy desktop and web host information. + bool supports_privacy_screen; }; struct ServiceStatus { diff --git a/libs/wire/src/remote_action.cpp b/libs/wire/src/remote_action.cpp index ba8b48ba..5d1f46d4 100644 --- a/libs/wire/src/remote_action.cpp +++ b/libs/wire/src/remote_action.cpp @@ -22,6 +22,7 @@ void ResetHostInfo(HostInfo& info) { info.top = nullptr; info.right = nullptr; info.bottom = nullptr; + info.supports_privacy_screen = false; } bool AllocateHostDisplays(HostInfo& info, std::size_t count) { @@ -132,7 +133,9 @@ std::string RemoteAction::ToJson(const RemoteAction& action) { } object["host_info"] = {{"host_name", action.i.host_name}, {"display_num", action.i.display_num}, - {"displays", displays}}; + {"displays", displays}, + {"supports_privacy_screen", + action.i.supports_privacy_screen}}; break; } case ControlType::invalid: @@ -272,6 +275,11 @@ bool RemoteAction::FromJson(const std::string& json_string, ResetHostInfo(output.i); owns_host_info = true; const auto& host_info_object = object.at("host_info"); + 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(); const std::string host_name = host_info_object.at("host_name").get(); std::strncpy(output.i.host_name, host_name.c_str(),