flutter: file-transfer/port forward/rdp support

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2023-07-07 12:22:39 +08:00
parent 6c8c3b4a7a
commit 1f137b3542
9 changed files with 200 additions and 115 deletions

View File

@ -1547,7 +1547,7 @@ Future<bool> initUniLinks() async {
if (initialLink == null) { if (initialLink == null) {
return false; return false;
} }
return parseRustdeskUri(initialLink); return handleUriLink(uriString: initialLink);
} catch (err) { } catch (err) {
debugPrintStack(label: "$err"); debugPrintStack(label: "$err");
return false; return false;
@ -1568,7 +1568,7 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
debugPrint("A uri was received: $uri."); debugPrint("A uri was received: $uri.");
if (uri != null) { if (uri != null) {
if (handleByFlutter) { if (handleByFlutter) {
callUniLinksUriHandler(uri); handleUriLink(uri: uri);
} else { } else {
bind.sendUrlScheme(url: uri.toString()); bind.sendUrlScheme(url: uri.toString());
} }
@ -1581,90 +1581,142 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
return sub; return sub;
} }
/// Handle command line arguments enum UriLinkType {
/// remoteDesktop,
/// * Returns true if we successfully handle the startup arguments. fileTransfer,
bool checkArguments() { portForward,
if (kBootArgs.isNotEmpty) { rdp,
final ret = parseRustdeskUri(kBootArgs.first); }
if (ret) {
return true; // uri link handler
bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
List<String>? args;
if (cmdArgs != null) {
args = cmdArgs;
if (args.isNotEmpty && args[0].startsWith(kUniLinksPrefix)) {
final uri = Uri.tryParse(args[0]);
if (uri != null) {
args = urlLinkToCmdArgs(uri);
}
}
} else if (uri != null) {
args = urlLinkToCmdArgs(uri);
} else if (uriString != null) {
final uri = Uri.tryParse(uriString);
if (uri != null) {
args = urlLinkToCmdArgs(uri);
} }
} }
// bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] if (args == null) return false;
// check connect args
var connectIndex = kBootArgs.indexOf("--connect"); UriLinkType? type;
if (connectIndex == -1) { String? id;
return false; String? password;
} String? switchUuid;
String? id = bool? forceRelay;
kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; for (int i = 0; i < args.length; i++) {
String? password = switch (args[i]) {
kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2]; case '--connect':
if (password != null && password.startsWith("--")) { case '--play':
password = null; type = UriLinkType.remoteDesktop;
} id = args[i + 1];
final switchUuidIndex = kBootArgs.indexOf("--switch_uuid"); i++;
String? switchUuid = kBootArgs.length <= switchUuidIndex + 1 break;
? null case '--file-transfer':
: kBootArgs[switchUuidIndex + 1]; type = UriLinkType.fileTransfer;
if (id != null) { id = args[i + 1];
if (id.startsWith(kUniLinksPrefix)) { i++;
return parseRustdeskUri(id); break;
} else { case '--port-forward':
// remove "--connect xxx" in the `bootArgs` array type = UriLinkType.portForward;
kBootArgs.removeAt(connectIndex); id = args[i + 1];
kBootArgs.removeAt(connectIndex); i++;
// fallback to peer id break;
Future.delayed(Duration.zero, () { case '--rdp':
rustDeskWinManager.newRemoteDesktop(id, type = UriLinkType.rdp;
password: password, switch_uuid: switchUuid); id = args[i + 1];
}); i++;
return true; break;
case '--password':
password = args[i + 1];
i++;
break;
case '--switch_uuid':
switchUuid = args[i + 1];
i++;
break;
case '--relay':
forceRelay = true;
break;
default:
break;
} }
} }
if (type != null && id != null) {
switch (type) {
case UriLinkType.remoteDesktop:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(id!,
password: password,
switch_uuid: switchUuid,
forceRelay: forceRelay);
});
break;
case UriLinkType.fileTransfer:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newFileTransfer(id!,
password: password, forceRelay: forceRelay);
});
break;
case UriLinkType.portForward:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newPortForward(id!, false,
password: password, forceRelay: forceRelay);
});
break;
case UriLinkType.rdp:
Future.delayed(Duration.zero, () {
rustDeskWinManager.newPortForward(id!, true,
password: password, forceRelay: forceRelay);
});
break;
}
return true;
}
return false; return false;
} }
/// Parse `rustdesk://` unilinks List<String>? urlLinkToCmdArgs(Uri uri) {
/// String? command;
/// Returns true if we successfully handle the uri provided. String? id;
/// [Functions]
/// 1. New Connection: rustdesk://connection/new/your_peer_id
bool parseRustdeskUri(String uriPath) {
final uri = Uri.tryParse(uriPath);
if (uri == null) {
debugPrint("uri is not valid: $uriPath");
return false;
}
return callUniLinksUriHandler(uri);
}
/// uri handler
///
/// Returns true if we successfully handle the uri provided.
bool callUniLinksUriHandler(Uri uri) {
debugPrint("uni links called: $uri");
// new connection
String peerId;
if (uri.authority == "connection" && uri.path.startsWith("/new/")) { if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
peerId = uri.path.substring("/new/".length); // For compatibility
} else if (uri.authority == "connect") { command = '--connect';
peerId = uri.path.substring(1); id = uri.path.substring("/new/".length);
} else if (uri.authority.length > 2 && uri.path.length <= 1) { } else if (['connect', "play", 'file-transfer', 'port-forward', 'rdp']
// "/" or "" .contains(uri.authority)) {
peerId = uri.authority; command = '--${uri.authority}';
} else { if (uri.path.length > 1) {
return false; id = uri.path.substring(1);
}
} }
var param = uri.queryParameters;
String? switch_uuid = param["switch_uuid"]; List<String> args = List.empty(growable: true);
String? password = param["password"]; if (command != null && id != null) {
Future.delayed(Duration.zero, () { args.add(command);
rustDeskWinManager.newRemoteDesktop(peerId, args.add(id);
password: password, switch_uuid: switch_uuid); var param = uri.queryParameters;
}); String? password = param["password"];
return true; if (password != null) args.addAll(['--password', password]);
String? switch_uuid = param["switch_uuid"];
if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]);
if (param["relay"] != null) args.add("--relay");
return args;
}
return null;
} }
connectMainDesktop(String id, connectMainDesktop(String id,

View File

@ -52,10 +52,12 @@ class FileManagerPage extends StatefulWidget {
const FileManagerPage( const FileManagerPage(
{Key? key, {Key? key,
required this.id, required this.id,
required this.password,
required this.tabController, required this.tabController,
this.forceRelay}) this.forceRelay})
: super(key: key); : super(key: key);
final String id; final String id;
final String? password;
final bool? forceRelay; final bool? forceRelay;
final DesktopTabController tabController; final DesktopTabController tabController;
@ -79,7 +81,10 @@ class _FileManagerPageState extends State<FileManagerPage>
void initState() { void initState() {
super.initState(); super.initState();
_ffi = FFI(); _ffi = FFI();
_ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay); _ffi.start(widget.id,
isFileTransfer: true,
password: widget.password,
forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager _ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection); .showLoading(translate('Connecting...'), onCancel: closeConnection);

View File

@ -44,6 +44,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
page: FileManagerPage( page: FileManagerPage(
key: ValueKey(params['id']), key: ValueKey(params['id']),
id: params['id'], id: params['id'],
password: params['password'],
tabController: tabController, tabController: tabController,
forceRelay: params['forceRelay'], forceRelay: params['forceRelay'],
))); )));
@ -72,6 +73,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
page: FileManagerPage( page: FileManagerPage(
key: ValueKey(id), key: ValueKey(id),
id: id, id: id,
password: args['password'],
tabController: tabController, tabController: tabController,
forceRelay: args['forceRelay'], forceRelay: args['forceRelay'],
))); )));

