diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 1a6160324..2e7247d95 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -759,9 +759,18 @@ List toolbarPrivacyMode( final ffiModel = ffi.ffiModel; final pi = ffiModel.pi; final sessionId = ffi.sessionId; + final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false; + + // Backend revocation already attempts to turn privacy mode off. + // Still keep this menu when privacy mode is active, so users can turn it off + // if there is a sync delay, version mismatch, or off attempt failure. + if (!hasPrivacyModePermission && privacyModeState.isEmpty) { + return []; // No permission and not active, hide options. + } getDefaultMenu(Future Function(SessionID sid, String opt) toggleFunc) { - final enabled = !ffi.ffiModel.viewOnly; + final enabled = + !ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty); return TToggleMenu( value: privacyModeState.isNotEmpty, onChanged: enabled @@ -810,18 +819,29 @@ List toolbarPrivacyMode( }) ]; } else { - return privacyModeImpls.map((e) { + final visibleImpls = hasPrivacyModePermission + ? privacyModeImpls + : privacyModeImpls.where((e) { + final implKey = (e as List)[0] as String; + return privacyModeState.value == implKey; + }).toList(); + return visibleImpls.map((e) { final implKey = (e as List)[0] as String; final implName = (e)[1] as String; + final enabled = !ffiModel.viewOnly && + (hasPrivacyModePermission || privacyModeState.value == implKey); return TToggleMenu( child: Text(translate(implName)), value: privacyModeState.value == implKey, - onChanged: (value) { - if (value == null) return; - togglePrivacyModeTime = DateTime.now(); - bind.sessionTogglePrivacyMode( - sessionId: sessionId, implKey: implKey, on: value); - }); + onChanged: enabled + ? (value) { + if (value == null) return; + if (value && !hasPrivacyModePermission) return; + togglePrivacyModeTime = DateTime.now(); + bind.sessionTogglePrivacyMode( + sessionId: sessionId, implKey: implKey, on: value); + } + : null); }).toList(); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 51c08cf33..832b96d24 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -114,6 +114,9 @@ const String kOptionTerminalPersistent = "terminal-persistent"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; +const String kOptionEnablePrivacyMode = "enable-privacy-mode"; +const String kOptionEnablePermChangeInAcceptWindow = + "enable-perm-change-in-accept-window"; const String kOptionAllowRemoteConfigModification = "allow-remote-config-modification"; const String kOptionVerificationMethod = "verification-method"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d118b6793..2841c1d27 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1062,6 +1062,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'Enable blocking user input', kOptionEnableBlockInput, enabled: enabled, fakeValue: fakeValue), + if (bind.mainSupportedPrivacyModeImpls() != '[]') + _OptionCheckBox( + context, 'Enable privacy mode', kOptionEnablePrivacyMode, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable remote configuration modification', kOptionAllowRemoteConfigModification, enabled: enabled, fakeValue: fakeValue), diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 7d48452a8..8bd7df08b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -610,19 +610,24 @@ class _PrivilegeBoard extends StatefulWidget { class _PrivilegeBoardState extends State<_PrivilegeBoard> { late final client = widget.client; Widget buildPermissionIcon(bool enabled, IconData iconData, - Function(bool)? onTap, String tooltipText) { + Function(bool)? onTap, String tooltipText, + {required bool canModify}) { return Tooltip( message: "$tooltipText: ${enabled ? "ON" : "OFF"}", waitDuration: Duration.zero, child: Container( decoration: BoxDecoration( - color: enabled ? MyTheme.accent : Colors.grey[700], + color: enabled + ? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6)) + : Colors.grey[700], borderRadius: BorderRadius.circular(10.0), ), padding: EdgeInsets.all(8.0), child: InkWell( - onTap: () => - checkClickTime(widget.client.id, () => onTap?.call(!enabled)), + onTap: canModify + ? () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)) + : null, child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -643,6 +648,9 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { Widget build(BuildContext context) { final crossAxisCount = 4; final spacing = 10.0; + final canModifyPermission = + bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) != + 'N'; return Container( width: double.infinity, height: 160.0, @@ -689,6 +697,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable audio'), + canModify: canModifyPermission, ), buildPermissionIcon( client.recording, @@ -703,6 +712,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable recording session'), + canModify: canModifyPermission, ), ] : [ @@ -719,6 +729,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable keyboard/mouse'), + canModify: canModifyPermission, ), buildPermissionIcon( client.clipboard, @@ -733,6 +744,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable clipboard'), + canModify: canModifyPermission, ), buildPermissionIcon( client.audio, @@ -747,6 +759,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable audio'), + canModify: canModifyPermission, ), buildPermissionIcon( client.file, @@ -761,6 +774,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable file copy and paste'), + canModify: canModifyPermission, ), buildPermissionIcon( client.restart, @@ -775,6 +789,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable remote restart'), + canModify: canModifyPermission, ), buildPermissionIcon( client.recording, @@ -789,6 +804,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable recording session'), + canModify: canModifyPermission, ), // only windows support block input if (isWindows) @@ -805,6 +821,23 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable blocking user input'), + canModify: canModifyPermission, + ), + if (bind.mainSupportedPrivacyModeImpls() != '[]') + buildPermissionIcon( + client.privacyMode, + Icons.visibility_off, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "privacy_mode", + enabled: enabled); + setState(() { + client.privacyMode = enabled; + }); + }, + translate('Enable privacy mode'), + canModify: canModifyPermission, ) ], ), diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index ec05c987f..5da253e80 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -996,10 +996,10 @@ class _DisplayMenuState extends State<_DisplayMenu> { toggles(), ]; // privacy mode + final privacyModeState = PrivacyModeState.find(id); if (ffi.connType == ConnType.defaultConn && - ffiModel.keyboard && - pi.features.privacyMode) { - final privacyModeState = PrivacyModeState.find(id); + (pi.features.privacyMode || privacyModeState.isNotEmpty) && + (ffiModel.keyboard || privacyModeState.isNotEmpty)) { final privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, ffi); if (privacyModeList.length == 1) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9064c122b..74a5af45c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1183,7 +1183,8 @@ void showOptions( List privacyModeList = []; // privacy mode final privacyModeState = PrivacyModeState.find(id); - if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) { + if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) || + privacyModeState.isNotEmpty) { privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI); if (privacyModeList.length == 1) { displayToggles.add(privacyModeList[0]); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 2c8b0f2d6..cd3f97a53 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -583,9 +583,16 @@ class _PermissionCheckerState extends State { Widget build(BuildContext context) { final serverModel = Provider.of(context); final hasAudioPermission = androidVersion >= 30; - final hideStopService = - isAndroid && - bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; + final hideStopService = isAndroid && + bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; + final allowPermChangeInAcceptWindow = option2bool( + kOptionEnablePermChangeInAcceptWindow, + bind.mainGetBuildinOption( + key: kOptionEnablePermChangeInAcceptWindow, + )); + final permissionChangeLocked = isAndroid && + serverModel.clients.any((c) => !c.disconnected) && + !allowPermChangeInAcceptWindow; return PaddingCard( title: translate("Permissions"), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -608,13 +615,21 @@ class _PermissionCheckerState extends State { bind.mainGetLocalOption(key: "show-scam-warning") != "N" ? () => showScamWarning(context, serverModel) : serverModel.toggleService), - PermissionRow(translate("Input Control"), serverModel.inputOk, - serverModel.toggleInput), - PermissionRow(translate("Transfer file"), serverModel.fileOk, - serverModel.toggleFile), + PermissionRow( + translate("Input Control"), + serverModel.inputOk, + serverModel.toggleInput, + ), + PermissionRow( + translate("Transfer file"), + serverModel.fileOk, + serverModel.toggleFile, + enabled: !permissionChangeLocked, + ), hasAudioPermission ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, - serverModel.toggleAudio) + serverModel.toggleAudio, + enabled: !permissionChangeLocked) : Row(children: [ Icon(Icons.info_outline).marginOnly(right: 15), Expanded( @@ -623,19 +638,25 @@ class _PermissionCheckerState extends State { style: const TextStyle(color: MyTheme.darkGray), )) ]), - PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, - serverModel.toggleClipboard), + PermissionRow( + translate("Enable clipboard"), + serverModel.clipboardOk, + serverModel.toggleClipboard, + enabled: !permissionChangeLocked, + ), ])); } } class PermissionRow extends StatelessWidget { - const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key}) + const PermissionRow(this.name, this.isOk, this.onPressed, + {Key? key, this.enabled = true}) : super(key: key); final String name; final bool isOk; final VoidCallback onPressed; + final bool enabled; @override Widget build(BuildContext context) { @@ -644,9 +665,11 @@ class PermissionRow extends StatelessWidget { contentPadding: EdgeInsets.all(0), title: Text(name), value: isOk, - onChanged: (bool value) { - onPressed(); - }); + onChanged: enabled + ? (bool value) { + onPressed(); + } + : null); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 78e334d4f..40c94fcf5 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier { } toggleAudio() async { - if (clients.isNotEmpty) { + if (clients.any((c) => !c.disconnected)) { await showClientsMayNotBeChangedAlert(parent.target); } if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { @@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier { } toggleFile() async { - if (clients.isNotEmpty) { + if (clients.any((c) => !c.disconnected)) { await showClientsMayNotBeChangedAlert(parent.target); } if (!_fileOk && @@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier { } toggleInput() async { - if (clients.isNotEmpty) { + if (clients.any((c) => !c.disconnected)) { await showClientsMayNotBeChangedAlert(parent.target); } if (_inputOk) { @@ -549,10 +549,19 @@ class ServerModel with ChangeNotifier { if (index < 0) { _clients.add(client); } else { + if (_clients[index].authorized) { + _clients[index].privacyMode = client.privacyMode; + notifyListeners(); + return; + } _clients[index].authorized = true; + _clients[index].privacyMode = client.privacyMode; } } else { - if (_clients.any((c) => c.id == client.id)) { + final index = _clients.indexWhere((c) => c.id == client.id); + if (index >= 0) { + _clients[index].privacyMode = client.privacyMode; + notifyListeners(); return; } _clients.add(client); @@ -818,6 +827,7 @@ class Client { bool restart = false; bool recording = false; bool blockInput = false; + bool privacyMode = false; bool disconnected = false; bool fromSwitch = false; bool inVoiceCall = false; @@ -846,6 +856,7 @@ class Client { restart = json['restart']; recording = json['recording']; blockInput = json['block_input']; + privacyMode = json['privacy_mode'] ?? privacyMode; disconnected = json['disconnected']; fromSwitch = json['from_switch']; inVoiceCall = json['in_voice_call']; @@ -870,6 +881,7 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['block_input'] = blockInput; + data['privacy_mode'] = privacyMode; data['disconnected'] = disconnected; data['from_switch'] = fromSwitch; data['in_voice_call'] = inVoiceCall; diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index a3d93f88e..54e6a9a9b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1729,7 +1729,7 @@ class RustdeskImpl { } String mainSupportedPrivacyModeImpls({dynamic hint}) { - throw UnimplementedError("mainSupportedPrivacyModeImpls"); + return '[]'; } String mainSupportedInputSource({dynamic hint}) { diff --git a/libs/hbb_common b/libs/hbb_common index 87b11a795..3e31a9493 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 87b11a795964b00deded250657a63626f2c1efa0 +Subproject commit 3e31a94939e026ab2c05d21a2c436960aa9bfea8 diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e8afa8e01..78ba9ebc6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1797,6 +1797,9 @@ impl Remote { Ok(Permission::BlockInput) => { self.handler.set_permission("block_input", p.enabled); } + Ok(Permission::PrivacyMode) => { + self.handler.set_permission("privacy_mode", p.enabled); + } _ => {} } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1ee13f4df..3f97df078 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -972,6 +972,27 @@ pub fn main_show_option(_key: String) -> SyncReturn { } pub fn main_set_option(key: String, value: String) { + #[cfg(target_os = "android")] + { + let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) + || key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER) + || key.eq(config::keys::OPTION_ENABLE_AUDIO); + let allow_perm_change_in_accept_window = config::option2bool( + config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ); + if is_permission_option + && !allow_perm_change_in_accept_window + && crate::ui_cm_interface::has_active_clients() + { + log::info!( + "blocked main_set_option by policy, key={}, value={}", + key, + value + ); + return; + } + } #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { crate::ui_cm_interface::switch_permission_all( @@ -1019,7 +1040,29 @@ pub fn main_get_options_sync() -> SyncReturn { } pub fn main_set_options(json: String) { - let map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + let mut map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + #[cfg(target_os = "android")] + { + let allow_perm_change_in_accept_window = config::option2bool( + config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ); + if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() { + for key in [ + config::keys::OPTION_ENABLE_CLIPBOARD, + config::keys::OPTION_ENABLE_FILE_TRANSFER, + config::keys::OPTION_ENABLE_AUDIO, + ] { + if let Some(value) = map.remove(key) { + log::info!( + "blocked main_set_options item by policy, key={}, value={}", + key, + value + ); + } + } + } + } if !map.is_empty() { set_options(map) } diff --git a/src/ipc.rs b/src/ipc.rs index 099c24d34..e6d4fc834 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -237,6 +237,7 @@ pub enum Data { restart: bool, recording: bool, block_input: bool, + privacy_mode: bool, from_switch: bool, }, ChatMessage { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 6d48e34ee..4113c1391 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "اسم العرض"), ("password-hidden-tip", "كلمة المرور مخفية"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 5ea7c3351..1a3260c5a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Імя для адлюстравання"), ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 218070291..17a89ce07 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 2f1cc8734..799ca951f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 75d16ff92..1ff10c49d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "显示名称"), ("password-hidden-tip", "永久密码已设置(已隐藏)"), ("preset-password-in-use-tip", "当前使用预设密码"), + ("Enable privacy mode", "允许隐私模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7b3dc7908..2b9c6219e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 06ad254c7..7410124df 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 39e077348..7d18cd7a1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Anzeigename"), ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 38e11bfce..0633889a7 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Εμφανιζόμενο όνομα"), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 921f79612..16d43c9b4 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0f49079a2..2e543c25e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index d65cd31c5..a00c312b8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index f12ecf371..aaf8a8be8 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 5f6d5f005..d34e4239e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 43c033a11..1bddd39d1 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8ad712f1e..ab6ed2e76 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nom d’affichage"), ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index dc78bc0d9..fba2fd83d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index 39c45597c..8b8568c85 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -742,5 +742,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "ડિસ્પ્લે નામ"), ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 741805e25..682ee0c46 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 2d596bacc..505b01df9 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2ba49a0cf..7f9b3299e 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Kijelző név"), ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 356a9ee2d..bbd95e79a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 1b6e49691..b83ee01ed 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Visualizza nome"), ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 56faba383..20caca0a7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "表示名"), ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7cc0c9067..7b3ffd98e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "표시 이름"), ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e943ff4cd..a2a1624f7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index a4f39f1e4..82422c30a 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 838984207..906d056bd 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d9cf6ad38..5795b9eeb 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6d140daad..833c947cf 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Naam Weergeven"), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2000de2c8..972afc170 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nazwa wyświetlana"), ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0cdcf93b4..899c8da71 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f9bae32b1..4eb2c1544 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7ace3f736..45b22684e 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nume afișat"), ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 14bc96390..20000cd26 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Отображаемое имя"), ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index f2c4fbfa2..68ce541f2 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d0e99b2a4..6b4e16688 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index aef6b7c66..3f35dea88 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5f9d5505b..f7f6c16d4 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 19ae6896f..bedbe4856 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 7ad257fcb..eda7851c1 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 2cee45268..6e5652560 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ff755768c..5e25801d2 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 2d3eb1d34..c2d058c98 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 5acb15221..40eb561ed 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Görünen Ad"), ("password-hidden-tip", "Şifre gizli"), ("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5211cc92b..b23b84949 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "顯示名稱"), ("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("preset-password-in-use-tip", "目前正在使用預設密碼"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 2594b7cc3..3e1c4f25e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 6939b2ea1..3fadb0efc 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 8b4eb0c48..bd5327bb2 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -241,6 +241,7 @@ pub struct Connection { restart: bool, recording: bool, block_input: bool, + privacy_mode: bool, control_permissions: Option, last_test_delay: Option, network_delay: u32, @@ -431,6 +432,7 @@ impl Connection { restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions), recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions), block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions), + privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions), control_permissions, last_test_delay: None, network_delay: 0, @@ -527,6 +529,9 @@ impl Connection { if !conn.block_input { conn.send_permission(Permission::BlockInput, false).await; } + if !conn.privacy_mode { + conn.send_permission(Permission::PrivacyMode, false).await; + } let mut test_delay_timer = crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT)); let mut last_recv_time = Instant::now(); @@ -674,6 +679,46 @@ impl Connection { } else if &name == "block_input" { conn.block_input = enabled; conn.send_permission(Permission::BlockInput, enabled).await; + } else if &name == "privacy_mode" { + // Keep permission state and runtime state consistent: + // when revoking the permission, try to leave privacy mode first. + // Otherwise we could end up in an inconsistent state where + // permission looks disabled while privacy mode is still active. + if !enabled && privacy_mode::is_in_privacy_mode() { + if let Some(conn_id) = privacy_mode::get_privacy_mode_conn_id() { + if conn_id == conn.inner.id() { + let impl_key = + privacy_mode::get_cur_impl_key().unwrap_or_default(); + let turn_off_res = + privacy_mode::turn_off_privacy(conn_id, None); + match turn_off_res { + Some(Ok(_)) => { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffByPeer, + impl_key.clone(), + ); + conn.send(msg_out).await; + } + _ => { + let msg_out = Self::turn_off_privacy_result_to_msg( + turn_off_res, + impl_key, + ); + conn.send(msg_out).await; + // Turn-off failed, so revert CM's optimistic toggle + // and keep the previous permission value. + conn.send_to_cm(ipc::Data::SwitchPermission { + name: "privacy_mode".to_owned(), + enabled: conn.privacy_mode, + }); + continue; + } + } + } + } + } + conn.privacy_mode = enabled; + conn.send_permission(Permission::PrivacyMode, enabled).await; } } ipc::Data::RawMessage(bytes) => { @@ -978,7 +1023,7 @@ impl Connection { if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { - let _ = Self::turn_off_privacy_to_msg(id); + let _ = Self::turn_off_privacy_to_msg(id, String::new()); } } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] @@ -1900,6 +1945,7 @@ impl Connection { restart: self.restart, recording: self.recording, block_input: self.block_input, + privacy_mode: self.privacy_mode, from_switch: self.from_switch, }); } @@ -2175,6 +2221,7 @@ impl Connection { keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart), keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording), keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input), + keys::OPTION_ENABLE_PRIVACY_MODE => Some(Permission::privacy_mode), _ => None, }; if let Some(permission) = permission { @@ -4145,6 +4192,15 @@ impl Connection { } async fn turn_on_privacy(&mut self, impl_key: String) { + if !self.is_authed_remote_conn() || !self.privacy_mode { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnFailedDenied, + impl_key, + ); + self.send(msg_out).await; + return; + } + let msg_out = if !privacy_mode::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, @@ -4186,7 +4242,7 @@ impl Connection { "Check privacy mode failed: {}, turn off privacy mode.", &err_msg ); - let _ = Self::turn_off_privacy_to_msg(self.inner.id); + let _ = Self::turn_off_privacy_to_msg(self.inner.id, String::new()); crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOnFailed, err_msg, @@ -4205,6 +4261,7 @@ impl Connection { if privacy_mode::is_in_privacy_mode() { let _ = Self::turn_off_privacy_to_msg( privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, + String::new(), ); } crate::common::make_privacy_mode_msg_with_details( @@ -4232,14 +4289,23 @@ impl Connection { impl_key, ) } else { - Self::turn_off_privacy_to_msg(self.inner.id) + Self::turn_off_privacy_to_msg(self.inner.id, impl_key) }; self.send(msg_out).await; } - pub fn turn_off_privacy_to_msg(_conn_id: i32) -> Message { - let impl_key = "".to_owned(); - match privacy_mode::turn_off_privacy(_conn_id, None) { + pub fn turn_off_privacy_to_msg(_conn_id: i32, impl_key: String) -> Message { + Self::turn_off_privacy_result_to_msg( + privacy_mode::turn_off_privacy(_conn_id, None), + impl_key, + ) + } + + fn turn_off_privacy_result_to_msg( + turn_off_res: Option>, + impl_key: String, + ) -> Message { + match turn_off_res { Some(Ok(_)) => crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, impl_key, diff --git a/src/ui.rs b/src/ui.rs index 154319ce4..6d0d0927a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -372,6 +372,11 @@ impl UI { is_installed() } + fn get_supported_privacy_mode_impls(&self) -> String { + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default() + } + fn is_root(&self) -> bool { is_root() } @@ -752,6 +757,7 @@ impl sciter::EventHandler for UI { fn get_icon(); fn install_me(String, String); fn is_installed(); + fn get_supported_privacy_mode_impls(); fn is_root(); fn is_release(); fn set_socks(String, String, String); diff --git a/src/ui/cm.css b/src/ui/cm.css index ba6de887b..3ac6c7be3 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -93,6 +93,13 @@ div.permissions > div:active { opacity: 0.5; } +div.permissions.locked, +div.permissions.locked *, +div.permissions.locked > div:active { + cursor: default !important; + opacity: 1; +} + icon.keyboard { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII='); } @@ -121,6 +128,10 @@ icon.block_input { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAjdJREFUWEe1V8tNAzEQfXOHAx2QG0UgQSqBFIIgHdABoQqOhBq4cCMlcMh90FvZq/HEXtvJxlKUZNceP783no+gY6jqNYBHAHcA+JufXTDBb37eRWTbalZqE82mz7W55v0ABMBGRCLA7PJJAKr6AiC3sT11NHyf2SEyQjvtAMKp3wBYo9VTGbYegjxxU65d5tg4YEBVbwF8ALgw2lLX4in80QqyZUEkAMLCb7P5n4hcdWifTA32Pg0bByA8AE4+oL3n9A1s7ERkEeeNAJzD/QC4OVaCAgjrU7wdK86zAHREJSKqyvvORRxVb67JFOT4NfYGpxwAqCo34oYcKxHZhOdzg7D2BhYigHj6RJ+5QbjrPezlqR61sZTOKYfztSUBWPoXpdA5FwjnC2sCGK+eiNRC8yw+oap0RiayLQHEPwf65zx7DibMoXcEEB0wq/85QJQAbEVkWbvP8f0pTFi/65ZgjtuRyJ7QYWL0OZnwTmiLDobH5nLqGDlUlcmON49jQwnsg/Wxma/VJ1zcGQIR7+OYJGyqbJWhhwlDPxh3JpNRL4Ba7nAsJckoYaFUv7UCyslBvQ3TNDWEfVsPJGH2FCkKTPAxD8ox+poFwJfZqqX15H6eYyK+TgJeriidLCJ7wAQHZ4Udy7u9iFxaG7mynEx4EF1leZDANzV7AE8i8joJICz2cvBxbExIYTZYTTQmxTxTzP+VnvC8rZlLOLEj7m5OW6JqtTs2US6247Hvy7XnX0OV05FP/gHde5fLZaGS8AAAAABJRU5ErkJggg=='); } +icon.privacy_mode { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAB7UlEQVR4AdyTrVYDMRCFuyjqiiuuOJA46sCVR6jDgQTXN+CgQIJCgkOCA0cduOLAgaOOuuW7czYhyWY5FcXQc28n85O5m9nsUuuPf/9IoCzLLnxd9MTCET3SvNckQnwL7lfcpnYueIGiKNbY8QYjERo+wZK4HuAcK94rVvGSWCO8gCqKjAixTXLPsAl7ldBxriASqAo6lfUnqUTaWAP5FajTYjxGCNXeYSRAwSflToBlKxSZKSCiMoUa6Uh+QNW/B37LC9D8lkTYHNegTf7JqNP8b5RB5AT7AkPoNqqXxUyATT28AUzhRuFFaLpDUYc9V1ihr7+EA/JdxUyAxQTWQDM3CuVSEWugGiUztJ5OIJPPhlKRbFEVXJZ1Anph8iNyTCsieA0dvIgCQY3ckBtyTIBjfuDcwRR2TPJDElkRcrpd6XcyJm7X2ATY3CKwi1UxxkNPeyiP/BAa8LVZObtdBMOPcYbvX7wXYJNE2lidBuNxyhgm0I1LCdcgFXmguXqoxhgJKELBKvYMhljH+ULEwDr8mEIRXWHSP6gJKIXIESxYh3PHzWJK1IuwjpAVcBWIhHPX0x2QE/vkHGofIzUevwr4KhZ003wvsOKYkAcxXfPoxbvk3AJuQ5MNRNwFsNKFCaibRGB0CxcqIJGU3wAAAP//8GtoDAAAAAZJREFUAwCJJuAxFVNbWwAAAABJRU5ErkJggg=='); +} + div.outer_buttons { flow:vertical; border-spacing:8; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 8eb8f494e..4a68a571d 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -36,7 +36,8 @@ impl InvokeUiCM for SciterHandler { client.file, client.restart, client.recording, - client.block_input + client.block_input, + client.privacy_mode ), ); } @@ -157,9 +158,18 @@ impl SciterConnectionManager { crate::ui_interface::get_option(key) } + fn get_builtin_option(&self, key: String) -> String { + crate::ui_interface::get_builtin_option(&key) + } + fn hide_cm(&self) -> bool { *crate::ui::cm::HIDE_CM.lock().unwrap() } + + fn get_supported_privacy_mode_impls(&self) -> String { + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default() + } } impl sciter::EventHandler for SciterConnectionManager { @@ -181,6 +191,8 @@ impl sciter::EventHandler for SciterConnectionManager { fn can_elevate(); fn elevate_portable(i32); fn get_option(String); + fn get_builtin_option(String); fn hide_cm(); + fn get_supported_privacy_mode_impls(); } } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index a06fb9ff8..f306e9032 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -4,6 +4,9 @@ var body; var connections = []; var show_chat = false; var show_elevation = true; +var is_privacy_mode_supported = handler.get_supported_privacy_mode_impls() != '[]'; +var allow_perm_change_in_accept_window = + handler.get_builtin_option('enable-perm-change-in-accept-window') != 'N'; var svg_elevate = ; var hide_cm = undefined; @@ -35,6 +38,7 @@ class Body: Reactor.Component me.sendMsg(msg); }; var right_style = show_chat ? "" : "display: none"; + var permissions_locked = !allow_perm_change_in_accept_window; var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; @@ -58,15 +62,16 @@ class Body: Reactor.Component
{c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
{translate('Permissions')}
} - {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
+ {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
-
+
+
} {c.is_file_transfer ?
{translate('Transfer file')}
: ""} @@ -103,6 +108,7 @@ class Body: Reactor.Component } event click $(icon.keyboard) (e) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.keyboard = !connection.keyboard; @@ -112,6 +118,7 @@ class Body: Reactor.Component } event click $(icon.clipboard) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.clipboard = !connection.clipboard; @@ -121,6 +128,7 @@ class Body: Reactor.Component } event click $(icon.audio) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.audio = !connection.audio; @@ -130,6 +138,7 @@ class Body: Reactor.Component } event click $(icon.file) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.file = !connection.file; @@ -139,6 +148,7 @@ class Body: Reactor.Component } event click $(icon.restart) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.restart = !connection.restart; @@ -148,6 +158,7 @@ class Body: Reactor.Component } event click $(icon.recording) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.recording = !connection.recording; @@ -157,6 +168,7 @@ class Body: Reactor.Component } event click $(icon.block_input) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.block_input = !connection.block_input; @@ -165,6 +177,16 @@ class Body: Reactor.Component }); } + event click $(icon.privacy_mode) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.privacy_mode = !connection.privacy_mode; + body.update(); + handler.switch_permission(cid, "privacy_mode", connection.privacy_mode); + }); + } + event click $(button#accept) { var { cid, connection } = this; checkClickTime(function() { @@ -368,7 +390,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -376,6 +398,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin }); if (conn) { conn.authorized = authorized; + conn.privacy_mode = privacy_mode; update(); return; } @@ -391,7 +414,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, audio: audio, file: file, restart: restart, recording: recording, - block_input:block_input, + block_input:block_input, privacy_mode:privacy_mode, disconnected: false }; if (idx < 0) { @@ -480,15 +503,21 @@ function getElapsed(time, now) { return out; } -var ui_status_cache = [""]; +var ui_status_cache = ["", ""]; function check_update_ui() { self.timer(1s, function() { var approve_mode = handler.get_option('approve-mode'); + var allow_perm_change = handler.get_builtin_option('enable-perm-change-in-accept-window'); var changed = false; if (ui_status_cache[0] != approve_mode) { ui_status_cache[0] = approve_mode; changed = true; } + if (ui_status_cache[1] != allow_perm_change) { + ui_status_cache[1] = allow_perm_change; + allow_perm_change_in_accept_window = allow_perm_change != 'N'; + changed = true; + } if (changed) update(); check_update_ui(); }); diff --git a/src/ui/header.tis b/src/ui/header.tis index 2698ce4d0..40ccbcbf2 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -218,7 +218,7 @@ class Header: Reactor.Component { {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} - {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {(pi.platform == "Windows" || pi.platform == "Mac OS") && (handler.get_toggle_option("privacy-mode") || (keyboard_enabled && privacy_mode_enabled)) ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} {handler.version_cmp(pi.version, '1.2.4') >= 0 ?
  • {svg_checkmark}{translate('True color (4:4:4)')}
  • : ""} diff --git a/src/ui/index.tis b/src/ui/index.tis index be826529d..a099b95f9 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -521,6 +521,7 @@ class MyIdMenu: Reactor.Component { {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote restart')}
  • } {!disable_settings &&
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • } {!disable_settings && is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} + {!disable_settings && (handler.get_supported_privacy_mode_impls() != '[]') &&
  • {svg_checkmark}{translate('Enable privacy mode')}
  • } {!disable_settings &&
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • } diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 7602432fe..28fbc3763 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -17,6 +17,7 @@ var audio_enabled = true; // server side var file_enabled = true; // server side var restart_enabled = true; // server side var recording_enabled = true; // server side +var privacy_mode_enabled = true; // server side var scroll_body = $(body); var peer_platform = ""; @@ -588,6 +589,7 @@ handler.setPermission = function(name, enabled) { if (name == "clipboard") clipboard_enabled = enabled; if (name == "restart") restart_enabled = enabled; if (name == "recording") recording_enabled = enabled; + if (name == "privacy_mode") privacy_mode_enabled = enabled; input_blocked = false; header.update(); }); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 19a9e74e7..831824947 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -12,7 +12,10 @@ use hbb_common::fs::serialize_transfer_job; use hbb_common::tokio::sync::mpsc::unbounded_channel; use hbb_common::{ allow_err, bail, - config::{keys::OPTION_FILE_TRANSFER_MAX_FILES, Config}, + config::{ + keys::{OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, OPTION_FILE_TRANSFER_MAX_FILES}, + option2bool, Config, + }, fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult}, log, message_proto::*, @@ -25,10 +28,7 @@ use hbb_common::{ ResultType, }; #[cfg(target_os = "windows")] -use hbb_common::{ - config::{keys::*, option2bool}, - tokio::sync::Mutex as TokioMutex, -}; +use hbb_common::{config::keys::*, tokio::sync::Mutex as TokioMutex}; use serde_derive::Serialize; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use std::iter::FromIterator; @@ -143,6 +143,7 @@ pub struct Client { pub restart: bool, pub recording: bool, pub block_input: bool, + pub privacy_mode: bool, pub from_switch: bool, pub in_voice_call: bool, pub incoming_voice_call: bool, @@ -230,6 +231,7 @@ impl ConnectionManager { restart: bool, recording: bool, block_input: bool, + privacy_mode: bool, from_switch: bool, #[cfg(not(any(target_os = "ios")))] tx: mpsc::UnboundedSender, ) { @@ -251,6 +253,7 @@ impl ConnectionManager { restart, recording, block_input, + privacy_mode, from_switch, #[cfg(not(any(target_os = "ios")))] tx, @@ -392,6 +395,23 @@ pub fn send_chat(id: i32, text: String) { #[inline] #[cfg(not(any(target_os = "ios")))] pub fn switch_permission(id: i32, name: String, enabled: bool) { + #[cfg(target_os = "android")] + let is_keyboard_permission = name == "keyboard"; + #[cfg(not(target_os = "android"))] + let is_keyboard_permission = false; + if !option2bool( + OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ) && !is_keyboard_permission + { + log::info!( + "blocked cm switch_permission by policy, conn_id={}, permission={}, enabled={}", + id, + name, + enabled + ); + return; + } if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); }; @@ -400,6 +420,19 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { #[inline] #[cfg(target_os = "android")] pub fn switch_permission_all(name: String, enabled: bool) { + if name != "keyboard" + && !option2bool( + OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ) + { + log::info!( + "blocked cm switch_permission_all by policy, permission={}, enabled={}", + name, + enabled + ); + return; + } for (_, client) in CLIENTS.read().unwrap().iter() { allow_err!(client.tx.send(Data::SwitchPermission { name: name.clone(), @@ -422,6 +455,13 @@ pub fn get_clients_length() -> usize { clients.len() } +#[inline] +#[cfg(target_os = "android")] +pub fn has_active_clients() -> bool { + let clients = CLIENTS.read().unwrap(); + clients.values().any(|c| !c.disconnected) +} + #[inline] #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "ios")))] @@ -503,9 +543,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, privacy_mode, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -533,6 +573,26 @@ impl IpcTaskRunner { Data::ChatMessage { text } => { self.cm.new_message(self.conn_id, text); } + Data::SwitchPermission { name, enabled } => { + // Keep this branch scoped to privacy mode rollback. + // Other CM permission toggles are updated optimistically by the UI itself. + // The backend currently sends SwitchPermission back to CM only when + // privacy-mode turn-off fails and the UI state must be restored. + if name == "privacy_mode" { + let client = { + let mut clients = CLIENTS.write().unwrap(); + clients.get_mut(&self.conn_id).map(|c| { + c.privacy_mode = enabled; + c.clone() + }) + }; + if let Some(client) = client { + // This reuses add_connection(), and cm.tis only selectively updates + // existing rows (authorized/privacy_mode) for this fallback path. + self.cm.ui_handler.add_connection(&client); + } + } + } Data::FS(mut fs) => { if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs { if let Ok(bytes) = self.stream.next_raw().await { @@ -835,6 +895,7 @@ pub async fn start_listen( restart, recording, block_input, + privacy_mode, from_switch, .. }) => { @@ -856,6 +917,7 @@ pub async fn start_listen( restart, recording, block_input, + privacy_mode, from_switch, tx.clone(), );