[fix] disable privacy screen for legacy and web clients

This commit is contained in:
dijunkun
2026-09-15 13:04:53 +08:00
parent f6324416ec
commit e40c7fca98
9 changed files with 135 additions and 21 deletions
@@ -146,6 +146,7 @@ void GuiRuntime::HandleServerControllerDisconnected(
std::unique_lock lock(connection_status_mutex_); std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id); connection_status_.erase(remote_id);
connection_host_names_.erase(remote_id); connection_host_names_.erase(remote_id);
privacy_sessions_.Disconnected(remote_id);
for (const auto& [id, status] : connection_status_) { for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) { if (status != ConnectionStatus::Connected) {
continue; continue;
@@ -20,6 +20,7 @@
#include "privacy_controller.h" #include "privacy_controller.h"
#include "runtime/gui_state.h" #include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h" #include "runtime/peer_event_handler.h"
#include "runtime/privacy_session_policy.h"
namespace crossdesk { namespace crossdesk {
@@ -63,6 +64,7 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
void HandlePrivacy(); void HandlePrivacy();
bool IsAuthorizedController(const std::string& remote_id); 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 QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
void CloseRemoteSession(std::shared_ptr<RemoteSession> props); void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
@@ -94,6 +96,7 @@ class GuiRuntime : protected gui_detail::GuiState {
#endif #endif
PrivacyController privacy_; PrivacyController privacy_;
PrivacySessionPolicy privacy_sessions_;
std::mutex privacy_commands_mutex_; std::mutex privacy_commands_mutex_;
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_; std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
uint32_t last_privacy_revision_ = 0; uint32_t last_privacy_revision_ = 0;
@@ -290,6 +290,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
runtime->connection_host_names_[remote_id] = host_name; runtime->connection_host_names_[remote_id] = host_name;
} }
LOG_INFO("Remote hostname: [{}]", 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); remote_action_codec::Free(remote_action);
} }
} else { } else {
@@ -264,7 +264,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
props->remote_cursor_state_received_ = false; props->remote_cursor_state_received_ = false;
} }
{ {
RemoteAction remote_action; RemoteAction remote_action{};
remote_action.i.supports_privacy_screen = true;
remote_action.i.display_num = remote_action.i.display_num =
runtime->devices_.display_info_list().size(); runtime->devices_.display_info_list().size();
remote_action.i.display_list = remote_action.i.display_list =
@@ -405,20 +406,16 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->show_connection_status_window_ = true; runtime->show_connection_status_window_ = true;
{ {
std::unique_lock lock(runtime->connection_status_mutex_); std::unique_lock lock(runtime->connection_status_mutex_);
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) if (status == ConnectionStatus::Connected) {
if (status == ConnectionStatus::Connected && // Wait for explicit desktop capability before enabling privacy or
runtime->config_center_->IsEnablePrivacyScreen() && // sending privacy messages. Legacy and web controllers never opt in.
std::none_of(runtime->connection_status_.begin(), runtime->privacy_sessions_.Connected(
runtime->connection_status_.end(), [](const auto& entry) { remote_id, runtime->config_center_->IsEnablePrivacyScreen());
return entry.second == ConnectionStatus::Connected; if (!runtime->privacy_sessions_.CanEnable() &&
})) { runtime->privacy_.Engaged()) {
// Request optional privacy without delaying connection or capture. runtime->privacy_.Disable();
// 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();
} }
#endif
runtime->connection_status_[remote_id] = status; runtime->connection_status_[remote_id] = status;
} }
@@ -428,7 +425,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->last_windows_service_status_tick_ = 0; runtime->last_windows_service_status_tick_ = 0;
#endif #endif
{ {
RemoteAction remote_action; RemoteAction remote_action{};
remote_action.i.supports_privacy_screen = true;
remote_action.i.display_num = remote_action.i.display_num =
runtime->devices_.display_info_list().size(); runtime->devices_.display_info_list().size();
remote_action.i.display_list = remote_action.i.display_list =
@@ -4,6 +4,7 @@
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/ */
#include <cstring>
#include <shared_mutex> #include <shared_mutex>
#include "rd_log.h" #include "rd_log.h"
@@ -20,9 +21,29 @@ bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
found->second == ConnectionStatus::Connected; 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, void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
const PrivacyCommand& command) { 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_); std::lock_guard lock(privacy_commands_mutex_);
if (privacy_commands_.size() < 32) if (privacy_commands_.size() < 32)
privacy_commands_.emplace_back(remote_id, command); privacy_commands_.emplace_back(remote_id, command);
@@ -35,15 +56,22 @@ void GuiRuntime::HandlePrivacy() {
commands.swap(privacy_commands_); commands.swap(privacy_commands_);
} }
for (const auto& [remote_id, command] : 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) { 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) { } else if (command.flag == PrivacyCommandFlag::disable) {
privacy_.Disable(); privacy_.Disable();
} }
last_privacy_status_tick_ = 0; last_privacy_status_tick_ = 0;
} }
const auto status = privacy_.Snapshot(); auto status = privacy_.Snapshot();
const uint64_t now = SDL_GetTicks(); const uint64_t now = SDL_GetTicks();
if (!peer_ || (last_privacy_status_tick_ != 0 && if (!peer_ || (last_privacy_status_tick_ != 0 &&
status.revision == last_privacy_revision_ && status.revision == last_privacy_revision_ &&
@@ -53,7 +81,15 @@ void GuiRuntime::HandlePrivacy() {
{ {
std::shared_lock lock(connection_status_mutex_); std::shared_lock lock(connection_status_mutex_);
for (const auto& [id, state] : connection_status_) 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{}; RemoteAction action{};
action.type = ControlType::privacy_status; action.type = ControlType::privacy_status;
@@ -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 <algorithm>
#include <string>
#include <unordered_map>
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<std::string, bool> controllers_;
std::string automatic_controller_;
};
} // namespace crossdesk
#endif
@@ -55,6 +55,7 @@ bool Deserialize(const char *data, size_t size, RemoteAction &out) {
out.type = static_cast<ControlType>(data[offset++]); out.type = static_cast<ControlType>(data[offset++]);
if (out.type == ControlType::host_infomation) { if (out.type == ControlType::host_infomation) {
out.i.supports_privacy_screen = false;
size_t name_len; size_t name_len;
if (!read(&name_len, sizeof(size_t)) || if (!read(&name_len, sizeof(size_t)) ||
name_len >= sizeof(out.i.host_name)) { name_len >= sizeof(out.i.host_name)) {
+2
View File
@@ -100,6 +100,8 @@ struct HostInfo {
int* top; int* top;
int* right; int* right;
int* bottom; int* bottom;
// Optional capability; absent in legacy desktop and web host information.
bool supports_privacy_screen;
}; };
struct ServiceStatus { struct ServiceStatus {
+9 -1
View File
@@ -22,6 +22,7 @@ void ResetHostInfo(HostInfo& info) {
info.top = nullptr; info.top = nullptr;
info.right = nullptr; info.right = nullptr;
info.bottom = nullptr; info.bottom = nullptr;
info.supports_privacy_screen = false;
} }
bool AllocateHostDisplays(HostInfo& info, std::size_t count) { 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}, object["host_info"] = {{"host_name", action.i.host_name},
{"display_num", action.i.display_num}, {"display_num", action.i.display_num},
{"displays", displays}}; {"displays", displays},
{"supports_privacy_screen",
action.i.supports_privacy_screen}};
break; break;
} }
case ControlType::invalid: case ControlType::invalid:
@@ -272,6 +275,11 @@ bool RemoteAction::FromJson(const std::string& json_string,
ResetHostInfo(output.i); ResetHostInfo(output.i);
owns_host_info = true; owns_host_info = true;
const auto& host_info_object = object.at("host_info"); 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<bool>();
const std::string host_name = const std::string host_name =
host_info_object.at("host_name").get<std::string>(); host_info_object.at("host_name").get<std::string>();
std::strncpy(output.i.host_name, host_name.c_str(), std::strncpy(output.i.host_name, host_name.c_str(),