View File

@ -28,11 +28,13 @@ class PortForwardPage extends StatefulWidget {
const PortForwardPage( const PortForwardPage(
{Key? key, {Key? key,
required this.id, required this.id,
required this.password,
required this.tabController, required this.tabController,
required this.isRDP, required this.isRDP,
this.forceRelay}) this.forceRelay})
: super(key: key); : super(key: key);
final String id; final String id;
final String password;
final DesktopTabController tabController; final DesktopTabController tabController;
final bool isRDP; final bool isRDP;
final bool? forceRelay; final bool? forceRelay;
@ -55,6 +57,7 @@ class _PortForwardPageState extends State<PortForwardPage>
_ffi = FFI(); _ffi = FFI();
_ffi.start(widget.id, _ffi.start(widget.id,
isPortForward: true, isPortForward: true,
password: widget.password,
forceRelay: widget.forceRelay, forceRelay: widget.forceRelay,
isRdp: widget.isRDP); isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}'); Get.put(_ffi, tag: 'pf_${widget.id}');

View File

@ -43,6 +43,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
page: PortForwardPage( page: PortForwardPage(
key: ValueKey(params['id']), key: ValueKey(params['id']),
id: params['id'], id: params['id'],
password: params['password'],
tabController: tabController, tabController: tabController,
isRDP: isRDP, isRDP: isRDP,
forceRelay: params['forceRelay'], forceRelay: params['forceRelay'],
@ -77,6 +78,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
page: PortForwardPage( page: PortForwardPage(
key: ValueKey(args['id']), key: ValueKey(args['id']),
id: id, id: id,
password: args['password'],
isRDP: isRDP, isRDP: isRDP,
tabController: tabController, tabController: tabController,
forceRelay: args['forceRelay'], forceRelay: args['forceRelay'],

View File

@ -134,7 +134,7 @@ void runMainApp(bool startService) async {
// Check the startup argument, if we successfully handle the argument, we keep the main window hidden. // Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
final handledByUniLinks = await initUniLinks(); final handledByUniLinks = await initUniLinks();
debugPrint("handled by uni links: $handledByUniLinks"); debugPrint("handled by uni links: $handledByUniLinks");
if (handledByUniLinks || checkArguments()) { if (handledByUniLinks || handleUriLink(cmdArgs: kBootArgs)) {
windowManager.hide(); windowManager.hide();
} else { } else {
windowManager.show(); windowManager.show();

View File

@ -248,7 +248,7 @@ class FfiModel with ChangeNotifier {
onUrlSchemeReceived(Map<String, dynamic> evt) { onUrlSchemeReceived(Map<String, dynamic> evt) {
final url = evt['url'].toString().trim(); final url = evt['url'].toString().trim();
if (url.startsWith(kUniLinksPrefix) && parseRustdeskUri(url)) { if (url.startsWith(kUniLinksPrefix) && handleUriLink(uriString: url)) {
return; return;
} }
switch (url) { switch (url) {

View File

@ -84,10 +84,12 @@ class RustDeskMultiWindowManager {
} }
} }
Future<dynamic> newFileTransfer(String remoteId, {bool? forceRelay}) async { Future<dynamic> newFileTransfer(String remoteId,
{String? password, bool? forceRelay}) async {
var msg = jsonEncode({ var msg = jsonEncode({
"type": WindowType.FileTransfer.index, "type": WindowType.FileTransfer.index,
"id": remoteId, "id": remoteId,
"password": password,
"forceRelay": forceRelay, "forceRelay": forceRelay,
}); });
@ -117,11 +119,12 @@ class RustDeskMultiWindowManager {
} }
Future<dynamic> newPortForward(String remoteId, bool isRDP, Future<dynamic> newPortForward(String remoteId, bool isRDP,
{bool? forceRelay}) async { {String? password, bool? forceRelay}) async {
final msg = jsonEncode({ final msg = jsonEncode({
"type": WindowType.PortForward.index, "type": WindowType.PortForward.index,
"id": remoteId, "id": remoteId,
"isRDP": isRDP, "isRDP": isRDP,
"password": password,
"forceRelay": forceRelay, "forceRelay": forceRelay,
}); });

View File

@ -19,15 +19,23 @@ pub fn core_main() -> Option<Vec<String>> {
let mut _is_elevate = false; let mut _is_elevate = false;
let mut _is_run_as_system = false; let mut _is_run_as_system = false;
let mut _is_quick_support = false; let mut _is_quick_support = false;
let mut _is_flutter_connect = false; let mut _is_flutter_invoke_new_connection = false;
let mut arg_exe = Default::default(); let mut arg_exe = Default::default();
for arg in std::env::args() { for arg in std::env::args() {
if i == 0 { if i == 0 {
arg_exe = arg; arg_exe = arg;
} else if i > 0 { } else if i > 0 {
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
if arg == "--connect" || arg == "--play" { if [
_is_flutter_connect = true; "--connect",
"--play",
"--file-transfer",
"--port-forward",
"--rdp",
]
.contains(&arg.as_str())
{
_is_flutter_invoke_new_connection = true;
} }
if arg == "--elevate" { if arg == "--elevate" {
_is_elevate = true; _is_elevate = true;
@ -63,7 +71,7 @@ pub fn core_main() -> Option<Vec<String>> {
} }
} }
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
if _is_flutter_connect { if _is_flutter_invoke_new_connection {
return core_main_invoke_new_connection(std::env::args()); return core_main_invoke_new_connection(std::env::args());
} }
let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe); let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe);
@ -318,38 +326,48 @@ fn import_config(path: &str) {
/// If it returns [`Some`], then the process will continue, and flutter gui will be started. /// If it returns [`Some`], then the process will continue, and flutter gui will be started.
#[cfg(feature = "flutter")] #[cfg(feature = "flutter")]
fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<String>> { fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<String>> {
args.position(|element| { let mut authority = None;
return element == "--connect" || element == "--play"; let mut id = None;
})?; let mut param_array = vec![];
let mut peer_id = args.next().unwrap_or("".to_string()); while let Some(arg) = args.next() {
if peer_id.is_empty() { match arg.as_str() {
eprintln!("please provide a valid peer id"); "--connect" | "--play" | "--file-transfer" | "--port-forward" | "--rdp" => {
return None; authority = Some((&arg.to_string()[2..]).to_owned());
} id = args.next();
let app_name = crate::get_app_name(); }
let ext = format!(".{}", app_name.to_lowercase()); "--password" => {
if peer_id.ends_with(&ext) { if let Some(password) = args.next() {
peer_id = peer_id.replace(&ext, ""); param_array.push(format!("password={password}"));
} }
let mut switch_uuid = None; }
while let Some(item) = args.next() { "--relay" => {
if item == "--switch_uuid" { param_array.push(format!("relay=true"));
switch_uuid = args.next(); }
// inner
"--switch_uuid" => {
if let Some(switch_uuid) = args.next() {
param_array.push(format!("switch_uuid={switch_uuid}"));
}
}
_ => {}
} }
} }
let mut param_array = vec![]; let mut uni_links = Default::default();
if switch_uuid.is_some() { if let Some(authority) = authority {
let switch_uuid = switch_uuid.map_or("".to_string(), |p| format!("switch_uuid={}", p)); if let Some(mut id) = id {
param_array.push(switch_uuid); let app_name = crate::get_app_name();
let ext = format!(".{}", app_name.to_lowercase());
if id.ends_with(&ext) {
id = id.replace(&ext, "");
}
let params = param_array.join("&");
let params_flag = if params.is_empty() { "" } else { "?" };
uni_links = format!("rustdesk://{}/{}{}{}", authority, id, params_flag, params);
}
}
if uni_links.is_empty() {
return None;
} }
let params = param_array.join("&");
let params_flag = if params.is_empty() { "" } else { "?" };
#[allow(unused)]
let uni_links = format!(
"rustdesk://connection/new/{}{}{}",
peer_id, params_flag, params
);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
return try_send_by_dbus(uni_links); return try_send_by_dbus(uni_links);