From 956031d1097676d2dc0189080b7530222159a404 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 28 Oct 2026 14:13:02 +0800 Subject: [PATCH] feat: add connections without sending passwords Add Without password actions and pass the selection through desktop, mobile and Web sessions. Omit the password when constructing LoginRequest, preserve saved credentials, and reuse the existing approval and OS login dialogs. Keep protocol and controlled-side authentication changes separate. --- flutter/lib/common.dart | 48 +++- flutter/lib/common/widgets/dialog.dart | 18 +- .../lib/common/widgets/login_method_menu.dart | 75 ++++++ flutter/lib/common/widgets/peer_card.dart | 34 ++- flutter/lib/common/widgets/toolbar.dart | 1 + .../lib/desktop/pages/connection_page.dart | 5 + .../lib/desktop/pages/desktop_home_page.dart | 1 + .../lib/desktop/pages/file_manager_page.dart | 3 + .../desktop/pages/file_manager_tab_page.dart | 2 + .../lib/desktop/pages/port_forward_page.dart | 3 + .../desktop/pages/port_forward_tab_page.dart | 2 + flutter/lib/desktop/pages/remote_page.dart | 3 + .../lib/desktop/pages/remote_tab_page.dart | 2 + .../pages/terminal_connection_manager.dart | 2 + flutter/lib/desktop/pages/terminal_page.dart | 3 + .../lib/desktop/pages/terminal_tab_page.dart | 5 + .../lib/desktop/pages/view_camera_page.dart | 3 + .../desktop/pages/view_camera_tab_page.dart | 2 + .../lib/mobile/pages/file_manager_page.dart | 3 + flutter/lib/mobile/pages/home_page.dart | 5 + flutter/lib/mobile/pages/remote_page.dart | 3 + flutter/lib/mobile/pages/terminal_page.dart | 3 + .../lib/mobile/pages/view_camera_page.dart | 3 + flutter/lib/models/model.dart | 11 +- flutter/lib/utils/multi_window_manager.dart | 16 ++ flutter/lib/web/bridge.dart | 2 + flutter/test/login_method_menu_test.dart | 146 ++++++++++++ flutter/test/uri_login_method_test.dart | 46 ++++ src/client.rs | 30 ++- src/client/login_method.rs | 225 ++++++++++++++++++ src/core_main.rs | 6 +- src/flutter.rs | 2 + src/flutter_ffi.rs | 2 + src/lang/ar.rs | 3 + src/lang/az.rs | 3 + src/lang/be.rs | 3 + src/lang/bg.rs | 3 + src/lang/ca.rs | 3 + src/lang/cn.rs | 3 + src/lang/cs.rs | 3 + src/lang/da.rs | 3 + src/lang/de.rs | 3 + src/lang/el.rs | 3 + src/lang/eo.rs | 3 + src/lang/es.rs | 3 + src/lang/et.rs | 3 + src/lang/eu.rs | 3 + src/lang/fa.rs | 3 + src/lang/fi.rs | 3 + src/lang/fr.rs | 3 + src/lang/ge.rs | 3 + src/lang/gl.rs | 3 + src/lang/gu.rs | 3 + src/lang/he.rs | 3 + src/lang/hi.rs | 3 + src/lang/hr.rs | 3 + src/lang/hu.rs | 3 + src/lang/id.rs | 3 + src/lang/it.rs | 3 + src/lang/ja.rs | 3 + src/lang/ko.rs | 3 + src/lang/kz.rs | 3 + src/lang/lt.rs | 3 + src/lang/lv.rs | 3 + src/lang/ml.rs | 3 + src/lang/nb.rs | 3 + src/lang/nl.rs | 3 + src/lang/pl.rs | 3 + src/lang/pt_BR.rs | 3 + src/lang/pt_PT.rs | 3 + src/lang/ro.rs | 3 + src/lang/ru.rs | 3 + src/lang/sc.rs | 3 + src/lang/sk.rs | 3 + src/lang/sl.rs | 3 + src/lang/sq.rs | 3 + src/lang/sr.rs | 3 + src/lang/sv.rs | 3 + src/lang/ta.rs | 3 + src/lang/template.rs | 3 + src/lang/th.rs | 3 + src/lang/tr.rs | 3 + src/lang/tw.rs | 3 + src/lang/uk.rs | 3 + src/lang/ur.rs | 3 + src/lang/vi.rs | 3 + src/ui/remote.rs | 2 +- 87 files changed, 853 insertions(+), 23 deletions(-) create mode 100644 flutter/lib/common/widgets/login_method_menu.dart create mode 100644 flutter/test/login_method_menu_test.dart create mode 100644 flutter/test/uri_login_method_test.dart create mode 100644 src/client/login_method.rs diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b80e0b5f9..ece2c500b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2286,6 +2286,7 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { String? password; String? switchUuid; bool? forceRelay; + var loginMethod = ''; for (int i = 0; i < args.length; i++) { switch (args[i]) { case '--connect': @@ -2329,6 +2330,9 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { password = args[i + 1]; i++; break; + case '--without-password': + loginMethod = 'consent'; + break; case '--switch_uuid': switchUuid = args[i + 1]; i++; @@ -2340,11 +2344,15 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { break; } } + if (loginMethod == 'consent') { + password = null; + } if (type != null && id != null) { switch (type) { case UriLinkType.remoteDesktop: Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(id!, + loginMethod: loginMethod, password: password, switchUuid: switchUuid, forceRelay: forceRelay); @@ -2353,30 +2361,35 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { case UriLinkType.fileTransfer: Future.delayed(Duration.zero, () { rustDeskWinManager.newFileTransfer(id!, + loginMethod: loginMethod, password: password, forceRelay: forceRelay); }); break; case UriLinkType.viewCamera: Future.delayed(Duration.zero, () { rustDeskWinManager.newViewCamera(id!, + loginMethod: loginMethod, password: password, forceRelay: forceRelay); }); break; case UriLinkType.portForward: Future.delayed(Duration.zero, () { rustDeskWinManager.newPortForward(id!, false, + loginMethod: loginMethod, password: password, forceRelay: forceRelay); }); break; case UriLinkType.rdp: Future.delayed(Duration.zero, () { rustDeskWinManager.newPortForward(id!, true, + loginMethod: loginMethod, password: password, forceRelay: forceRelay); }); break; case UriLinkType.terminal: Future.delayed(Duration.zero, () { rustDeskWinManager.newTerminal(id!, + loginMethod: loginMethod, password: password, forceRelay: forceRelay); }); break; @@ -2473,6 +2486,10 @@ List? urlLinkToCmdArgs(Uri uri) { var queryParameters = uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v)); + final withoutPassword = queryParameters['without-password'] == 'true'; + if (withoutPassword) { + queryParameters.remove('password'); + } var key = queryParameters["key"]; if (id != null) { @@ -2484,24 +2501,30 @@ List? urlLinkToCmdArgs(Uri uri) { if (isMobile && id != null) { final forceRelay = queryParameters["relay"] != null; final password = queryParameters["password"]; + final loginMethod = withoutPassword ? 'consent' : ''; // Determine connection type based on command if (command == '--file-transfer') { connect(Get.context!, id, + loginMethod: loginMethod, isFileTransfer: true, forceRelay: forceRelay, password: password); } else if (command == '--view-camera') { connect(Get.context!, id, + loginMethod: loginMethod, isViewCamera: true, forceRelay: forceRelay, password: password); } else if (command == '--terminal') { connect(Get.context!, id, + loginMethod: loginMethod, isTerminal: true, forceRelay: forceRelay, password: password); - } else if (command == 'terminal-admin') { + } else if (command == '--terminal-admin') { setEnvTerminalAdmin(); connect(Get.context!, id, + loginMethod: loginMethod, isTerminal: true, forceRelay: forceRelay, password: password); } else { // Default to remote desktop for '--connect', '--play', or direct connection - connect(Get.context!, id, forceRelay: forceRelay, password: password); + connect(Get.context!, id, + loginMethod: loginMethod, forceRelay: forceRelay, password: password); } return null; } @@ -2513,6 +2536,7 @@ List? urlLinkToCmdArgs(Uri uri) { var param = queryParameters; String? password = param["password"]; if (password != null) args.addAll(['--password', password]); + if (withoutPassword) args.add('--without-password'); String? switch_uuid = param["switch_uuid"]; if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]); if (param["relay"] != null) args.add("--relay"); @@ -2531,33 +2555,39 @@ connectMainDesktop(String id, bool? forceRelay, String? password, String? connToken, - bool? isSharedPassword}) async { + bool? isSharedPassword, + String loginMethod = ''}) async { if (isFileTransfer) { await rustDeskWinManager.newFileTransfer(id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); } else if (isViewCamera) { await rustDeskWinManager.newViewCamera(id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); } else if (isTerminal) { await rustDeskWinManager.newTerminal(id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); } else { await rustDeskWinManager.newRemoteDesktop(id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, forceRelay: forceRelay); @@ -2578,7 +2608,8 @@ connect(BuildContext context, String id, bool forceRelay = false, String? password, String? connToken, - bool? isSharedPassword}) async { + bool? isSharedPassword, + String loginMethod = ''}) async { if (id == '') return; if (!isDesktop || desktopType == DesktopType.main) { try { @@ -2603,6 +2634,7 @@ connect(BuildContext context, String id, if (desktopType == DesktopType.main) { await connectMainDesktop( id, + loginMethod: loginMethod, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, isTerminal: isTerminal, @@ -2624,6 +2656,7 @@ connect(BuildContext context, String id, 'isSharedPassword': isSharedPassword, 'forceRelay': forceRelay, 'connToken': connToken, + 'loginMethod': loginMethod, }); } } else { @@ -2635,6 +2668,7 @@ connect(BuildContext context, String id, builder: (BuildContext context) => desktop_file_manager.FileManagerPage( id: id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword), ), @@ -2645,6 +2679,7 @@ connect(BuildContext context, String id, MaterialPageRoute( builder: (BuildContext context) => FileManagerPage( id: id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, forceRelay: forceRelay), @@ -2660,6 +2695,7 @@ connect(BuildContext context, String id, desktop_view_camera.ViewCameraPage( key: ValueKey(id), id: id, + loginMethod: loginMethod, toolbarState: ToolbarState(), password: password, isSharedPassword: isSharedPassword, @@ -2672,6 +2708,7 @@ connect(BuildContext context, String id, MaterialPageRoute( builder: (BuildContext context) => ViewCameraPage( id: id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, forceRelay: forceRelay), @@ -2684,6 +2721,7 @@ connect(BuildContext context, String id, MaterialPageRoute( builder: (BuildContext context) => TerminalPage( id: id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, forceRelay: forceRelay, @@ -2698,6 +2736,7 @@ connect(BuildContext context, String id, builder: (BuildContext context) => desktop_remote.RemotePage( key: ValueKey(id), id: id, + loginMethod: loginMethod, toolbarState: ToolbarState(), password: password, isSharedPassword: isSharedPassword, @@ -2710,6 +2749,7 @@ connect(BuildContext context, String id, MaterialPageRoute( builder: (BuildContext context) => RemotePage( id: id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, forceRelay: forceRelay), diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index c756818b0..cef49cdd2 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -937,13 +937,15 @@ void enterPasswordDialog( } void enterUserLoginDialog(SessionID sessionId, - OverlayDialogManager dialogManager, String osAccountDescTip) async { + OverlayDialogManager dialogManager, String osAccountDescTip, + {bool waitForAccept = false}) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), osAccountDescTip: osAccountDescTip, + waitForAccept: waitForAccept, ); } @@ -966,6 +968,7 @@ _connectDialog( TextEditingController? osPasswordController, TextEditingController? passwordController, String? osAccountDescTip, + bool waitForAccept = false, }) async { final errUsername = ''.obs; var rememberPassword = false; @@ -1008,8 +1011,17 @@ _connectDialog( rememberPassword, ); close(); - dialogManager.showLoading(translate('Logging in...'), - onCancel: closeConnection); + if (waitForAccept) { + showWaitAcceptDialog( + sessionId, + 'wait-remote-accept-nook', + 'Prompt', + 'Please wait for the remote side to accept your session request...', + dialogManager); + } else { + dialogManager.showLoading(translate('Logging in...'), + onCancel: closeConnection); + } } descWidget(String text) { diff --git a/flutter/lib/common/widgets/login_method_menu.dart b/flutter/lib/common/widgets/login_method_menu.dart new file mode 100644 index 000000000..97d97e32c --- /dev/null +++ b/flutter/lib/common/widgets/login_method_menu.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import '../../desktop/widgets/popup_menu.dart'; + +class LoginMethodMenu extends MenuEntryBase { + final MenuEntryButton defaultEntry; + final List> methods; + + LoginMethodMenu({required this.defaultEntry, required this.methods}); + + @override + List> build( + BuildContext context, MenuConfig conf) { + return [ + _LoginMethodMenuItem( + height: conf.height, + child: IntrinsicHeight( + child: + Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Expanded( + child: MenuEntryButton( + padding: defaultEntry.padding, + enabled: defaultEntry.enabled, + dismissOnClicked: true, + proc: defaultEntry.proc, + childBuilder: defaultEntry.childBuilder, + ).build(context, conf).single), + PopupMenuChildrenItem( + height: conf.height, + padding: EdgeInsets.zero, + enabled: defaultEntry.enabled, + itemBuilder: (submenuContext) => methods + .expand((entry) => MenuEntryButton( + childBuilder: entry.childBuilder, + proc: entry.proc, + padding: entry.padding, + enabled: entry.enabled, + dismissOnClicked: true, + dismissCallback: () { + if (Navigator.canPop(context)) Navigator.pop(context); + }, + ).build(submenuContext, conf)) + .toList(), + child: SizedBox( + width: 56, + child: Icon(Icons.keyboard_arrow_right, color: conf.commonColor), + ), + ), + ])), + ) + ]; + } +} + +// PopupMenuItem adds a TextButton around its child; these actions need separate hit targets. +class _LoginMethodMenuItem extends mod_menu.PopupMenuEntry { + const _LoginMethodMenuItem({required this.height, required this.child}); + + final Widget child; + + @override + final double height; + + @override + bool represents(String? value) => false; + + @override + State<_LoginMethodMenuItem> createState() => _LoginMethodMenuItemState(); +} + +class _LoginMethodMenuItemState extends State<_LoginMethodMenuItem> { + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 86ce7c623..6e8e667b6 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -15,6 +15,7 @@ import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/popup_menu.dart'; +import 'login_method_menu.dart'; import 'dart:math' as math; typedef PopupMenuEntryBuilder = Future>> @@ -581,12 +582,16 @@ abstract class BasePeerCard extends StatelessWidget { bool isRDP = false, bool isTerminal = false, bool isTerminalRunAsAdmin = false, + MenuEntryButton? defaultEntry, }) { - return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - title, - style: style, - ), + action(String text, String loginMethod) => MenuEntryButton( + childBuilder: (TextStyle? style) => loginMethod == 'consent' + ? Tooltip( + message: translate( + 'Skip saved passwords and ask the remote user to accept the connection.'), + child: Text(text, style: style), + ) + : Text(text, style: style), proc: () { if (isTerminalRunAsAdmin) { setEnvTerminalAdmin(); @@ -600,20 +605,27 @@ abstract class BasePeerCard extends StatelessWidget { isTcpTunneling: isTcpTunneling, isRDP: isRDP, isTerminal: isTerminal || isTerminalRunAsAdmin, + loginMethod: loginMethod, ); }, padding: menuPadding, dismissOnClicked: true, ); + return LoginMethodMenu( + defaultEntry: defaultEntry ?? action(title, ''), + methods: [ + action(translate('Without password'), 'consent'), + ], + ); } @protected MenuEntryBase _connectAction(BuildContext context) { return _connectCommonAction( context, - (peer.alias.isEmpty + peer.alias.isEmpty ? translate('Connect') - : '${translate('Connect')} ${peer.id}'), + : '${translate('Connect')} ${peer.id}', ); } @@ -664,7 +676,7 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _rdpAction(BuildContext context, String id) { - return MenuEntryButton( + final action = MenuEntryButton( childBuilder: (TextStyle? style) => Container( alignment: AlignmentDirectional.center, height: CustomPopupMenuTheme.height, @@ -698,6 +710,8 @@ abstract class BasePeerCard extends StatelessWidget { padding: menuPadding, dismissOnClicked: true, ); + return _connectCommonAction(context, translate('RDP'), + isRDP: true, defaultEntry: action); } @protected @@ -1591,7 +1605,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, - bool isTerminal = false}) async { + bool isTerminal = false, + String loginMethod = ''}) async { var password = ''; bool isSharedPassword = false; if (tab == PeerTabIndex.ab) { @@ -1619,6 +1634,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, } } connect(context, peer.id, + loginMethod: loginMethod, password: password, isSharedPassword: isSharedPassword, isFileTransfer: isFileTransfer, diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 0ab0a4aa3..b1f84438d 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -579,6 +579,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { bool isTerminal = false}) { final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); connect(context, id, + loginMethod: ffi.loginMethod, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, isTerminal: isTerminal, diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 1d4928bbd..303487f36 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -558,6 +558,11 @@ class _ConnectionPageState extends State context: context, position: RelativeRect.fromLTRB(x, y, x, y), items: [ + ( + 'Connect without password', + () => connect(context, _idController.id, + loginMethod: 'consent') + ), ( 'Transfer file', () => onConnect(isFileTransfer: true) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1c7ae3d9b..12951ca4e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -804,6 +804,7 @@ class _DesktopHomePageState extends State password: call.arguments['password'], forceRelay: call.arguments['forceRelay'], connToken: call.arguments['connToken'], + loginMethod: call.arguments['loginMethod'] ?? '', ); } else if (call.method == kWindowBumpMouse) { return RdPlatformChannel.instance.bumpMouse( diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index eb7cd2bfd..9491d9f8d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -56,6 +56,7 @@ class FileManagerPage extends StatefulWidget { {Key? key, required this.id, required this.password, + this.loginMethod = '', required this.isSharedPassword, this.tabController, this.connToken, @@ -63,6 +64,7 @@ class FileManagerPage extends StatefulWidget { : super(key: key); final String id; final String? password; + final String loginMethod; final bool? isSharedPassword; final bool? forceRelay; final String? connToken; @@ -99,6 +101,7 @@ class _FileManagerPageState extends State _ffi.start(widget.id, isFileTransfer: true, password: widget.password, + loginMethod: widget.loginMethod, isSharedPassword: widget.isSharedPassword, connToken: widget.connToken, forceRelay: widget.forceRelay); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index ed3e9682d..b6f633cf5 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -54,6 +54,7 @@ class _FileManagerTabPageState extends State { key: ValueKey(params['id']), id: params['id'], password: params['password'], + loginMethod: params['loginMethod'] ?? '', isSharedPassword: params['isSharedPassword'], tabController: tabController, forceRelay: params['forceRelay'], @@ -91,6 +92,7 @@ class _FileManagerTabPageState extends State { key: ValueKey(id), id: id, password: args['password'], + loginMethod: args['loginMethod'] ?? '', isSharedPassword: args['isSharedPassword'], tabController: tabController, forceRelay: args['forceRelay'], diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 13dca0eaf..529c6f658 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -29,6 +29,7 @@ class PortForwardPage extends StatefulWidget { Key? key, required this.id, required this.password, + this.loginMethod = '', required this.tabController, required this.isRDP, required this.isSharedPassword, @@ -37,6 +38,7 @@ class PortForwardPage extends StatefulWidget { }) : super(key: key); final String id; final String? password; + final String loginMethod; final DesktopTabController tabController; final bool isRDP; final bool? forceRelay; @@ -69,6 +71,7 @@ class _PortForwardPageState extends State _ffi.start(widget.id, isPortForward: true, password: widget.password, + loginMethod: widget.loginMethod, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, connToken: widget.connToken, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 9d366bcb0..a0833bf02 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(params['id']), id: params['id'], password: params['password'], + loginMethod: params['loginMethod'] ?? '', isSharedPassword: params['isSharedPassword'], tabController: tabController, isRDP: isRDP, @@ -79,6 +80,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(args['id']), id: id, password: args['password'], + loginMethod: args['loginMethod'] ?? '', isSharedPassword: args['isSharedPassword'], isRDP: isRDP, tabController: tabController, diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ecd9080e9..15a6ce622 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -41,6 +41,7 @@ class RemotePage extends StatefulWidget { this.sessionId, this.tabWindowId, this.password, + this.loginMethod = '', this.display, this.displays, this.tabController, @@ -57,6 +58,7 @@ class RemotePage extends StatefulWidget { final int? display; final List? displays; final String? password; + final String loginMethod; final ToolbarState toolbarState; final String? switchUuid; final bool? forceRelay; @@ -180,6 +182,7 @@ class _RemotePageState extends State _ffi.start( widget.id, password: widget.password, + loginMethod: widget.loginMethod, isSharedPassword: widget.isSharedPassword, switchUuid: widget.switchUuid, forceRelay: widget.forceRelay, diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 0b94a4916..acd46db55 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -97,6 +97,7 @@ class _ConnectionTabPageState extends State { display: display, displays: displays?.cast(), password: params['password'], + loginMethod: params['loginMethod'] ?? '', toolbarState: ToolbarState(), tabController: tabController, switchUuid: params['switch_uuid'], @@ -470,6 +471,7 @@ class _ConnectionTabPageState extends State { display: display, displays: displays?.cast(), password: args['password'], + loginMethod: args['loginMethod'] ?? '', toolbarState: ToolbarState(), tabController: tabController, switchUuid: switchUuid, diff --git a/flutter/lib/desktop/pages/terminal_connection_manager.dart b/flutter/lib/desktop/pages/terminal_connection_manager.dart index 91b8baa97..759e17e05 100644 --- a/flutter/lib/desktop/pages/terminal_connection_manager.dart +++ b/flutter/lib/desktop/pages/terminal_connection_manager.dart @@ -14,6 +14,7 @@ class TerminalConnectionManager { static FFI getConnection({ required String peerId, required String? password, + String loginMethod = '', required bool? isSharedPassword, required bool? forceRelay, required String? connToken, @@ -32,6 +33,7 @@ class TerminalConnectionManager { ffi.start( peerId, password: password, + loginMethod: loginMethod, isSharedPassword: isSharedPassword, forceRelay: forceRelay, connToken: connToken, diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index b6ea485a1..2cb09ab9b 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -13,6 +13,7 @@ class TerminalPage extends StatefulWidget { Key? key, required this.id, required this.password, + this.loginMethod = '', required this.tabController, required this.isSharedPassword, required this.terminalId, @@ -24,6 +25,7 @@ class TerminalPage extends StatefulWidget { }) : super(key: key); final String id; final String? password; + final String loginMethod; final DesktopTabController tabController; final bool? forceRelay; final bool? isSharedPassword; @@ -68,6 +70,7 @@ class _TerminalPageState extends State _ffi = TerminalConnectionManager.getConnection( peerId: widget.id, password: widget.password, + loginMethod: widget.loginMethod, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, connToken: widget.connToken, diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 2612c9d35..3fa93fb9c 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -79,6 +79,7 @@ class _TerminalTabPageState extends State { peerId: params['id'], terminalId: terminalId, password: params['password'], + loginMethod: params['loginMethod'] ?? '', isSharedPassword: params['isSharedPassword'], forceRelay: params['forceRelay'], connToken: params['connToken'], @@ -89,6 +90,7 @@ class _TerminalTabPageState extends State { required String peerId, required int terminalId, String? password, + String loginMethod = '', bool? isSharedPassword, bool? forceRelay, String? connToken, @@ -114,6 +116,7 @@ class _TerminalTabPageState extends State { terminalId: terminalId, tabKey: tabKey, password: password, + loginMethod: loginMethod, isSharedPassword: isSharedPassword, tabController: tabController, forceRelay: forceRelay, @@ -514,6 +517,7 @@ class _TerminalTabPageState extends State { peerId: id, terminalId: terminalId, password: args['password'], + loginMethod: args['loginMethod'] ?? '', isSharedPassword: args['isSharedPassword'], forceRelay: args['forceRelay'], connToken: args['connToken'], @@ -713,6 +717,7 @@ class _TerminalTabPageState extends State { peerId: peerId, terminalId: newTerminalId, password: page.password, + loginMethod: page.loginMethod, isSharedPassword: page.isSharedPassword, forceRelay: page.forceRelay, connToken: page.connToken, diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index 6eb65b11d..3b87d915f 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -37,6 +37,7 @@ class ViewCameraPage extends StatefulWidget { this.sessionId, this.tabWindowId, this.password, + this.loginMethod = '', this.display, this.displays, this.tabController, @@ -53,6 +54,7 @@ class ViewCameraPage extends StatefulWidget { final int? display; final List? displays; final String? password; + final String loginMethod; final ToolbarState toolbarState; final bool? forceRelay; final bool? isSharedPassword; @@ -112,6 +114,7 @@ class _ViewCameraPageState extends State widget.id, isViewCamera: true, password: widget.password, + loginMethod: widget.loginMethod, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, tabWindowId: widget.tabWindowId, diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart index 36fa623ff..b72a8674f 100644 --- a/flutter/lib/desktop/pages/view_camera_tab_page.dart +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -97,6 +97,7 @@ class _ViewCameraTabPageState extends State { display: display, displays: displays?.cast(), password: params['password'], + loginMethod: params['loginMethod'] ?? '', toolbarState: ToolbarState(), tabController: tabController, connToken: params['connToken'], @@ -433,6 +434,7 @@ class _ViewCameraTabPageState extends State { display: display, displays: displays?.cast(), password: args['password'], + loginMethod: args['loginMethod'] ?? '', toolbarState: ToolbarState(), tabController: tabController, connToken: args['connToken'], diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index fa57fdbb2..ad9e7d97f 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -16,11 +16,13 @@ class FileManagerPage extends StatefulWidget { FileManagerPage( {Key? key, required this.id, + this.loginMethod = '', this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; + final String loginMethod; final String? password; final bool? isSharedPassword; final bool? forceRelay; @@ -276,6 +278,7 @@ class _FileManagerPageState extends State { super.initState(); gFFI.start(widget.id, isFileTransfer: true, + loginMethod: widget.loginMethod, password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay); diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 651ec4f17..37dc685d5 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -208,6 +208,7 @@ class WebHomePage extends StatelessWidget { bool isTerminal = false; String? id; String? password; + var loginMethod = ''; for (int i = 0; i < args.length; i++) { switch (args[i]) { case '--connect': @@ -240,6 +241,9 @@ class WebHomePage extends StatelessWidget { password = args[i + 1]; i++; break; + case '--without-password': + loginMethod = 'consent'; + break; default: break; } @@ -249,6 +253,7 @@ class WebHomePage extends StatelessWidget { isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, isTerminal: isTerminal, + loginMethod: loginMethod, password: password); } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9da67ab8b..f41ee4808 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -71,12 +71,14 @@ class RemotePage extends StatefulWidget { RemotePage( {Key? key, required this.id, + this.loginMethod = '', this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; + final String loginMethod; final String? password; final bool? isSharedPassword; final bool? forceRelay; @@ -123,6 +125,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { gFFI.ffiModel.updateEventListener(sessionId, widget.id); gFFI.start( widget.id, + loginMethod: widget.loginMethod, password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index ac2ca42f5..19d0a8414 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -71,8 +71,10 @@ class TerminalPage extends StatefulWidget { required this.isSharedPassword, this.forceRelay, this.connToken, + this.loginMethod = '', }) : super(key: key); final String id; + final String loginMethod; final String? password; final bool? forceRelay; final bool? isSharedPassword; @@ -141,6 +143,7 @@ class _TerminalPageState extends State // Use shared FFI instance from connection manager _ffi = TerminalConnectionManager.getConnection( peerId: widget.id, + loginMethod: widget.loginMethod, password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 1bedca19a..9dadcf9a5 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -40,12 +40,14 @@ class ViewCameraPage extends StatefulWidget { ViewCameraPage( {Key? key, required this.id, + this.loginMethod = '', this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; + final String loginMethod; final String? password; final bool? isSharedPassword; final bool? forceRelay; @@ -90,6 +92,7 @@ class _ViewCameraPageState extends State gFFI.start( widget.id, isViewCamera: true, + loginMethod: widget.loginMethod, password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 20e71f10d..501a6fab9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -920,9 +920,10 @@ class FfiModel with ChangeNotifier { enter2FaDialog(sessionId, dialogManager); } else if (type == 'input-password') { enterPasswordDialog(sessionId, dialogManager); - } else if (type == 'terminal-admin-login') { - enterUserLoginDialog( - sessionId, dialogManager, 'terminal-admin-login-tip'); + } else if (type == 'terminal-admin-login' || + type == 'terminal-admin-login-consent') { + enterUserLoginDialog(sessionId, dialogManager, 'terminal-admin-login-tip', + waitForAccept: type == 'terminal-admin-login-consent'); } else if (type == 'terminal-admin-login-password') { enterUserLoginAndPasswordDialog( sessionId, dialogManager, 'terminal-admin-login-tip'); @@ -4031,6 +4032,7 @@ class FFI { var id = ''; var version = ''; var connType = ConnType.defaultConn; + var loginMethod = ''; var closed = false; /// dialogManager use late to ensure init after main page binding [globalKey] @@ -4123,7 +4125,9 @@ class FFI { int? tabWindowId, int? display, List? displays, + String loginMethod = '', }) { + this.loginMethod = loginMethod; closed = false; if (isMobile) mobileReset(); assert( @@ -4168,6 +4172,7 @@ class FFI { password: password ?? '', isSharedPassword: isSharedPassword ?? false, connToken: connToken, + loginMethod: loginMethod, ); } else if (display != null) { if (displays == null) { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 9e26f8cf9..09bcc1a58 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -225,6 +225,7 @@ class RustDeskMultiWindowManager { String remoteId, List windows, { String? password, + String loginMethod = '', bool? forceRelay, String? switchUuid, bool? isRDP, @@ -249,6 +250,9 @@ class RustDeskMultiWindowManager { if (connToken != null) { params['connToken'] = connToken; } + if (loginMethod.isNotEmpty) { + params['loginMethod'] = loginMethod; + } final msg = jsonEncode(params); // separate window for file transfer is not supported @@ -270,6 +274,7 @@ class RustDeskMultiWindowManager { Future newRemoteDesktop( String remoteId, { String? password, + String loginMethod = '', bool? isSharedPassword, String? switchUuid, bool? forceRelay, @@ -280,6 +285,7 @@ class RustDeskMultiWindowManager { remoteId, _remoteDesktopWindows, password: password, + loginMethod: loginMethod, forceRelay: forceRelay, switchUuid: switchUuid, isSharedPassword: isSharedPassword, @@ -289,6 +295,7 @@ class RustDeskMultiWindowManager { Future newFileTransfer( String remoteId, { String? password, + String loginMethod = '', bool? isSharedPassword, bool? forceRelay, String? connToken, @@ -299,6 +306,7 @@ class RustDeskMultiWindowManager { remoteId, _fileTransferWindows, password: password, + loginMethod: loginMethod, forceRelay: forceRelay, isSharedPassword: isSharedPassword, connToken: connToken, @@ -308,6 +316,7 @@ class RustDeskMultiWindowManager { Future newViewCamera( String remoteId, { String? password, + String loginMethod = '', bool? isSharedPassword, String? switchUuid, bool? forceRelay, @@ -319,6 +328,7 @@ class RustDeskMultiWindowManager { remoteId, _viewCameraWindows, password: password, + loginMethod: loginMethod, forceRelay: forceRelay, switchUuid: switchUuid, isSharedPassword: isSharedPassword, @@ -330,6 +340,7 @@ class RustDeskMultiWindowManager { String remoteId, bool isRDP, { String? password, + String loginMethod = '', bool? isSharedPassword, bool? forceRelay, String? connToken, @@ -340,6 +351,7 @@ class RustDeskMultiWindowManager { remoteId, _portForwardWindows, password: password, + loginMethod: loginMethod, forceRelay: forceRelay, isRDP: isRDP, isSharedPassword: isSharedPassword, @@ -350,6 +362,7 @@ class RustDeskMultiWindowManager { Future newTerminal( String remoteId, { String? password, + String loginMethod = '', bool? isSharedPassword, bool? forceRelay, String? connToken, @@ -375,6 +388,9 @@ class RustDeskMultiWindowManager { "isSharedPassword": isSharedPassword, "connToken": connToken, }; + if (loginMethod.isNotEmpty) { + params['loginMethod'] = loginMethod; + } final msg = jsonEncode(params); // Always create a new window for terminal diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index bfde2985b..9f601bfa0 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -149,6 +149,7 @@ class RustdeskImpl { required String password, required bool isSharedPassword, String? connToken, + required String loginMethod, dynamic hint}) { return js.context.callMethod('setByName', [ 'session_add_sync', @@ -156,6 +157,7 @@ class RustdeskImpl { 'id': id, 'password': password, 'is_shared_password': isSharedPassword, + 'login_method': loginMethod, 'isFileTransfer': isFileTransfer, 'isViewCamera': isViewCamera, 'isTerminal': isTerminal diff --git a/flutter/test/login_method_menu_test.dart b/flutter/test/login_method_menu_test.dart new file mode 100644 index 000000000..634674e39 --- /dev/null +++ b/flutter/test/login_method_menu_test.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_hbb/common/widgets/login_method_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; + +Future openMenu(WidgetTester tester, List actions) async { + MenuEntryButton entry(String title) => MenuEntryButton( + childBuilder: (style) => Text(title, style: style), + proc: () => actions.add(title), + dismissOnClicked: true, + ); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return TextButton( + onPressed: () => mod_menu.showMenu( + context: context, + position: const RelativeRect.fromLTRB(20, 20, 780, 580), + items: [ + LoginMethodMenu( + defaultEntry: entry('Connect'), + methods: [entry('Without password')], + ), + LoginMethodMenu( + defaultEntry: entry('View camera'), + methods: [entry('Without password')], + ), + LoginMethodMenu( + defaultEntry: MenuEntryButton( + childBuilder: (style) => Row(children: [ + Text('RDP', style: style), + const Spacer(), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + Navigator.pop(context); + actions.add('Edit RDP'); + }, + ), + ]), + proc: () => actions.add('RDP'), + ), + methods: [entry('Without password')], + ), + entry('Rename'), + ] + .expand((entry) => entry.build(context, + const MenuConfig(commonColor: Colors.black, height: 20))) + .toList(), + ), + child: const Text('Open menu'), + ); + }), + ), + )); + await tester.tap(find.text('Open menu')); + await tester.pumpAndSettle(); +} + +void main() { + testWidgets('main rows align and clicking them connects by default', + (tester) async { + for (final title in ['Connect', 'View camera', 'RDP']) { + final actions = []; + await openMenu(tester, actions); + final left = tester.getTopLeft(find.text('Rename')).dx; + for (final row in ['Connect', 'View camera', 'RDP']) { + expect(tester.getTopLeft(find.text(row)).dx, left); + } + await tester.tap(find.text(title)); + await tester.pumpAndSettle(); + expect(actions, [title]); + expect(find.text('Without password'), findsNothing); + expect(find.text('Rename'), findsNothing); + } + }); + + testWidgets( + 'arrow offers connection without password and dismisses both menus', + (tester) async { + final actions = []; + await openMenu(tester, actions); + await tester.tap(find.byIcon(Icons.keyboard_arrow_right).first); + await tester.pumpAndSettle(); + expect(actions, isEmpty); + expect(find.text('Without password'), findsOneWidget); + expect(find.text('Enter password manually'), findsNothing); + await tester.tap(find.text('Without password')); + await tester.pumpAndSettle(); + expect(actions, ['Without password']); + expect(find.text('Without password'), findsNothing); + expect(find.text('Rename'), findsNothing); + }, + variant: TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + TargetPlatform.linux, + TargetPlatform.macOS, + TargetPlatform.windows, + })); + + testWidgets('RDP edit remains independent of the connection action', + (tester) async { + final actions = []; + await openMenu(tester, actions); + await tester.tap(find.byIcon(Icons.edit)); + await tester.pumpAndSettle(); + expect(actions, ['Edit RDP']); + expect(find.text('Rename'), findsNothing); + }); + + testWidgets('touches around the arrow open the submenu without connecting', + (tester) async { + tester.view.physicalSize = const Size(360, 640); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + for (final corner in [ + Alignment.topLeft, + Alignment.topRight, + Alignment.bottomLeft, + Alignment.bottomRight, + ]) { + final actions = []; + await openMenu(tester, actions); + final arrow = find.byIcon(Icons.keyboard_arrow_right).first; + final row = tester + .getRect(find.ancestor(of: arrow, matching: find.byType(Row)).first); + final target = Rect.fromCenter( + center: tester.getCenter(arrow), width: 56, height: row.height); + await tester.tapAt(corner.withinRect(target.deflate(2))); + await tester.pumpAndSettle(); + expect(actions, isEmpty); + expect(find.text('Without password'), findsOneWidget); + await tester.tap(find.text('Without password')); + await tester.pumpAndSettle(); + expect(actions, ['Without password']); + expect(find.text('Rename'), findsNothing); + } + }, + variant: + TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); +} diff --git a/flutter/test/uri_login_method_test.dart b/flutter/test/uri_login_method_test.dart new file mode 100644 index 000000000..63d6e3f11 --- /dev/null +++ b/flutter/test/uri_login_method_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('without-password links preserve their connection type', () { + for (final type in [ + 'connect', + 'play', + 'file-transfer', + 'view-camera', + 'port-forward', + 'rdp', + 'terminal', + 'terminal-admin', + ]) { + expect( + urlLinkToCmdArgs( + Uri.parse('rustdesk://$type/123456789?without-password=true')), + ['--$type', '123456789', '--without-password'], + ); + } + expect( + urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?without-password=true')), + ['--connect', '123456789', '--without-password'], + ); + }); + + test('omitted and empty passwords retain automatic login', () { + expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789')), + ['--connect', '123456789']); + for (final query in ['password=', 'password=&without-password=false']) { + expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')), + ['--connect', '123456789', '--password', '']); + } + }); + + test('without-password ignores an explicit password in either order', () { + for (final query in [ + 'password=provided&without-password=true', + 'without-password=true&password=provided', + ]) { + expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')), + ['--connect', '123456789', '--without-password']); + } + }); +} diff --git a/src/client.rs b/src/client.rs index 597dc863a..d0d63e7bd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -93,6 +93,8 @@ use crate::ui_session_interface::SessionPermissionConfig; pub use super::lang::*; +mod login_method; + #[cfg(not(target_os = "linux"))] mod audio_playback; #[cfg(target_os = "windows")] @@ -2834,6 +2836,7 @@ pub struct LoginConfigHandler { hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, + login_method: login_method::LoginMethod, config: PeerConfig, pub port_forward: (String, i32), /// This login's `multiplex`, filled with `port_forward` under the turn @@ -2911,6 +2914,7 @@ impl LoginConfigHandler { adapter_luid: Option, shared_password: Option, conn_token: Option, + login_method: &str, ) { let mut id = id; if id.contains("@") { @@ -2951,6 +2955,10 @@ impl LoginConfigHandler { self.id = id; self.conn_type = conn_type; + self.login_method = Default::default(); + if !login_method.is_empty() && !self.set_login_method(login_method) { + log::warn!("Failed to initialize login method: {}", login_method); + } let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; @@ -3860,6 +3868,9 @@ impl LoginConfigHandler { avatar, ..Default::default() }; + if self.skips_saved_password() { + lr.password.clear(); + } match self.conn_type { ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer { dir: self.get_remote_dir(), @@ -4733,10 +4744,14 @@ pub async fn handle_hash( lc.write().unwrap().password = password.clone(); + let request_consent = lc.read().unwrap().skips_saved_password(); + let is_terminal_admin = lc.read().unwrap().is_terminal_admin; let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); if is_terminal && is_terminal_admin { - if password.is_empty() { + if request_consent { + interface.msgbox("terminal-admin-login-consent", "", "", ""); + } else if password.is_empty() { interface.msgbox("terminal-admin-login-password", "", "", ""); } else { interface.msgbox("terminal-admin-login", "", "", ""); @@ -4747,7 +4762,9 @@ pub async fn handle_hash( let password = if password.is_empty() { // login without password, the remote side can click accept - interface.msgbox("input-password", "Password Required", "", ""); + if !request_consent { + interface.msgbox("input-password", "Password Required", "", ""); + } Vec::new() } else { let mut hasher = Sha256::new(); @@ -4756,6 +4773,15 @@ pub async fn handle_hash( hasher.finalize()[..].into() }; + if request_consent { + interface.msgbox( + "wait-remote-accept-nook", + "Prompt", + "Please wait for the remote side to accept your session request...", + "", + ); + } + if let Err(error) = send_login( lc.clone(), String::new(), diff --git a/src/client/login_method.rs b/src/client/login_method.rs new file mode 100644 index 000000000..cdd2de7bf --- /dev/null +++ b/src/client/login_method.rs @@ -0,0 +1,225 @@ +use super::LoginConfigHandler; + +#[derive(Default, PartialEq, Eq)] +pub(super) enum LoginMethod { + #[default] + Automatic, + Consent, +} + +impl LoginConfigHandler { + pub(crate) fn set_login_method(&mut self, method: &str) -> bool { + self.login_method = match method { + "consent" => LoginMethod::Consent, + _ => return false, + }; + true + } + + pub(crate) fn skips_saved_password(&self) -> bool { + self.login_method != LoginMethod::Automatic + } +} + +#[cfg(all(test, feature = "flutter"))] +mod tests { + use crate::{client, flutter::FlutterHandler, ui_session_interface::Session}; + use base::message_proto::{Hash, Message, PeerInfo}; + use hbb_common::{ + config::PeerConfig, protobuf::Message as _, rendezvous_proto::ConnType, tcp::FramedStream, + tokio, Stream, + }; + + async fn connection() -> (Stream, Stream) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let client = tokio::net::TcpStream::connect(addr).await.unwrap(); + let (socket, addr) = listener.accept().await.unwrap(); + ( + Stream::Tcp(FramedStream::from(client, listener.local_addr().unwrap())), + Stream::Tcp(FramedStream::from(socket, addr)), + ) + } + + async fn received_password(peer: &mut Stream) -> Vec { + let bytes = peer.next_timeout(3000).await.unwrap().unwrap(); + Message::parse_from_bytes(&bytes) + .unwrap() + .login_request() + .password + .to_vec() + } + + #[test] + fn session_creation_initializes_login_method() { + let remote_id = hbb_common::config::LocalConfig::get_remote_id(); + for method in ["", "consent"] { + let session_id = uuid::Uuid::new_v4(); + let result = crate::flutter_ffi::session_add_sync( + session_id, + format!("login-method-init-{}", session_id), + false, + false, + false, + false, + false, + String::new(), + false, + String::new(), + false, + None, + method.to_owned(), + ); + let session = crate::flutter::sessions::remove_session_by_session_id(&session_id); + hbb_common::config::LocalConfig::set_remote_id(&remote_id); + assert!(result.0.is_empty(), "{}", result.0); + let session = session.unwrap(); + let request = session + .lc + .read() + .unwrap() + .create_login_msg(String::new(), String::new(), vec![1; 32]); + assert_eq!( + request.login_request().password.is_empty(), + method == "consent" + ); + } + } + + #[tokio::test] + async fn configured_passwords_are_skipped_only_for_the_selected_connection() { + for conn_type in [ + ConnType::DEFAULT_CONN, + ConnType::FILE_TRANSFER, + ConnType::VIEW_CAMERA, + ConnType::PORT_FORWARD, + ConnType::RDP, + ConnType::TERMINAL, + ] { + for skip in [false, true] { + let session = Session::::default(); + { + let mut lc = session.lc.write().unwrap(); + lc.id = format!("skip-saved-password-test-{}", uuid::Uuid::new_v4()); + lc.conn_type = conn_type; + lc.config.password = vec![1; 32]; + lc.password = vec![2; 32]; + lc.shared_password = Some("shared password".into()); + let token = lc.get_conn_token(); + if skip { + assert!(lc.set_login_method("consent")); + } + assert_eq!(lc.get_conn_token(), token); + let request = lc.create_login_msg(String::new(), String::new(), vec![4; 32]); + assert_eq!(request.login_request().password.is_empty(), skip); + } + let (mut stream, mut peer) = connection().await; + assert!( + client::handle_hash( + session.lc.clone(), + "preset password", + Hash { + salt: "salt".into(), + challenge: "challenge".into(), + ..Default::default() + }, + &session, + &mut Default::default(), + &mut stream, + ) + .await + ); + let password = received_password(&mut peer).await; + assert_eq!(password.is_empty(), skip); + } + } + } + + #[tokio::test] + async fn admin_terminal_waits_for_os_credentials_before_requesting_consent() { + let session = Session::::default(); + { + let mut lc = session.lc.write().unwrap(); + lc.conn_type = ConnType::TERMINAL; + lc.is_terminal_admin = true; + assert!(lc.set_login_method("consent")); + } + let (mut stream, mut peer) = connection().await; + let mut trust = Default::default(); + assert!( + client::handle_hash( + session.lc.clone(), + "preset password", + Hash::default(), + &session, + &mut trust, + &mut stream, + ) + .await + ); + assert!(peer.next_timeout(50).await.is_none()); + client::handle_login_from_ui( + session.lc.clone(), + "administrator".into(), + "os password".into(), + String::new(), + false, + &mut trust, + &mut stream, + ) + .await + .unwrap(); + let bytes = peer.next_timeout(3000).await.unwrap().unwrap(); + let message = Message::parse_from_bytes(&bytes).unwrap(); + let login = message.login_request(); + assert!(login.password.is_empty()); + assert_eq!(login.os_login.username, "administrator"); + assert_eq!(login.os_login.password, "os password"); + } + + #[tokio::test] + async fn consent_preserves_saved_password() { + let (mut stream, mut peer) = connection().await; + let id = format!("skip-saved-password-test-{}", uuid::Uuid::new_v4()); + let mut config = PeerConfig::default(); + config.password = vec![3; 32]; + config.store(&id); + let session = Session::::default(); + { + let mut lc = session.lc.write().unwrap(); + lc.initialize( + id.clone(), + ConnType::DEFAULT_CONN, + None, + false, + None, + None, + None, + "", + ); + assert!(!lc.set_login_method("password")); + assert!(lc.set_login_method("consent")); + } + assert!( + client::handle_hash( + session.lc.clone(), + "", + Hash::default(), + &session, + &mut Default::default(), + &mut stream, + ) + .await + ); + let first = received_password(&mut peer).await; + assert!(first.is_empty()); + session + .lc + .write() + .unwrap() + .handle_peer_info(&PeerInfo::default()); + let saved = PeerConfig::load(&id).password; + PeerConfig::remove(&id); + assert_eq!(saved, config.password); + } +} diff --git a/src/core_main.rs b/src/core_main.rs index 19a26703a..9e964bcb9 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -73,6 +73,7 @@ pub fn core_main() -> Option { "--view-camera", "--port-forward", "--terminal", + "--terminal-admin", "--rdp", ] .contains(&arg.as_str()) @@ -763,7 +764,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { + | "--terminal" | "--terminal-admin" | "--rdp" => { authority = Some((&arg.to_string()[2..]).to_owned()); id = args.next(); } @@ -775,6 +776,9 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { param_array.push(format!("relay=true")); } + "--without-password" => { + param_array.push("without-password=true".to_owned()); + } // inner "--switch_uuid" => { if let Some(switch_uuid) = args.next() { diff --git a/src/flutter.rs b/src/flutter.rs index 8c393d4dd..31e6582e6 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1316,6 +1316,7 @@ pub fn session_add( password: String, is_shared_password: bool, conn_token: Option, + login_method: &str, ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER @@ -1376,6 +1377,7 @@ pub fn session_add( get_adapter_luid(), shared_password, conn_token, + login_method, ); let session = Arc::new(session.clone()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8473e38fa..1b7ef4d8f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -160,6 +160,7 @@ pub fn session_add_sync( password: String, is_shared_password: bool, conn_token: Option, + login_method: String, ) -> SyncReturn { let add_res = session_add( &session_id, @@ -174,6 +175,7 @@ pub fn session_add_sync( password, is_shared_password, conn_token, + &login_method, ); // We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`. // Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last. diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 1ebc7c757..7cc0055b6 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "الرجاء اختيار Android → media."), ("Trust duration (days)", "مدة الثقة (بالأيام)"), ("Renew trust", "تجديد الثقة"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/az.rs b/src/lang/az.rs index 714f4110f..0004f3e7d 100644 --- a/src/lang/az.rs +++ b/src/lang/az.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Zəhmət olmasa, Android → media seçin."), ("Trust duration (days)", "Etibar müddəti (gün)"), ("Renew trust", "Etibarı yenilə"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index cadc4e797..f8f461582 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Выберыце Android → media."), ("Trust duration (days)", "Тэрмін даверу (у днях)"), ("Renew trust", "Падоўжыць давер"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index c8ed6cf42..7a01ef725 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Моля, изберете Android → media."), ("Trust duration (days)", "Срок на доверие (дни)"), ("Renew trust", "Подновяване на доверието"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d8b7dfc14..7333f4799 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Seleccioneu Android → media."), ("Trust duration (days)", "Durada de la confiança (dies)"), ("Renew trust", "Renova la confiança"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 98e97631f..75f228976 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "请选择 Android → media。"), ("Trust duration (days)", "信任有效期(天)"), ("Renew trust", "延长信任有效期"), + ("Without password", "不使用密码"), + ("Connect without password", "免密连接"), + ("Skip saved passwords and ask the remote user to accept the connection.", "不使用已保存的密码,请求对方接受连接。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 721051999..4e59a31ab 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Vyberte prosím Android → media."), ("Trust duration (days)", "Doba důvěry (dny)"), ("Renew trust", "Obnovit důvěru"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index e3ed60033..0cc567cd5 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Vælg Android → media."), ("Trust duration (days)", "Tillidsperiode (dage)"), ("Renew trust", "Forny tillid"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a03e3a260..1e57e9f59 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Bitte wählen Sie Android → media."), ("Trust duration (days)", "Vertrauensdauer (Tage)"), ("Renew trust", "Vertrauen erneuern"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 1a5799e83..d8b3a3d67 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Παρακαλώ επιλέξτε Android → media."), ("Trust duration (days)", "Διάρκεια εμπιστοσύνης (ημέρες)"), ("Renew trust", "Ανανέωση εμπιστοσύνης"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index fccb8616f..5fa74fd0e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Bonvolu elekti Android → media."), ("Trust duration (days)", "Daŭro de fido (tagoj)"), ("Renew trust", "Renovigi fidon"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ff9127d1b..30535c6b3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Seleccione Android → media."), ("Trust duration (days)", "Duración de la confianza (días)"), ("Renew trust", "Renovar confianza"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index a7d8980ef..fab3053c8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Palun vali Android → media."), ("Trust duration (days)", "Usalduse kestus (päevades)"), ("Renew trust", "Uuenda usaldust"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index dc00c810a..3ccaf74e9 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Hautatu Android → media."), ("Trust duration (days)", "Konfiantzaren iraupena (egunak)"), ("Renew trust", "Berritu konfiantza"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 66d5dc79c..d1e6b76d2 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "لطفاً Android → media را انتخاب کنید."), ("Trust duration (days)", "مدت اعتماد (روز)"), ("Renew trust", "تمدید اعتماد"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index d4277de0f..70e5e76fe 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Valitse Android → media."), ("Trust duration (days)", "Luottamuksen kesto (päivää)"), ("Renew trust", "Jatka luottamusta"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 72376b909..75859046e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Veuillez sélectionner Android → media."), ("Trust duration (days)", "Durée de confiance (jours)"), ("Renew trust", "Renouveler la confiance"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index fe76e3551..107b8f0a9 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "გთხოვთ, აირჩიოთ Android → media."), ("Trust duration (days)", "ნდობის ხანგრძლივობა (დღე)"), ("Renew trust", "ნდობის განახლება"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gl.rs b/src/lang/gl.rs index 1cade2857..7448caacd 100644 --- a/src/lang/gl.rs +++ b/src/lang/gl.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Selecciona Android → media."), ("Trust duration (days)", "Duración da confianza (días)"), ("Renew trust", "Renovar a confianza"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index c73cdb5b5..ee58aad1c 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "કૃપા કરીને Android → media પસંદ કરો."), ("Trust duration (days)", "વિશ્વાસ અવધિ (દિવસો)"), ("Renew trust", "વિશ્વાસ નવીકરણ કરો"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 102a129bc..36cca3c9a 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "אנא בחר Android → media."), ("Trust duration (days)", "משך האמון (ימים)"), ("Renew trust", "חידוש אמון"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index ce5dc2140..360962c4d 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "कृपया Android → media चुनें।"), ("Trust duration (days)", "विश्वास अवधि (दिन)"), ("Renew trust", "विश्वास नवीनीकृत करें"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index bef672007..ff4f0a8af 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Odaberite Android → media."), ("Trust duration (days)", "Trajanje povjerenja (dani)"), ("Renew trust", "Obnovi povjerenje"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 145914e3c..ac4fb3185 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Válassza az Android → media mappát."), ("Trust duration (days)", "Megbízhatóság időtartama (nap)"), ("Renew trust", "Megbízhatóság megújítása"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 364d542fc..3e3577163 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Silakan pilih Android → media."), ("Trust duration (days)", "Durasi kepercayaan (hari)"), ("Renew trust", "Perbarui kepercayaan"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 42d74e8a8..9f0944b2e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Seleziona Android → media."), ("Trust duration (days)", "Durata attendibilità (giorni)"), ("Renew trust", "Rinnova attendibilità"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 3239326c0..23e04d7cb 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Android → media を選択してください。"), ("Trust duration (days)", "信頼期間 (日)"), ("Renew trust", "信頼を更新"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 490e780e1..371cfb1f3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Android → media를 선택해 주시기 바랍니다."), ("Trust duration (days)", "신뢰 유지 기간 (일)"), ("Renew trust", "신뢰 갱신"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9dbdf611f..ff7a32c4a 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Android → media таңдаңыз."), ("Trust duration (days)", "Сенім мерзімі (күн)"), ("Renew trust", "Сенімді жаңарту"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index bd4f1ce6c..b529a0410 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Pasirinkite Android → media."), ("Trust duration (days)", "Pasitikėjimo trukmė (dienomis)"), ("Renew trust", "Atnaujinti pasitikėjimą"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index f00b2fe4a..0b8c98948 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Lūdzu, atlasiet Android → media."), ("Trust duration (days)", "Uzticēšanās ilgums (dienas)"), ("Renew trust", "Atjaunot uzticēšanos"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index e5be24a38..3134fab9a 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "ദയവായി Android → media തിരഞ്ഞെടുക്കുക."), ("Trust duration (days)", "വിശ്വാസ കാലാവധി (ദിവസം)"), ("Renew trust", "വിശ്വാസം പുതുക്കുക"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 33ef45e7e..cf0ab3f99 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Velg Android → media."), ("Trust duration (days)", "Klareringsperiode (dager)"), ("Renew trust", "Forny klarering"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index e5dad3719..10122e7b6 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Selecteer Android → media."), ("Trust duration (days)", "Vertrouwensperiode (dagen)"), ("Renew trust", "Vertrouwen hernieuwen"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 192c36604..986d35070 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Wybierz Android → media."), ("Trust duration (days)", "Okres zaufania (dni)"), ("Renew trust", "Odnów zaufanie"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_BR.rs b/src/lang/pt_BR.rs index 7008ac620..4fd9ac507 100644 --- a/src/lang/pt_BR.rs +++ b/src/lang/pt_BR.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Selecione Android → media."), ("Trust duration (days)", "Duração da confiança (dias)"), ("Renew trust", "Renovar confiança"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1adcb6e7e..af7a83aff 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Por favor selecione Android → media."), ("Trust duration (days)", "Duração da confiança (dias)"), ("Renew trust", "Renovar confiança"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 05cce6ab8..354c11430 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Selectează Android → media."), ("Trust duration (days)", "Durata încrederii (zile)"), ("Renew trust", "Reînnoiește încrederea"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index dc7757219..444981948 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Выберите Android → media."), ("Trust duration (days)", "Срок доверия (в днях)"), ("Renew trust", "Продлить доверие"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index f17608d4d..9a11b2cd8 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Seletziona Android → media."), ("Trust duration (days)", "Durada de sa fidùtzia (dies)"), ("Renew trust", "Annoa sa fidùtzia"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7d5faf167..75523c8a2 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Vyberte Android → media."), ("Trust duration (days)", "Doba dôvery (dni)"), ("Renew trust", "Obnoviť dôveru"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1107246c7..5f07f29c7 100644 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Izberite Android → media."), ("Trust duration (days)", "Trajanje zaupanja (dni)"), ("Renew trust", "Obnovi zaupanje"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3d329af4c..3511599a3 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Ju lutemi zgjidhni Android → media."), ("Trust duration (days)", "Kohëzgjatja e besimit (ditë)"), ("Renew trust", "Rinovo besimin"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index def1efeb8..3d93b2324 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Izaberite Android → media."), ("Trust duration (days)", "Trajanje poverenja (dani)"), ("Renew trust", "Obnovi poverenje"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 5aad93760..290c477d9 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Välj Android → media."), ("Trust duration (days)", "Förtroendeperiod (dagar)"), ("Renew trust", "Förnya förtroende"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 51ba9f8bc..e5c85717d 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Android → media என்பதைத் தேர்ந்தெடுக்கவும்."), ("Trust duration (days)", "நம்பிக்கைக் காலம் (நாட்கள்)"), ("Renew trust", "நம்பிக்கையைப் புதுப்பி"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 0ca3c660b..a590cc67f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", ""), ("Trust duration (days)", ""), ("Renew trust", ""), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 14deca923..782783070 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "กรุณาเลือก Android → media"), ("Trust duration (days)", "ระยะเวลาการเชื่อถือ (วัน)"), ("Renew trust", "ต่ออายุการเชื่อถือ"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 44dd56e7e..bff3aadaf 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Lütfen Android → media konumunu seçin."), ("Trust duration (days)", "Güven süresi (gün)"), ("Renew trust", "Güveni yenile"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 793e8d0b1..3f41aa044 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "請選擇 Android → media。"), ("Trust duration (days)", "信任有效期(天)"), ("Renew trust", "延長信任期限"), + ("Without password", "不使用密碼"), + ("Connect without password", "免密連線"), + ("Skip saved passwords and ask the remote user to accept the connection.", "不使用已儲存的密碼,請求對方接受連線。"), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 5a3a3f7fc..2b41a902a 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Будь ласка, виберіть Android → media."), ("Trust duration (days)", "Термін довіри (у днях)"), ("Renew trust", "Подовжити довіру"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ur.rs b/src/lang/ur.rs index ad2f10513..32f5fe20e 100644 --- a/src/lang/ur.rs +++ b/src/lang/ur.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "براہ کرم Android → media منتخب کریں۔"), ("Trust duration (days)", "اعتماد کی مدت (دن)"), ("Renew trust", "اعتماد کی تجدید کریں"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index a51fd0473..a0af25460 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -823,5 +823,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select Android → media.", "Vui lòng chọn Android → media."), ("Trust duration (days)", "Thời hạn tin cậy (ngày)"), ("Renew trust", "Gia hạn tin cậy"), + ("Without password", ""), + ("Connect without password", ""), + ("Skip saved passwords and ask the remote user to accept the connection.", ""), ].iter().cloned().collect(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1bdf90a55..56c09e268 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -621,7 +621,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, force_relay, None, None, None); + .initialize(id, conn_type, None, force_relay, None, None, None, ""); Self(session) }