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) }