diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 63be444e1..6e3ec7020 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -52,6 +52,8 @@ class MyTheme { static const Color darkGray = Color(0xFFB9BABC); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; + static const Color disabledTextLight = Color(0xFF888888); + static const Color disabledTextDark = Color(0xFF777777); static ThemeData lightTheme = ThemeData( brightness: Brightness.light, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 65c7ae819..9be269370 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -253,28 +253,47 @@ class _Safety extends StatefulWidget { class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); return ListView( children: [ - permissions(), - password(), - whitelist(), + Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(), + password(), + whitelist(), + ]), + ), + ], + ) ], ).marginOnly(bottom: _kListViewBottomMargin); } Widget permissions() { + bool enabled = !locked; return _Card(title: 'Permissions', children: [ - _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard'), - _OptionCheckBox('Enable Clipboard', 'enable-clipboard'), - _OptionCheckBox('Enable File Transfer', 'enable-file-transfer'), - _OptionCheckBox('Enable Audio', 'enable-audio'), - _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart'), + _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard', + enabled: enabled), + _OptionCheckBox('Enable Clipboard', 'enable-clipboard', enabled: enabled), + _OptionCheckBox('Enable File Transfer', 'enable-file-transfer', + enabled: enabled), + _OptionCheckBox('Enable Audio', 'enable-audio', enabled: enabled), + _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart', + enabled: enabled), _OptionCheckBox('Enable remote configuration modification', - 'allow-remote-config-modification'), + 'allow-remote-config-modification', + enabled: enabled), ]); } @@ -297,15 +316,17 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( - value: value, - groupValue: currentValue, - label: value, - onChanged: ((value) { - model.verificationMethod = keys[values.indexOf(value)]; - }))) + value: value, + groupValue: currentValue, + label: value, + onChanged: ((value) { + model.verificationMethod = keys[values.indexOf(value)]; + }), + enabled: !locked, + )) .toList(); - var onChanged = tmp_enabled + var onChanged = tmp_enabled && !locked ? (value) { if (value != null) model.temporaryPasswordLength = value.toString(); @@ -319,7 +340,11 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { value: value, groupValue: model.temporaryPasswordLength, onChanged: onChanged), - Text(value), + Text( + value, + style: TextStyle( + color: _disabledTextColor(onChanged != null)), + ), ], ).paddingSymmetric(horizontal: 10), onTap: () => onChanged?.call(value), @@ -335,10 +360,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...lengthRadios, ], ), - enabled: tmp_enabled), + enabled: tmp_enabled && !locked), radios[1], - _SubButton( - 'Set permanent password', setPasswordDialog, perm_enabled), + _SubButton('Set permanent password', setPasswordDialog, + perm_enabled && !locked), radios[2], ]); }))); @@ -346,7 +371,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { return _Card(title: 'IP Whitelisting', children: [ - _Button('IP Whitelisting', changeWhiteList, tip: 'whitelist_tip') + _Button('IP Whitelisting', changeWhiteList, + tip: 'whitelist_tip', enabled: !locked) ]); } } @@ -362,31 +388,46 @@ class _ConnectionState extends State<_Connection> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); - return ListView( - children: [ - _Card(title: 'Server', children: [ - _Button('ID/Relay Server', changeServer), - ]), - _Card(title: 'Service', children: [ - _OptionCheckBox('Enable Service', 'stop-service', reverse: true), - // TODO: Not implemented - // _option_check('Always connected via relay', 'allow-always-relay'), - // _option_check('Start ID/relay service', 'stop-rendezvous-service', - // reverse: true), - ]), - _Card(title: 'TCP Tunneling', children: [ - _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel'), - ]), - direct_ip(), - _Card(title: 'Proxy', children: [ - _Button('Socks5 Proxy', changeSocks5Proxy), - ]), - ], - ).marginOnly(bottom: _kListViewBottomMargin); + bool enabled = !locked; + return ListView(children: [ + Column( + children: [ + _lock(locked, 'Unlock Connection Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + _Card(title: 'Server', children: [ + _Button('ID/Relay Server', changeServer, enabled: enabled), + ]), + _Card(title: 'Service', children: [ + _OptionCheckBox('Enable Service', 'stop-service', + reverse: true, enabled: enabled), + // TODO: Not implemented + // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), + // _option_check('Start ID/relay service', 'stop-rendezvous-service', + // reverse: true, enabled: enabled), + ]), + _Card(title: 'TCP Tunneling', children: [ + _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled), + ]), + direct_ip(), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), + ]), + ]), + ), + ], + ) + ]).marginOnly(bottom: _kListViewBottomMargin); } Widget direct_ip() { @@ -395,7 +436,7 @@ class _ConnectionState extends State<_Connection> RxBool apply_enabled = false.obs; return _Card(title: 'Direct IP Access', children: [ _OptionCheckBox('Enable Direct IP Access', 'direct-server', - update: update), + update: update, enabled: !locked), _futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); @@ -414,7 +455,7 @@ class _ConnectionState extends State<_Connection> width: 80, child: TextField( controller: controller, - enabled: enabled, + enabled: enabled && !locked, onChanged: (_) => apply_enabled.value = true, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp( @@ -429,10 +470,10 @@ class _ConnectionState extends State<_Connection> ), ), ), - enabled: enabled, - ), + enabled: enabled && !locked, + ).marginOnly(left: 5), Obx(() => ElevatedButton( - onPressed: apply_enabled.value && enabled + onPressed: apply_enabled.value && enabled && !locked ? () async { apply_enabled.value = false; await bind.mainSetOption( @@ -440,7 +481,9 @@ class _ConnectionState extends State<_Connection> value: controller.text); } : null, - child: Text(translate('Apply')), + child: Text( + translate('Apply'), + ), ).marginOnly(left: 20)) ]); }, @@ -700,8 +743,16 @@ Widget _Card({required String title, required List children}) { ); } +Color? _disabledTextColor(bool enabled) { + return enabled + ? null + : isDarkTheme() + ? MyTheme.disabledTextDark + : MyTheme.disabledTextLight; +} + Widget _OptionCheckBox(String label, String key, - {Function()? update = null, bool reverse = false}) { + {Function()? update = null, bool reverse = false, bool enabled = true}) { return _futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { @@ -721,9 +772,14 @@ Widget _OptionCheckBox(String label, String key, child: Obx( () => Row( children: [ - Checkbox(value: ref.value, onChanged: onChanged) + Checkbox( + value: ref.value, onChanged: enabled ? onChanged : null) .marginOnly(right: 10), - Expanded(child: Text(translate(label))) + Expanded( + child: Text( + translate(label), + style: TextStyle(color: _disabledTextColor(enabled)), + )) ], ), ).marginOnly(left: _kCheckBoxLeftMargin), @@ -734,29 +790,33 @@ Widget _OptionCheckBox(String label, String key, }); } -Widget _Radio({ - required T value, - required T groupValue, - required String label, - required Function(T value) onChanged, -}) { - var on_change = (T? value) { - if (value != null) { - onChanged(value); - } - }; +Widget _Radio( + {required T value, + required T groupValue, + required String label, + required Function(T value) onChanged, + bool enabled = true}) { + var on_change = enabled + ? (T? value) { + if (value != null) { + onChanged(value); + } + } + : null; return GestureDetector( child: Row( children: [ Radio(value: value, groupValue: groupValue, onChanged: on_change), Expanded( child: Text(translate(label), - style: TextStyle(fontSize: _kContentFontSize)) + style: TextStyle( + fontSize: _kContentFontSize, + color: _disabledTextColor(enabled))) .marginOnly(left: 5), ), ], ).marginOnly(left: _kRadioLeftMargin), - onTap: () => on_change(value), + onTap: () => on_change?.call(value), ); } @@ -808,19 +868,19 @@ Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { decoration: BoxDecoration( border: Border.all( color: hover.value && enabled - ? Colors.grey.withOpacity(0.8) - : Colors.grey.withOpacity(0.5), + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), width: hover.value && enabled ? 2 : 1)), child: Row( children: [ Container( height: 28, color: (hover.value && enabled) - ? Colors.grey.withOpacity(0.8) - : Colors.grey.withOpacity(0.5), + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), child: Text( label + ': ', - style: TextStyle(), + style: TextStyle(fontWeight: FontWeight.w300), ), alignment: Alignment.center, padding: @@ -851,6 +911,43 @@ Widget _futureBuilder( }); } +Widget _lock( + bool locked, + String label, + Function() onUnlock, +) { + return Offstage( + offstage: !locked, + child: Row( + children: [ + Container( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.security_sharp, + size: 20, + ), + Text(translate(label)).marginOnly(left: 5), + ]).marginSymmetric(vertical: 2)), + onPressed: () async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + }, + ).marginSymmetric(horizontal: 2, vertical: 4), + ).marginOnly(left: _kCardLeftMargin), + ).marginOnly(top: 10), + ], + )); +} + // ignore: must_be_immutable class _ComboBox extends StatelessWidget { late final List keys; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d2acb87ad..094659251 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -70,7 +70,6 @@ class DesktopTabBar extends StatelessWidget { super(key: key) { scrollController.itemCount = tabs.length; WidgetsBinding.instance.addPostFrameCallback((_) { - debugPrint("callback"); scrollController.scrollToItem(selected.value, center: true, animate: true); }); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d3560ba4a..53e3f1ff8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,12 +22,12 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - discover, forget_password, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, - get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, - get_version, has_hwcodec, has_rendezvous_service, post_request, set_local_option, set_option, - set_options, set_peer_option, set_permanent_password, set_socks, store_fav, - test_if_valid_server, update_temporary_password, using_public_server, + check_super_user_permission, discover, forget_password, get_api_server, get_app_name, + get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, + get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, + get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, + set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, + store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -735,6 +735,10 @@ pub fn main_set_permanent_password(password: String) { set_permanent_password(password); } +pub fn main_check_super_user_permission() -> bool { + check_super_user_permission() +} + pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 85947a143..0ead52f31 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -629,3 +629,9 @@ extern "C" { pub fn quit_gui() { unsafe { gtk_main_quit() }; } + +pub fn check_super_user_permission() -> ResultType { + // TODO: replace echo with a rustdesk's program, which is location-fixed and non-gui. + let status = std::process::Command::new("pkexec").arg("echo").status()?; + Ok(status.success() && status.code() == Some(0)) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index cb0fd778f..fa9fb5b10 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -8,7 +8,7 @@ use hbb_common::{ }; use std::io::prelude::*; use std::{ - ffi::OsString, + ffi::{CString, OsString}, fs, io, mem, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -17,7 +17,8 @@ use winapi::{ shared::{minwindef::*, ntdef::NULL, windef::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, - processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*, + processthreadsapi::GetExitCodeProcess, shellapi::ShellExecuteA, winbase::*, wingdi::*, + winnt::HANDLE, winuser::*, }, }; use windows_service::{ @@ -1418,3 +1419,17 @@ pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { } } } + +pub fn check_super_user_permission() -> ResultType { + unsafe { + let ret = ShellExecuteA( + NULL as _, + CString::new("runas")?.as_ptr() as _, + CString::new("cmd")?.as_ptr() as _, + CString::new("/c /q")?.as_ptr() as _, + NULL as _, + SW_SHOWNORMAL, + ); + return Ok(ret as i32 > 32); + } +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d45b83b75..f59f96090 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -676,6 +676,13 @@ pub fn has_hwcodec() -> bool { return true; } +pub fn check_super_user_permission() -> bool { + #[cfg(any(windows, target_os = "linux"))] + return crate::platform::check_super_user_permission().unwrap_or(false); + #[cfg(not(any(windows, target_os = "linux")))] + true +} + pub fn check_zombie(childs: Childs) { let mut deads = Vec::new(); loop {