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.
This commit is contained in:
21pages committed 2026-10-31 12:08:07 +08:00
1 parent ef827fdb34
commit 956031d109
87 files changed
+853 -23

No files matched your search

+44 -4
View File
@@ -2286,6 +2286,7 @@ bool handleUriLink({List<String>? 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<String>? 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<String>? 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<String>? 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<String>? 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<String>? 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<String>? 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),
+15 -3
View File
@@ -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) {
@@ -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<String> {
final MenuEntryButton<String> defaultEntry;
final List<MenuEntryButton<String>> methods;
LoginMethodMenu({required this.defaultEntry, required this.methods});
@override
List<mod_menu.PopupMenuEntry<String>> build(
BuildContext context, MenuConfig conf) {
return [
_LoginMethodMenuItem(
height: conf.height,
child: IntrinsicHeight(
child:
Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Expanded(
child: MenuEntryButton<String>(
padding: defaultEntry.padding,
enabled: defaultEntry.enabled,
dismissOnClicked: true,
proc: defaultEntry.proc,
childBuilder: defaultEntry.childBuilder,
).build(context, conf).single),
PopupMenuChildrenItem<String>(
height: conf.height,
padding: EdgeInsets.zero,
enabled: defaultEntry.enabled,
itemBuilder: (submenuContext) => methods
.expand((entry) => MenuEntryButton<String>(
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<String> {
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;
}
+25 -9
View File
@@ -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<List<mod_menu.PopupMenuEntry<String>>>
@@ -581,12 +582,16 @@ abstract class BasePeerCard extends StatelessWidget {
bool isRDP = false,
bool isTerminal = false,
bool isTerminalRunAsAdmin = false,
MenuEntryButton<String>? defaultEntry,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
title,
style: style,
),
action(String text, String loginMethod) => MenuEntryButton<String>(
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<String> _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<String> _rdpAction(BuildContext context, String id) {
return MenuEntryButton<String>(
final action = MenuEntryButton<String>(
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,
+1
View File
@@ -579,6 +579,7 @@ List<TTextMenu> 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,
@@ -558,6 +558,11 @@ class _ConnectionPageState extends State<ConnectionPage>
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: [
(
'Connect without password',
() => connect(context, _idController.id,
loginMethod: 'consent')
),
(
'Transfer file',
() => onConnect(isFileTransfer: true)
@@ -804,6 +804,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
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(
@@ -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<FileManagerPage>
_ffi.start(widget.id,
isFileTransfer: true,
password: widget.password,
loginMethod: widget.loginMethod,
isSharedPassword: widget.isSharedPassword,
connToken: widget.connToken,
forceRelay: widget.forceRelay);
@@ -54,6 +54,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
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<FileManagerTabPage> {
key: ValueKey(id),
id: id,
password: args['password'],
loginMethod: args['loginMethod'] ?? '',
isSharedPassword: args['isSharedPassword'],
tabController: tabController,
forceRelay: args['forceRelay'],
@@ -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<PortForwardPage>
_ffi.start(widget.id,
isPortForward: true,
password: widget.password,
loginMethod: widget.loginMethod,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
connToken: widget.connToken,
@@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
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<PortForwardTabPage> {
key: ValueKey(args['id']),
id: id,
password: args['password'],
loginMethod: args['loginMethod'] ?? '',
isSharedPassword: args['isSharedPassword'],
isRDP: isRDP,
tabController: tabController,
@@ -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<int>? displays;
final String? password;
final String loginMethod;
final ToolbarState toolbarState;
final String? switchUuid;
final bool? forceRelay;
@@ -180,6 +182,7 @@ class _RemotePageState extends State<RemotePage>
_ffi.start(
widget.id,
password: widget.password,
loginMethod: widget.loginMethod,
isSharedPassword: widget.isSharedPassword,
switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay,
@@ -97,6 +97,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
display: display,
displays: displays?.cast<int>(),
password: params['password'],
loginMethod: params['loginMethod'] ?? '',
toolbarState: ToolbarState(),
tabController: tabController,
switchUuid: params['switch_uuid'],
@@ -470,6 +471,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
display: display,
displays: displays?.cast<int>(),
password: args['password'],
loginMethod: args['loginMethod'] ?? '',
toolbarState: ToolbarState(),
tabController: tabController,
switchUuid: switchUuid,
@@ -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,
@@ -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<TerminalPage>
_ffi = TerminalConnectionManager.getConnection(
peerId: widget.id,
password: widget.password,
loginMethod: widget.loginMethod,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
connToken: widget.connToken,
@@ -79,6 +79,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
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<TerminalTabPage> {
required String peerId,
required int terminalId,
String? password,
String loginMethod = '',
bool? isSharedPassword,
bool? forceRelay,
String? connToken,
@@ -114,6 +116,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
terminalId: terminalId,
tabKey: tabKey,
password: password,
loginMethod: loginMethod,
isSharedPassword: isSharedPassword,
tabController: tabController,
forceRelay: forceRelay,
@@ -514,6 +517,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
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<TerminalTabPage> {
peerId: peerId,
terminalId: newTerminalId,
password: page.password,
loginMethod: page.loginMethod,
isSharedPassword: page.isSharedPassword,
forceRelay: page.forceRelay,
connToken: page.connToken,
@@ -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<int>? displays;
final String? password;
final String loginMethod;
final ToolbarState toolbarState;
final bool? forceRelay;
final bool? isSharedPassword;
@@ -112,6 +114,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
widget.id,
isViewCamera: true,
password: widget.password,
loginMethod: widget.loginMethod,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
tabWindowId: widget.tabWindowId,
@@ -97,6 +97,7 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
display: display,
displays: displays?.cast<int>(),
password: params['password'],
loginMethod: params['loginMethod'] ?? '',
toolbarState: ToolbarState(),
tabController: tabController,
connToken: params['connToken'],
@@ -433,6 +434,7 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
display: display,
displays: displays?.cast<int>(),
password: args['password'],
loginMethod: args['loginMethod'] ?? '',
toolbarState: ToolbarState(),
tabController: tabController,
connToken: args['connToken'],
@@ -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<FileManagerPage> {
super.initState();
gFFI.start(widget.id,
isFileTransfer: true,
loginMethod: widget.loginMethod,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay);
+5
View File
@@ -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);
}
}
@@ -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<RemotePage> with WidgetsBindingObserver {
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
gFFI.start(
widget.id,
loginMethod: widget.loginMethod,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
@@ -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<TerminalPage>
// 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,
@@ -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<ViewCameraPage>
gFFI.start(
widget.id,
isViewCamera: true,
loginMethod: widget.loginMethod,
password: widget.password,
isSharedPassword: widget.isSharedPassword,
forceRelay: widget.forceRelay,
+8 -3
View File
@@ -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<int>? 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) {
@@ -225,6 +225,7 @@ class RustDeskMultiWindowManager {
String remoteId,
List<int> 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<MultiWindowCallResult> 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<MultiWindowCallResult> 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<MultiWindowCallResult> 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<MultiWindowCallResult> 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
+2
View File
@@ -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
+146
View File
@@ -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<void> openMenu(WidgetTester tester, List<String> actions) async {
MenuEntryButton<String> entry(String title) => MenuEntryButton<String>(
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<String>(
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<String>(
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 = <String>[];
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 = <String>[];
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 = <String>[];
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 = <String>[];
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}));
}
+46
View File
@@ -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']);
}
});
}