From 43a0a4f8e0bc0102d1d7ce706c12e3736c89e4e1 Mon Sep 17 00:00:00 2001 From: yuluo Date: Thu, 25 Apr 2024 11:46:21 +0800 Subject: [PATCH 01/35] fix: http/https proxy (#7821) * add http(s) proxy * Add front-end translation * fix ui description * For linux platform, add rustls support * fix: Fix the proxy address test function. * add: Added default prompts for agency agreement and some multi-language translations * add: Http proxy request client * fix: add async http proxy func and format the code * add: Preliminary support for flutter front-end calling rust back-end http request * Optimize HTTP calls * Optimize HTTP calls * fix: Optimize HTTP requests, refine translations, and fix dependencies * fix: Win and macOS compilation errors * fix: web platforms * fix: Optimize import * fix: Fix web platform issues * fix: Fix web platform issues * fix: update ci * fix: test ci * test: test CI * Revert "fix: update ci" This reverts commit 2e5f247b2ed0cc63a6f6f7bbaaffd0a1223712e5. * test: test CI * test: test CI * fix: fix lock file * fix: Optimize imports --- Cargo.lock | 204 +++++-- Cargo.toml | 5 +- .../desktop/pages/desktop_setting_page.dart | 8 +- flutter/lib/models/ab_model.dart | 2 +- flutter/lib/models/group_model.dart | 2 +- flutter/lib/models/user_model.dart | 3 +- flutter/lib/utils/http_service.dart | 115 ++++ flutter/lib/web/bridge.dart | 18 + libs/hbb_common/Cargo.toml | 9 + libs/hbb_common/src/lib.rs | 2 + libs/hbb_common/src/proxy.rs | 561 ++++++++++++++++++ libs/hbb_common/src/socket_client.rs | 32 +- libs/hbb_common/src/tcp.rs | 58 +- src/common.rs | 72 ++- src/custom_server.rs | 8 +- src/flutter_ffi.rs | 13 +- src/hbbs_http.rs | 3 + src/hbbs_http/account.rs | 5 +- src/hbbs_http/http_client.rs | 71 +++ src/hbbs_http/record_upload.rs | 3 +- src/ipc.rs | 3 + src/lang/ar.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 2 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 2 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/he.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + src/naming.rs | 3 +- src/plugin/callback_msg.rs | 3 +- src/plugin/manager.rs | 6 +- src/rendezvous_mediator.rs | 10 +- src/ui.rs | 8 + src/ui_interface.rs | 35 +- 68 files changed, 1174 insertions(+), 131 deletions(-) create mode 100644 flutter/lib/utils/http_service.dart create mode 100644 libs/hbb_common/src/proxy.rs create mode 100644 src/hbbs_http/http_client.rs diff --git a/Cargo.lock b/Cargo.lock index ce54262ea..340dc52f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" +checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" dependencies = [ "flate2", "futures-core", @@ -517,6 +517,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "base64ct" version = "1.6.0" @@ -1825,9 +1831,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if 1.0.0", ] @@ -2825,9 +2831,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -2876,6 +2882,7 @@ version = "0.1.0" dependencies = [ "anyhow", "backtrace", + "base64 0.22.0", "bytes", "chrono", "confy", @@ -2887,6 +2894,7 @@ dependencies = [ "flexi_logger", "futures", "futures-util", + "httparse", "lazy_static", "libc", "log", @@ -2898,6 +2906,8 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", + "rustls-pki-types", + "rustls-platform-verifier", "serde 1.0.190", "serde_derive", "serde_json 1.0.107", @@ -2906,9 +2916,12 @@ dependencies = [ "sysinfo", "thiserror", "tokio", - "tokio-socks", + "tokio-native-tls", + "tokio-rustls 0.26.0", + "tokio-socks 0.5.1-2", "tokio-util", "toml 0.7.8", + "url", "uuid", "winapi 0.3.9", "zstd 0.13.0", @@ -2985,9 +2998,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -2996,9 +3009,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -3038,9 +3051,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -3053,7 +3066,7 @@ dependencies = [ "httpdate", "itoa 1.0.9", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -3071,7 +3084,7 @@ dependencies = [ "hyper", "rustls 0.21.10", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", ] [[package]] @@ -4565,7 +4578,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa" dependencies = [ - "base64", + "base64 0.21.5", "indexmap 1.9.3", "line-wrap", "quick-xml", @@ -4838,7 +4851,7 @@ dependencies = [ "ring 0.16.20", "rustc-hash", "rustls 0.20.9", - "rustls-native-certs", + "rustls-native-certs 0.6.3", "slab", "thiserror", "tinyvec", @@ -5173,10 +5186,10 @@ dependencies = [ [[package]] name = "reqwest" version = "0.11.23" -source = "git+https://github.com/rustdesk-org/reqwest" +source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3" dependencies = [ "async-compression", - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -5196,8 +5209,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.10", - "rustls-native-certs", - "rustls-pemfile", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.3", "serde 1.0.190", "serde_json 1.0.107", "serde_urlencoded", @@ -5205,14 +5218,15 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.24.1", + "tokio-socks 0.5.1", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg 0.50.0", ] @@ -5358,7 +5372,6 @@ dependencies = [ "arboard", "async-process", "async-trait", - "base64", "bytes", "cc", "cfg-if 1.0.0", @@ -5519,10 +5532,25 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring 0.17.5", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1" +dependencies = [ + "log", + "once_cell", + "ring 0.17.5", + "rustls-pki-types", + "rustls-webpki 0.102.2", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -5530,7 +5558,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.3", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", "schannel", "security-framework", ] @@ -5541,9 +5582,52 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64", + "base64 0.21.5", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-platform-verifier" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d" +dependencies = [ + "core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.4", + "jni 0.19.0", + "log", + "once_cell", + "rustls 0.23.4", + "rustls-native-certs 0.7.0", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.2", + "security-framework", + "security-framework-sys", + "webpki-roots 0.26.1", + "winapi 0.3.9", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -5554,6 +5638,17 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring 0.17.5", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -5666,22 +5761,23 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "core-foundation-sys 0.8.4", "libc", + "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys 0.8.4", "libc", @@ -6404,6 +6500,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1-2" @@ -6420,6 +6527,18 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -6637,9 +6756,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -7081,9 +7200,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "weezl" @@ -7856,6 +7984,12 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 8a9d3d5fb..a9bbcef9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,6 @@ samplerate = { version = "0.2", optional = true } uuid = { version = "1.3", features = ["v4"] } clap = "4.2" rpassword = "7.2" -base64 = "0.21" num_cpus = "1.15" bytes = { version = "1.4", features = ["serde"] } default-net = "0.14" @@ -145,10 +144,10 @@ wallpaper = { git = "https://github.com/21pages/wallpaper.rs" } [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] # https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support -reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false } +reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false } [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] -reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } +reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.27" } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 7eedb1d5b..17998dd9a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1117,7 +1117,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { child: Column(children: [ server(enabled), _Card(title: 'Proxy', children: [ - _Button('Socks5 Proxy', changeSocks5Proxy, + _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, enabled: enabled), ]), ]), @@ -2047,7 +2047,7 @@ void changeSocks5Proxy() async { } return CustomAlertDialog( - title: Text(translate('Socks5 Proxy')), + title: Text(translate('Socks5/Http(s) Proxy')), content: ConstrainedBox( constraints: const BoxConstraints(minWidth: 500), child: Column( @@ -2064,7 +2064,9 @@ void changeSocks5Proxy() async { Expanded( child: TextField( decoration: InputDecoration( - errorText: proxyMsg.isNotEmpty ? proxyMsg : null), + errorText: proxyMsg.isNotEmpty ? proxyMsg : null, + hintText: translate('Default protocol and port are Socks5 and 1080'), + ), controller: proxyController, autofocus: true, ), diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index b166817c5..9849e39bf 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -10,8 +10,8 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:bot_toast/bot_toast.dart'; -import 'package:http/http.dart' as http; +import '../utils/http_service.dart' as http; import '../common.dart'; final syncAbOption = 'sync-ab-with-recent-sessions'; diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index b8bc2722d..c2a50afb3 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'dart:convert'; -import 'package:http/http.dart' as http; +import '../utils/http_service.dart' as http; class GroupModel { final RxBool groupLoading = false.obs; diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 04b7ebb5b..4e7f881ad 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -6,9 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:get/get.dart'; -import 'package:http/http.dart' as http; import '../common.dart'; +import '../utils/http_service.dart' as http; import 'model.dart'; import 'platform_model.dart'; @@ -136,7 +136,6 @@ class UserModel { Future login(LoginRequest loginRequest) async { final url = await bind.mainGetApiServer(); final resp = await http.post(Uri.parse('$url/api/login'), - headers: {'Content-Type': 'application/json'}, body: jsonEncode(loginRequest.toJson())); final Map body; diff --git a/flutter/lib/utils/http_service.dart b/flutter/lib/utils/http_service.dart new file mode 100644 index 000000000..49855017b --- /dev/null +++ b/flutter/lib/utils/http_service.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import '../models/platform_model.dart'; +export 'package:http/http.dart' show Response; + +enum HttpMethod { get, post, put, delete } + +class HttpService { + Future sendRequest( + Uri url, + HttpMethod method, { + Map? headers, + dynamic body, + }) async { + headers ??= {'Content-Type': 'application/json'}; + + // Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method. + final isProxy = await bind.mainGetProxyStatus(); + + if (!isProxy) { + return await _pollFultterHttp(url, method, headers: headers, body: body); + } + + String headersJson = jsonEncode(headers); + String methodName = method.toString().split('.').last; + await bind.mainHttpRequest( + url: url.toString(), + method: methodName.toLowerCase(), + body: body, + header: headersJson); + + var resJson = await _pollForResponse(url.toString()); + return _parseHttpResponse(resJson); + } + + Future _pollFultterHttp( + Uri url, + HttpMethod method, { + Map? headers, + dynamic body, + }) async { + var response = http.Response('', 400); + + switch (method) { + case HttpMethod.get: + response = await http.get(url, headers: headers); + break; + case HttpMethod.post: + response = await http.post(url, headers: headers, body: body); + break; + case HttpMethod.put: + response = await http.put(url, headers: headers, body: body); + break; + case HttpMethod.delete: + response = await http.delete(url, headers: headers, body: body); + break; + default: + throw Exception('Unsupported HTTP method'); + } + + return response; + } + + Future _pollForResponse(String url) async { + String? responseJson = " "; + while (responseJson == " ") { + responseJson = await bind.mainGetHttpStatus(url: url); + if (responseJson == null) { + throw Exception('The HTTP request failed'); + } + if (responseJson == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + } + } + return responseJson!; + } + + http.Response _parseHttpResponse(String responseJson) { + try { + var parsedJson = jsonDecode(responseJson); + String body = parsedJson['body']; + Map headers = {}; + for (var key in parsedJson['headers'].keys) { + headers[key] = parsedJson['headers'][key]; + } + int statusCode = parsedJson['status_code']; + return http.Response(body, statusCode, headers: headers); + } catch (e) { + throw Exception('Failed to parse response: $e'); + } + } +} + +Future get(Uri url, {Map? headers}) async { + return await HttpService().sendRequest(url, HttpMethod.get, headers: headers); +} + +Future post(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return await HttpService() + .sendRequest(url, HttpMethod.post, body: body, headers: headers); +} + +Future put(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return await HttpService() + .sendRequest(url, HttpMethod.put, body: body, headers: headers); +} + +Future delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}) async { + return await HttpService() + .sendRequest(url, HttpMethod.delete, body: body, headers: headers); +} diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index fd0d7189b..91e7e9711 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -770,6 +770,24 @@ class RustdeskImpl { throw UnimplementedError(); } + Future mainGetProxyStatus({dynamic hint}) { + return Future(() => false); + } + + Future mainHttpRequest({ + required String url, + required String method, + String? body, + required String header, + dynamic hint, + }) { + throw UnimplementedError(); + } + + Future mainGetHttpStatus({required String url, dynamic hint}){ + throw UnimplementedError(); + } + String mainGetLocalOption({required String key, dynamic hint}) { return js.context.callMethod('getByName', ['option:local', key]); } diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 9f77abf0e..38c02ceae 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -41,10 +41,19 @@ uuid = { version = "1.3", features = ["v4"] } # crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052 sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" } thiserror = "1.0" +httparse = "1.5" +base64 = "0.22" +url = "2.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" machine-uid = { git = "https://github.com/21pages/machine-uid" } +[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] +tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false } +rustls-platform-verifier = "0.3.1" +rustls-pki-types = "1.4" +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +tokio-native-tls ="0.3" [features] quic = [] diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index eed2331fb..57d38db09 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -16,6 +16,7 @@ use std::{ }; pub use tokio; pub use tokio_util; +pub mod proxy; pub mod socket_client; pub mod tcp; pub mod udp; @@ -51,6 +52,7 @@ pub use serde_json; pub use sysinfo; pub use toml; pub use uuid; +pub use base64; pub use thiserror; #[cfg(feature = "quic")] diff --git a/libs/hbb_common/src/proxy.rs b/libs/hbb_common/src/proxy.rs new file mode 100644 index 000000000..34d2c5109 --- /dev/null +++ b/libs/hbb_common/src/proxy.rs @@ -0,0 +1,561 @@ +use std::{ + io::Error as IoError, + net::{SocketAddr, ToSocketAddrs}, +}; + +use base64::{engine::general_purpose, Engine}; +use httparse::{Error as HttpParseError, Response, EMPTY_HEADER}; +use log::info; +use thiserror::Error as ThisError; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use tokio_native_tls::{native_tls, TlsConnector, TlsStream}; +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +use tokio_rustls::{client::TlsStream, TlsConnector}; +use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr}; +use tokio_util::codec::Framed; +use url::Url; + +use crate::{ + bytes_codec::BytesCodec, + config::Socks5Server, + tcp::{DynTcpStream, FramedStream}, + ResultType, +}; + +#[derive(Debug, ThisError)] +pub enum ProxyError { + #[error("IO Error: {0}")] + IoError(#[from] IoError), + #[error("Target parse error: {0}")] + TargetParseError(String), + #[error("HTTP parse error: {0}")] + HttpParseError(#[from] HttpParseError), + #[error("The maximum response header length is exceeded: {0}")] + MaximumResponseHeaderLengthExceeded(usize), + #[error("The end of file is reached")] + EndOfFile, + #[error("The url is error: {0}")] + UrlBadScheme(String), + #[error("The url parse error: {0}")] + UrlParseScheme(#[from] url::ParseError), + #[error("No HTTP code was found in the response")] + NoHttpCode, + #[error("The HTTP code is not equal 200: {0}")] + HttpCode200(u16), + #[error("The proxy address resolution failed: {0}")] + AddressResolutionFailed(String), + #[cfg(any(target_os = "windows", target_os = "macos"))] + #[error("The native tls error: {0}")] + NativeTlsError(#[from] tokio_native_tls::native_tls::Error), +} + +const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096; +/// The maximum HTTP Headers, which can be parsed. +const MAXIMUM_RESPONSE_HEADERS: usize = 16; +const DEFINE_TIME_OUT: u64 = 600; + +pub trait IntoUrl { + + // Besides parsing as a valid `Url`, the `Url` must be a valid + // `http::Uri`, in that it makes sense to use in a network request. + fn into_url(self) -> Result; + + fn as_str(&self) -> &str; +} + +impl IntoUrl for Url { + fn into_url(self) -> Result { + if self.has_host() { + Ok(self) + } else { + Err(ProxyError::UrlBadScheme(self.to_string())) + } + } + + fn as_str(&self) -> &str { + self.as_ref() + } +} + +impl<'a> IntoUrl for &'a str { + fn into_url(self) -> Result { + Url::parse(self) + .map_err(ProxyError::UrlParseScheme)? + .into_url() + } + + fn as_str(&self) -> &str { + self + } +} + +impl<'a> IntoUrl for &'a String { + fn into_url(self) -> Result { + (&**self).into_url() + } + + fn as_str(&self) -> &str { + self.as_ref() + } +} + +impl<'a> IntoUrl for String { + fn into_url(self) -> Result { + (&*self).into_url() + } + + fn as_str(&self) -> &str { + self.as_ref() + } +} + +#[derive(Clone)] +pub struct Auth { + user_name: String, + password: String, +} + +impl Auth { + fn get_proxy_authorization(&self) -> String { + format!( + "Proxy-Authorization: Basic {}\r\n", + self.get_basic_authorization() + ) + } + + pub fn get_basic_authorization(&self) -> String { + let authorization = format!("{}:{}", &self.user_name, &self.password); + general_purpose::STANDARD.encode(authorization.as_bytes()) + } +} + +#[derive(Clone)] +pub enum ProxyScheme { + Http { + auth: Option, + host: String, + }, + Https { + auth: Option, + host: String, + }, + Socks5 { + addr: SocketAddr, + auth: Option, + remote_dns: bool, + }, +} + +impl ProxyScheme { + pub fn maybe_auth(&self) -> Option<&Auth> { + match self { + ProxyScheme::Http { auth, .. } + | ProxyScheme::Https { auth, .. } + | ProxyScheme::Socks5 { auth, .. } => auth.as_ref(), + } + } + + fn socks5(addr: SocketAddr) -> Result { + Ok(ProxyScheme::Socks5 { + addr, + auth: None, + remote_dns: false, + }) + } + + fn http(host: &str) -> Result { + Ok(ProxyScheme::Http { + auth: None, + host: host.to_string(), + }) + } + fn https(host: &str) -> Result { + Ok(ProxyScheme::Https { + auth: None, + host: host.to_string(), + }) + } + + fn set_basic_auth, U: Into>(&mut self, username: T, password: U) { + let auth = Auth { + user_name: username.into(), + password: password.into(), + }; + match self { + ProxyScheme::Http { auth: a, .. } => *a = Some(auth), + ProxyScheme::Https { auth: a, .. } => *a = Some(auth), + ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth), + } + } + + fn parse(url: Url) -> Result { + use url::Position; + + // Resolve URL to a host and port + let to_addr = || { + let addrs = url.socket_addrs(|| match url.scheme() { + "socks5" => Some(1080), + _ => None, + })?; + addrs + .into_iter() + .next() + .ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost)) + }; + + let mut scheme: Self = match url.scheme() { + "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?, + "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?, + "socks5" => Self::socks5(to_addr()?)?, + e => return Err(ProxyError::UrlBadScheme(e.to_string())), + }; + + if let Some(pwd) = url.password() { + let username = url.username(); + scheme.set_basic_auth(username, pwd); + } + + Ok(scheme) + } + pub async fn socket_addrs(&self) -> Result { + info!("Resolving socket address"); + match self { + ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await, + ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await, + ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()), + } + } + + async fn resolve_host(&self, host: &str, default_port: u16) -> Result { + let (host_str, port) = match host.split_once(':') { + Some((h, p)) => (h, p.parse::().ok()), + None => (host, None), + }; + let addr = (host_str, port.unwrap_or(default_port)) + .to_socket_addrs()? + .next() + .ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?; + Ok(addr) + } + + pub fn get_domain(&self) -> Result { + match self { + ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => { + let domain = host + .split(':') + .next() + .ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?; + Ok(domain.to_string()) + } + ProxyScheme::Socks5 { addr, .. } => match addr { + SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()), + SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()), + }, + } + } + pub fn get_host_and_port(&self) -> Result { + match self { + ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)), + ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)), + ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)), + } + } + fn append_default_port(&self, host: &str, default_port: u16) -> String { + if host.contains(':') { + host.to_string() + } else { + format!("{}:{}", host, default_port) + } + } +} + +pub trait IntoProxyScheme { + fn into_proxy_scheme(self) -> Result; +} + +impl IntoProxyScheme for S { + fn into_proxy_scheme(self) -> Result { + // validate the URL + let url = match self.as_str().into_url() { + Ok(ok) => ok, + Err(e) => { + match e { + // If the string does not contain protocol headers, try to parse it using the socks5 protocol + ProxyError::UrlParseScheme(_source) => { + let try_this = format!("socks5://{}", self.as_str()); + try_this.into_url()? + } + _ => { + return Err(e); + } + } + } + }; + ProxyScheme::parse(url) + } +} + +impl IntoProxyScheme for ProxyScheme { + fn into_proxy_scheme(self) -> Result { + Ok(self) + } +} + +#[derive(Clone)] +pub struct Proxy { + pub intercept: ProxyScheme, + ms_timeout: u64, +} + +impl Proxy { + pub fn new(proxy_scheme: U, ms_timeout: u64) -> Result { + Ok(Self { + intercept: proxy_scheme.into_proxy_scheme()?, + ms_timeout, + }) + } + + pub fn is_http_or_https(&self) -> bool { + return match self.intercept { + ProxyScheme::Socks5 { .. } => false, + _ => true, + }; + } + + pub fn from_conf(conf: &Socks5Server, ms_timeout: Option) -> Result { + let mut proxy; + match ms_timeout { + None => { + proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?; + } + Some(time_out) => { + proxy = Self::new(&conf.proxy, time_out)?; + } + } + + if !conf.password.is_empty() && !conf.username.is_empty() { + proxy = proxy.basic_auth(&conf.username, &conf.password); + } + Ok(proxy) + } + + pub async fn proxy_addrs(&self) -> Result { + self.intercept.socket_addrs().await + } + + fn basic_auth(mut self, username: &str, password: &str) -> Proxy { + self.intercept.set_basic_auth(username, password); + self + } + + pub async fn connect<'t, T>( + self, + target: T, + local_addr: Option, + ) -> ResultType + where + T: IntoTargetAddr<'t>, + { + info!("Connect to proxy server"); + let proxy = self.proxy_addrs().await?; + + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) + }; + + let stream = super::timeout( + self.ms_timeout, + crate::tcp::new_socket(local, true)?.connect(proxy), + ) + .await??; + stream.set_nodelay(true).ok(); + + let addr = stream.local_addr()?; + + return match self.intercept { + ProxyScheme::Http { .. } => { + info!("Connect to remote http proxy server: {}", proxy); + let stream = + super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??; + Ok(FramedStream( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )) + } + ProxyScheme::Https { .. } => { + info!("Connect to remote https proxy server: {}", proxy); + let stream = + super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??; + Ok(FramedStream( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )) + } + ProxyScheme::Socks5 { .. } => { + info!("Connect to remote socket5 proxy server: {}", proxy); + let stream = if let Some(auth) = self.intercept.maybe_auth() { + super::timeout( + self.ms_timeout, + Socks5Stream::connect_with_password_and_socket( + stream, + target, + &auth.user_name, + &auth.password, + ), + ) + .await?? + } else { + super::timeout( + self.ms_timeout, + Socks5Stream::connect_with_socket(stream, target), + ) + .await?? + }; + Ok(FramedStream( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )) + } + }; + } + + #[cfg(any(target_os = "windows", target_os = "macos"))] + pub async fn https_connect<'a, Input, T>( + self, + io: Input, + target: T, + ) -> Result>, ProxyError> + where + Input: AsyncRead + AsyncWrite + Unpin, + T: IntoTargetAddr<'a>, + { + let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?); + let stream = tls_connector + .connect(&self.intercept.get_domain()?, io) + .await?; + self.http_connect(stream, target).await + } + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + pub async fn https_connect<'a, Input, T>( + self, + io: Input, + target: T, + ) -> Result>, ProxyError> + where + Input: AsyncRead + AsyncWrite + Unpin, + T: IntoTargetAddr<'a>, + { + use std::convert::TryFrom; + let verifier = rustls_platform_verifier::tls_config(); + let url_domain = self.intercept.get_domain()?; + + let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str()) + .map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))? + .to_owned(); + + let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier)); + let stream = tls_connector.connect(domain, io).await?; + self.http_connect(stream, target).await + } + + pub async fn http_connect<'a, Input, T>( + self, + io: Input, + target: T, + ) -> Result, ProxyError> + where + Input: AsyncRead + AsyncWrite + Unpin, + T: IntoTargetAddr<'a>, + { + let mut stream = BufStream::new(io); + let (domain, port) = get_domain_and_port(target)?; + + let request = self.make_request(&domain, port); + stream.write_all(request.as_bytes()).await?; + stream.flush().await?; + recv_and_check_response(&mut stream).await?; + Ok(stream) + } + + fn make_request(&self, host: &str, port: u16) -> String { + let mut request = format!( + "CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n", + host = host, + port = port + ); + + if let Some(auth) = self.intercept.maybe_auth() { + request = format!("{}{}", request, auth.get_proxy_authorization()); + } + + request.push_str("\r\n"); + request + } +} + +fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> { + let target_addr = target + .into_target_addr() + .map_err(|e| ProxyError::TargetParseError(e.to_string()))?; + match target_addr { + tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())), + tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)), + } +} + +async fn get_response(stream: &mut BufStream) -> Result +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + use tokio::io::AsyncBufReadExt; + let mut response = String::new(); + + loop { + if stream.read_line(&mut response).await? == 0 { + return Err(ProxyError::EndOfFile); + } + + if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() { + return Err(ProxyError::MaximumResponseHeaderLengthExceeded( + response.len(), + )); + } + + if response.ends_with("\r\n\r\n") { + return Ok(response); + } + } +} + +async fn recv_and_check_response(stream: &mut BufStream) -> Result<(), ProxyError> +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + let response_string = get_response(stream).await?; + + let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS]; + let mut response = Response::new(&mut response_headers); + let response_bytes = response_string.into_bytes(); + response.parse(&response_bytes)?; + + return match response.code { + Some(code) => { + if code == 200 { + Ok(()) + } else { + Err(ProxyError::HttpCode200(code)) + } + } + None => Err(ProxyError::NoHttpCode), + }; +} diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 2d9b5a984..aaafeb861 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -1,11 +1,13 @@ use crate::{ config::{Config, NetworkType}, + proxy::IntoProxyScheme, tcp::FramedStream, udp::FramedSocket, ResultType, }; use anyhow::Context; use std::net::SocketAddr; +use log::info; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; @@ -50,19 +52,15 @@ pub fn increase_port(host: T, offset: i32) -> String { } pub fn test_if_valid_server(host: &str) -> String { - let host = check_port(host, 0); - + info!("Testing server validity for host: {}", host); use std::net::ToSocketAddrs; - match Config::get_network_type() { - NetworkType::Direct => match host.to_socket_addrs() { - Err(err) => err.to_string(), - Ok(_) => "".to_owned(), - }, - NetworkType::ProxySocks => match &host.into_target_addr() { - Err(err) => err.to_string(), - Ok(_) => "".to_owned(), - }, - } + // Even if the current network type is a proxy type, + // the system DNS should be used to resolve the proxy server address. + host.into_proxy_scheme() + .and_then(|scheme| scheme.get_host_and_port()) + .and_then(|domain| domain.to_socket_addrs().map_err(Into::into)) + .map(|_| "".to_owned()) // on success, return an empty string + .unwrap_or_else(|e| e.to_string()) // on error, convert the error into a string } pub trait IsResolvedSocketAddr { @@ -107,15 +105,7 @@ pub async fn connect_tcp_local< ms_timeout: u64, ) -> ResultType { if let Some(conf) = Config::get_socks() { - return FramedStream::connect( - conf.proxy.as_str(), - target, - local, - conf.username.as_str(), - conf.password.as_str(), - ms_timeout, - ) - .await; + return FramedStream::connect(target, local, &conf, ms_timeout).await; } if let Some(target) = target.resolve() { if let Some(local) = local { diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index 71aa46ec4..17f360ff9 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -1,4 +1,4 @@ -use crate::{bail, bytes_codec::BytesCodec, ResultType}; +use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy}; use anyhow::Context as AnyhowCtx; use bytes::{BufMut, Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; @@ -18,20 +18,20 @@ use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf}, net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs}, }; -use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs}; +use tokio_socks::IntoTargetAddr; use tokio_util::codec::Framed; pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {} -pub struct DynTcpStream(Box); +pub struct DynTcpStream(pub(crate) Box); #[derive(Clone)] pub struct Encrypt(Key, u64, u64); pub struct FramedStream( - Framed, - SocketAddr, - Option, - u64, + pub(crate) Framed, + pub(crate) SocketAddr, + pub(crate) Option, + pub(crate) u64, ); impl Deref for FramedStream { @@ -62,7 +62,7 @@ impl DerefMut for DynTcpStream { } } -fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result { +pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result { let socket = match addr { std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?, std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?, @@ -109,51 +109,17 @@ impl FramedStream { bail!(format!("Failed to connect to {remote_addr}")); } - pub async fn connect<'a, 't, P, T>( - proxy: P, + pub async fn connect<'t, T>( target: T, local_addr: Option, - username: &'a str, - password: &'a str, + proxy_conf: &Socks5Server, ms_timeout: u64, ) -> ResultType where - P: ToProxyAddrs, T: IntoTargetAddr<'t>, { - if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await { - let local = if let Some(addr) = local_addr { - addr - } else { - crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) - }; - let stream = - super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??; - stream.set_nodelay(true).ok(); - let stream = if username.trim().is_empty() { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_socket(stream, target), - ) - .await?? - } else { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_password_and_socket( - stream, target, username, password, - ), - ) - .await?? - }; - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); - } - bail!("could not resolve to any address"); + let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?; + proxy.connect::(target, local_addr).await } pub fn local_addr(&self) -> SocketAddr { diff --git a/src/common.rs b/src/common.rs index 4d2f4c62b..41b872afc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,6 +5,8 @@ use std::{ task::Poll, }; +use serde_json::Value; + #[derive(Debug, Eq, PartialEq)] pub enum GrabState { Ready, @@ -123,7 +125,7 @@ use hbb_common::compress::decompress; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, - bail, + bail, base64, bytes::Bytes, compress::compress as compress_func, config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT}, @@ -145,7 +147,10 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use crate::ui_interface::{get_option, set_option}; +use crate::{ + hbbs_http::create_http_client_async, + ui_interface::{get_option, set_option}, +}; pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; @@ -972,7 +977,7 @@ pub fn check_software_update() { #[tokio::main(flavor = "current_thread")] async fn check_software_update_() -> hbb_common::ResultType<()> { let url = "https://github.com/rustdesk/rustdesk/releases/latest"; - let latest_release_response = reqwest::get(url).await?; + let latest_release_response = create_http_client_async().get(url).send().await?; let latest_release_version = latest_release_response .url() .path() @@ -1067,7 +1072,7 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String { } pub async fn post_request(url: String, body: String, header: &str) -> ResultType { - let mut req = reqwest::Client::new().post(url); + let mut req = create_http_client_async().post(url); if !header.is_empty() { let tmp: Vec<&str> = header.split(": ").collect(); if tmp.len() == 2 { @@ -1084,6 +1089,65 @@ pub async fn post_request_sync(url: String, body: String, header: &str) -> Resul post_request(url, body, header).await } +#[tokio::main(flavor = "current_thread")] +pub async fn http_request_sync( + url: String, + method: String, + body: Option, + header: String, +) -> ResultType { + let http_client = create_http_client_async(); + let mut http_client = match method.as_str() { + "get" => http_client.get(url), + "post" => http_client.post(url), + "put" => http_client.put(url), + "delete" => http_client.delete(url), + _ => return Err(anyhow!("The HTTP request method is not supported!")), + }; + let v = serde_json::from_str(header.as_str())?; + + if let Value::Object(obj) = v { + for (key, value) in obj.iter() { + http_client = http_client.header(key, value.as_str().unwrap_or_default()); + } + } else { + return Err(anyhow!("HTTP header information parsing failed!")); + } + + if let Some(b) = body { + http_client = http_client.body(b); + } + + let response = http_client + .timeout(std::time::Duration::from_secs(12)) + .send() + .await?; + + // Serialize response headers + let mut response_headers = serde_json::map::Map::new(); + for (key, value) in response.headers() { + response_headers.insert( + key.to_string(), + serde_json::json!(value.to_str().unwrap_or("")), + ); + } + + let status_code = response.status().as_u16(); + let response_body = response.text().await?; + + // Construct the JSON object + let mut result = serde_json::map::Map::new(); + result.insert("status_code".to_string(), serde_json::json!(status_code)); + result.insert( + "headers".to_string(), + serde_json::Value::Object(response_headers), + ); + result.insert("body".to_string(), serde_json::json!(response_body)); + + // Convert map to JSON string + serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e)) +} + #[inline] pub fn make_privacy_mode_msg_with_details( state: back_notification::PrivacyModeState, diff --git a/src/custom_server.rs b/src/custom_server.rs index 22bb9ee8f..58d34d853 100644 --- a/src/custom_server.rs +++ b/src/custom_server.rs @@ -1,5 +1,9 @@ -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use hbb_common::{bail, sodiumoxide::crypto::sign, ResultType}; +use hbb_common::{ + bail, + base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}, + sodiumoxide::crypto::sign, + ResultType, +}; use serde_derive::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e210fbd67..016263e74 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -749,6 +749,10 @@ pub fn main_get_async_status() -> String { get_async_job_status() } +pub fn main_get_http_status(url: String) -> Option { + get_async_http_status(url) +} + pub fn main_get_option(key: String) -> String { get_option(key) } @@ -805,6 +809,10 @@ pub fn main_set_socks(proxy: String, username: String, password: String) { set_socks(proxy, username, password) } +pub fn main_get_proxy_status() -> bool { + get_proxy_status() +} + pub fn main_get_socks() -> Vec { get_socks() } @@ -878,9 +886,8 @@ pub fn main_get_api_server() -> String { get_api_server() } -// This function doesn't seem to be used. -pub fn main_post_request(url: String, body: String, header: String) { - post_request(url, body, header) +pub fn main_http_request(url: String, method: String, body: Option, header: String) { + http_request(url,method, body, header) } pub fn main_get_local_option(key: String) -> SyncReturn { diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 76ced87a0..71fff7ca8 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -4,8 +4,11 @@ use serde_json::{Map, Value}; #[cfg(feature = "flutter")] pub mod account; +mod http_client; pub mod record_upload; pub mod sync; +pub use http_client::create_http_client; +pub use http_client::create_http_client_async; #[derive(Debug)] pub enum HbbHttpResponse { diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 3f1a7f1c1..8d4eb28b1 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,4 +1,5 @@ use super::HbbHttpResponse; +use crate::hbbs_http::create_http_client; use hbb_common::{config::LocalConfig, log, ResultType}; use reqwest::blocking::Client; use serde_derive::{Deserialize, Serialize}; @@ -130,7 +131,7 @@ impl Default for UserStatus { impl OidcSession { fn new() -> Self { Self { - client: Client::new(), + client: create_http_client(), state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, @@ -168,7 +169,7 @@ impl OidcSession { id: &str, uuid: &str, ) -> ResultType> { - let url = reqwest::Url::parse_with_params( + let url = Url::parse_with_params( &format!("{}/api/oidc/auth-query", api_server), &[("code", code), ("id", id), ("uuid", uuid)], )?; diff --git a/src/hbbs_http/http_client.rs b/src/hbbs_http/http_client.rs new file mode 100644 index 000000000..c4bb2452c --- /dev/null +++ b/src/hbbs_http/http_client.rs @@ -0,0 +1,71 @@ +use hbb_common::config::Config; +use hbb_common::log::info; +use hbb_common::proxy::{Proxy, ProxyScheme}; +use reqwest::blocking::Client as SyncClient; +use reqwest::Client as AsyncClient; + +macro_rules! configure_http_client { + ($builder:expr, $Client: ty) => {{ + let mut builder = $builder; + let client = if let Some(conf) = Config::get_socks() { + let proxy_result = Proxy::from_conf(&conf, None); + + match proxy_result { + Ok(proxy) => { + let proxy_setup = match &proxy.intercept { + ProxyScheme::Http { host, .. } =>{ reqwest::Proxy::http(format!("http://{}", host))}, + ProxyScheme::Https { host, .. } => {reqwest::Proxy::https(format!("https://{}", host))}, + ProxyScheme::Socks5 { addr, .. } => { reqwest::Proxy::all(&format!("socks5://{}", addr)) } + }; + + match proxy_setup { + Ok(p) => { + builder = builder.proxy(p); + if let Some(auth) = proxy.intercept.maybe_auth() { + let basic_auth = + format!("Basic {}", auth.get_basic_authorization()); + builder = builder.default_headers( + vec![( + reqwest::header::PROXY_AUTHORIZATION, + basic_auth.parse().unwrap(), + )] + .into_iter() + .collect(), + ); + } + builder.build().unwrap_or_else(|e| { + info!("Failed to create a proxied client: {}", e); + <$Client>::new() + }) + } + Err(e) => { + info!("Failed to set up proxy: {}", e); + <$Client>::new() + } + } + } + Err(e) => { + info!("Failed to configure proxy: {}", e); + <$Client>::new() + } + } + } else { + builder.build().unwrap_or_else(|e| { + info!("Failed to create a client: {}", e); + <$Client>::new() + }) + }; + + client + }}; +} + +pub fn create_http_client() -> SyncClient { + let builder = SyncClient::builder(); + configure_http_client!(builder, SyncClient) +} + +pub fn create_http_client_async() -> AsyncClient { + let builder = AsyncClient::builder(); + configure_http_client!(builder, AsyncClient) +} diff --git a/src/hbbs_http/record_upload.rs b/src/hbbs_http/record_upload.rs index 79e836988..a25aae42d 100644 --- a/src/hbbs_http/record_upload.rs +++ b/src/hbbs_http/record_upload.rs @@ -1,3 +1,4 @@ +use crate::hbbs_http::create_http_client; use bytes::Bytes; use hbb_common::{bail, config::Config, lazy_static, log, ResultType}; use reqwest::blocking::{Body, Client}; @@ -25,7 +26,7 @@ pub fn is_enable() -> bool { pub fn run(rx: Receiver) { let mut uploader = RecordUploader { - client: Client::new(), + client: create_http_client(), api_server: crate::get_api_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), diff --git a/src/ipc.rs b/src/ipc.rs index 3a2b88aed..3ee18be43 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -904,6 +904,9 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { Ok(()) } +pub fn get_proxy_status() -> bool { + Config::get_socks().is_some() +} #[tokio::main(flavor = "current_thread")] pub async fn test_rendezvous_server() -> ResultType<()> { let mut c = connect(1000, "").await?; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index bdfd83cc1..2b3d9faec 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "فارغ"), ("Invalid folder name", "اسم المجلد غير صحيح"), ("Socks5 Proxy", "وكيل Socks5"), + ("Socks5/Http(s) Proxy", "وكيل Socks5/Http(s)"), ("Discovered", "المكتشفة"), ("install_daemon_tip", "للبدء مع بدء تشغيل النظام. تحتاج الى تثبيت خدمة النظام."), ("Remote ID", "المعرف البعيد"), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 3a3a40ee9..6384083d6 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", ""), ("Invalid folder name", ""), ("Socks5 Proxy", "Socks5 прокси"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"), ("Discovered", ""), ("install_daemon_tip", "За стартиране с компютъра трябва да инсталирате системна услуга."), ("Remote ID", ""), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 610282b1a..8cf9738c4 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Buit"), ("Invalid folder name", "Nom de carpeta incorrecte"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Descobert"), ("install_daemon_tip", ""), ("Remote ID", "ID remot"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0309cf562..25851db83 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -239,6 +239,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "空空如也"), ("Invalid folder name", "无效文件夹名称"), ("Socks5 Proxy", "Socks5 代理"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 代理"), + ("Default protocol and port are Socks5 and 1080", "默认代理协议及端口为Socks5和1080"), ("Discovered", "已发现"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"), ("Remote ID", "远程 ID"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 23cb7442e..10c78f118 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Prázdné"), ("Invalid folder name", "Neplatný název složky"), ("Socks5 Proxy", "Socks5 proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proxy"), ("Discovered", "Objeveno"), ("install_daemon_tip", "Pokud má být spouštěno při startu systému, je třeba nainstalovat systémovou službu."), ("Remote ID", "Vzdálené ID"), diff --git a/src/lang/da.rs b/src/lang/da.rs index c20f15128..9e28f4b4d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Tom"), ("Invalid folder name", "Ugyldigt mappenavn"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Fundet"), ("install_daemon_tip", "For at starte efter PC'en er startet op, skal du installere systemtjenesten"), ("Remote ID", "Fjern-ID"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 73262e267..ed97679b2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Keine Einträge"), ("Invalid folder name", "Ungültiger Ordnername"), ("Socks5 Proxy", "SOCKS5-Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s)-Proxy"), ("Discovered", "Im LAN erkannt"), ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), ("Remote ID", "Entfernte ID"), diff --git a/src/lang/el.rs b/src/lang/el.rs index 7d4966120..8fcb403b9 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Άδειο"), ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), + ("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"), ("Discovered", "Ανακαλύφθηκαν"), ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), ("Remote ID", "Απομακρυσμένο ID"), diff --git a/src/lang/en.rs b/src/lang/en.rs index ac6cd2cd3..35e4eff53 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -60,6 +60,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Favorites", "Add to favorites"), ("Remove from Favorites", "Remove from favorites"), ("Socks5 Proxy", "Socks5 proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proxy"), + ("Default protocol and port are Socks5 and 1080", "Default protocol and port are Socks5 and 1080"), ("install_daemon_tip", "For starting on boot, you need to install system service."), ("Are you sure to close the connection?", "Are you sure you want to close the connection?"), ("One-Finger Tap", "One-finger tap"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index e1bab260d..0e38c6d9f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Malplena"), ("Invalid folder name", "Dosiernomo nevalida"), ("Socks5 Proxy", "Socks5 prokura servilo"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"), ("Discovered", "Malkovritaj"), ("install_daemon_tip", ""), ("Remote ID", "Fora identigilo"), diff --git a/src/lang/es.rs b/src/lang/es.rs index f037ac2c1..92ebb67e9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Vacío"), ("Invalid folder name", "Nombre de carpeta incorrecto"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Descubierto"), ("install_daemon_tip", "Para comenzar en el encendido, debe instalar el servicio del sistema."), ("Remote ID", "ID remoto"), diff --git a/src/lang/et.rs b/src/lang/et.rs index 2e49ddee7..7bb359cd8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", ""), ("Invalid folder name", ""), ("Socks5 Proxy", "Socks5 proksi"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"), ("Discovered", ""), ("install_daemon_tip", "Süsteemikäivitusel käivitamiseks tuleb paigaldada süsteemiteenus."), ("Remote ID", ""), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6eb59377d..8cef3ff08 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "موردی وجود ندارد"), ("Invalid folder name", "نام پوشه نامعتبر است"), ("Socks5 Proxy", "Socks5 پروکسی"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) پروکسی"), ("Discovered", "پیدا شده"), ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), ("Remote ID", "شناسه راه دور"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ff644806e..4353b5889 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Vide"), ("Invalid folder name", "Nom de dossier invalide"), ("Socks5 Proxy", "Socks5 Agents"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Agents"), ("Discovered", "Découvert"), ("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."), ("Remote ID", "ID de l'appareil distant"), diff --git a/src/lang/he.rs b/src/lang/he.rs index 2da9339a2..d9b3d0c54 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", ""), ("Invalid folder name", ""), ("Socks5 Proxy", "פרוקסי Socks5"), + ("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"), ("Discovered", ""), ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), ("Remote ID", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index bdae7d9bb..6cd18ce80 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Üres"), ("Invalid folder name", "Helytelen mappa név"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Felfedezett"), ("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"), ("Remote ID", "Távoli azonosító"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 9a0fc988f..696e75b81 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Kosong"), ("Invalid folder name", "Nama folder tidak valid"), ("Socks5 Proxy", "Proksi Socks5"), + ("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"), ("Discovered", "Telah ditemukan"), ("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."), ("Remote ID", "ID Remote"), diff --git a/src/lang/it.rs b/src/lang/it.rs index a362e2348..bb27633ac 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Vuoto"), ("Invalid folder name", "Nome della cartella non valido"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Rilevate"), ("install_daemon_tip", "Per avviare il programma all'accensione, è necessario installarlo come servizio di sistema."), ("Remote ID", "ID remoto"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 400ae1329..6d1164115 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "空"), ("Invalid folder name", "無効なフォルダ名"), ("Socks5 Proxy", "SOCKS5プロキシ"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s)プロキシ"), ("Discovered", "探知済み"), ("install_daemon_tip", "起動時に開始するには、システムサービスをインストールする必要があります。"), ("Remote ID", "リモートのID"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index e0fc91a06..95c003de6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "비어 있음"), ("Invalid folder name", "유효하지 않은 폴더명"), ("Socks5 Proxy", "Socks5 프록시"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 프록시"), ("Discovered", "찾음"), ("install_daemon_tip", "부팅된 이후 시스템 서비스에 설치해야 합니다."), ("Remote ID", "원격 ID"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 7bffe6a2e..9ef2113cb 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Бос"), ("Invalid folder name", "Бұрыс бума атауы"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Табылды"), ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), ("Remote ID", "Қашықтағы ID"), diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 06d3ce2bd..5f21af1c1 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Tuščia"), ("Invalid folder name", "Neteisingas aplanko pavadinimas"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Aptikta tinkle"), ("install_daemon_tip", "Norėdami, kad RustDesk startuotų automatiškai, turite ją įdiegti"), ("Remote ID", "Nuotolinis ID"), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9e0b496ab..6e0fd5a84 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Tukšs"), ("Invalid folder name", "Nederīgs mapes nosaukums"), ("Socks5 Proxy", "Socks5 starpniekserveris"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) starpniekserveris"), ("Discovered", "Atklāts"), ("install_daemon_tip", "Lai palaistu pie startēšanas, ir jāinstalē sistēmas serviss."), ("Remote ID", "Attālais ID"), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d4251a554..e6d6b3e7b 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Tom"), ("Invalid folder name", "Ugyldig mappenavn"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Oppdaget"), ("install_daemon_tip", "For å starte når PC'en har startet opp, må du installere systemtjenesten"), ("Remote ID", "Fjern-ID"), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 705b34f16..23f7b721a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Leeg"), ("Invalid folder name", "Ongeldige mapnaam"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Ontdekt"), ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet u de systeemservice installeren."), ("Remote ID", "Externe ID"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 06adebc09..9f8124b15 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Pusto"), ("Invalid folder name", "Nieprawidłowa nazwa folderu"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Wykryte"), ("install_daemon_tip", "By uruchomić RustDesk przy starcie systemu, musisz zainstalować usługę systemową."), ("Remote ID", "Zdalne ID"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 76fa2c214..d483a7ed3 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Vazio"), ("Invalid folder name", "Nome de diretório inválido"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Descoberto"), ("install_daemon_tip", "Para inicialização junto do sistema, deve instalar o serviço de sistema."), ("Remote ID", "ID Remoto"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 2958878a5..cf71f8a57 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Vazio"), ("Invalid folder name", "Nome de diretório inválido"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Descoberto"), ("install_daemon_tip", "Para inicialização junto ao sistema, você deve instalar o serviço de sistema."), ("Remote ID", "ID Remoto"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c7780c371..abf0e078c 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Gol"), ("Invalid folder name", "Denumire folder nevalidă"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Descoperite"), ("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."), ("Remote ID", "ID dispozitiv la distanță"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 3e28744c5..ab9fcc0b1 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Пусто"), ("Invalid folder name", "Недопустимое имя папки"), ("Socks5 Proxy", "SOCKS5-прокси"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s)-прокси"), ("Discovered", "Найдено"), ("install_daemon_tip", "Для запуска при загрузке необходимо установить системную службу"), ("Remote ID", "Удалённый ID"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 1abfe46a3..3c63acdf5 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Prázdne"), ("Invalid folder name", "Neplatný názov adresára"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Objavené"), ("install_daemon_tip", "Ak chcete, aby sa spúšťal pri štarte systému, musíte nainštalovať systémovú službu."), ("Remote ID", "Vzdialené ID"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index cdc37594d..6559c0cf1 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Prazno"), ("Invalid folder name", "Napačno ime mape"), ("Socks5 Proxy", "Socks5 posredniški strežnik"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) posredniški strežnik"), ("Discovered", "Odkriti"), ("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"), ("Remote ID", "Oddaljeni ID"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6a7ee5d63..1a1f9c291 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Bosh"), ("Invalid folder name", "Emri i dosjes i pavlefshëm"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "I pambuluar"), ("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"), ("Remote ID", "ID në distancë"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index e03383415..18918f645 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Prazno"), ("Invalid folder name", "Pogrešno ime direktorijuma"), ("Socks5 Proxy", "Socks5 proksi"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"), ("Discovered", "Otkriveno"), ("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."), ("Remote ID", "Udaljeni ID"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index b78677fe6..c2a0a73f3 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Tom"), ("Invalid folder name", "Ogiltigt mappnamn"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Upptäckt"), ("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."), ("Remote ID", "Fjärr ID"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 02aa92450..3a2a007db 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", ""), ("Invalid folder name", ""), ("Socks5 Proxy", ""), + ("Socks5/Http(s) Proxy", ""), ("Discovered", ""), ("install_daemon_tip", ""), ("Remote ID", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index ce262d19a..05285579d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "ว่างเปล่า"), ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), ("Socks5 Proxy", "พรอกซี Socks5"), + ("Socks5/Http(s) Proxy", "พรอกซี Socks5/Http(s)"), ("Discovered", "ค้นพบ"), ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), ("Remote ID", "ID ปลายทาง"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 27fca3807..0b2de53d5 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Boş"), ("Invalid folder name", "Geçersiz klasör adı"), ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Keşfedilenler"), ("install_daemon_tip", "Başlangıçta başlamak için sistem hizmetini yüklemeniz gerekir."), ("Remote ID", "Uzak ID"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4e86fe760..c7ae7e427 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "空空如也"), ("Invalid folder name", "資料夾名稱無效"), ("Socks5 Proxy", "Socks5 代理伺服器"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 代理伺服器"), ("Discovered", "已探索"), ("install_daemon_tip", "若要在開機時啟動,您需要安裝系統服務。"), ("Remote ID", "遠端 ID"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 63a8800f5..6d18e7a70 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Пусто"), ("Invalid folder name", "Неприпустима назва теки"), ("Socks5 Proxy", "Проксі-сервер Socks5"), + ("Socks5/Http(s) Proxy", "Проксі-сервер Socks5/Http(s)"), ("Discovered", "Знайдено"), ("install_daemon_tip", "Для запуску під час завантаження, вам необхідно встановити системну службу"), ("Remote ID", "Віддалений ідентифікатор"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 96cbbee7e..117442756 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -239,6 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Trống"), ("Invalid folder name", "Tên thư mục không hợp lệ"), ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), ("Discovered", "Đuợc phát hiện"), ("install_daemon_tip", "Để chạy lúc khởi động máy, bạn cần phải cài dịch vụ hệ thống."), ("Remote ID", "ID từ xa"), diff --git a/src/naming.rs b/src/naming.rs index bae4d0f9d..0436a23f2 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -1,6 +1,5 @@ mod custom_server; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use hbb_common::ResultType; +use hbb_common::{ResultType, base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}}; use custom_server::*; fn gen_name(lic: &CustomServer) -> ResultType { diff --git a/src/plugin/callback_msg.rs b/src/plugin/callback_msg.rs index e634595e8..2a23b03dd 100644 --- a/src/plugin/callback_msg.rs +++ b/src/plugin/callback_msg.rs @@ -1,4 +1,5 @@ use super::*; +use crate::hbbs_http::create_http_client; use crate::{ flutter::{self, APP_TYPE_CM, APP_TYPE_MAIN, SESSIONS}, ui_interface::get_api_server, @@ -280,7 +281,7 @@ fn request_plugin_sign(id: String, msg_to_rustdesk: MsgToRustDesk) -> PluginRetu ); thread::spawn(move || { let sign_url = format!("{}/lic/web/api/plugin-sign", get_api_server()); - let client = reqwest::blocking::Client::new(); + let client = create_http_client(); let req = PluginSignReq { plugin_id: id.clone(), version: signature_data.version, diff --git a/src/plugin/manager.rs b/src/plugin/manager.rs index 507df441e..25e73541e 100644 --- a/src/plugin/manager.rs +++ b/src/plugin/manager.rs @@ -3,6 +3,7 @@ use super::{desc::Meta as PluginMeta, ipc::InstallStatus, *}; use crate::flutter; +use crate::hbbs_http::create_http_client; use hbb_common::{allow_err, bail, log, tokio, toml}; use serde_derive::{Deserialize, Serialize}; use serde_json; @@ -67,7 +68,7 @@ fn get_source_plugins() -> HashMap { let mut plugins = HashMap::new(); for source in get_plugin_source_list().into_iter() { let url = format!("{}/meta.toml", source.url); - match reqwest::blocking::get(&url) { + match create_http_client().get(&url).send() { Ok(resp) => { if !resp.status().is_success() { log::error!( @@ -441,6 +442,7 @@ fn update_uninstall_id_set(set: HashSet) -> ResultType<()> { // install process pub(super) mod install { use super::IPC_PLUGIN_POSTFIX; + use crate::hbbs_http::create_http_client; use crate::{ ipc::{connect, Data}, plugin::ipc::{InstallStatus, Plugin}, @@ -469,7 +471,7 @@ pub(super) mod install { } fn download_to_file(url: &str, file: File) -> ResultType<()> { - let resp = match reqwest::blocking::get(url) { + let resp = match create_http_client().get(url).send() { Ok(resp) => resp, Err(e) => { bail!("get plugin from '{}', {}", url, e); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 0bdd6388d..58c4fee15 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -15,6 +15,7 @@ use hbb_common::{ config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT}, futures::future::join_all, log, + proxy::Proxy, protobuf::Message as _, rendezvous_proto::*, sleep, @@ -388,7 +389,14 @@ impl RendezvousMediator { pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { log::info!("start rendezvous mediator of {}", host); - if cfg!(debug_assertions) && option_env!("TEST_TCP").is_some() { + //If the investment agent type is http or https, then tcp forwarding is enabled. + let is_http_proxy = if let Some(conf) = Config::get_socks() { + let proxy = Proxy::from_conf(&conf, None)?; + proxy.is_http_or_https() + } else { + false + }; + if (cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()) || is_http_proxy { Self::start_tcp(server, host).await } else { Self::start_udp(server, host).await diff --git a/src/ui.rs b/src/ui.rs index 10aefe5ff..2e9ea2f91 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -548,6 +548,10 @@ impl UI { change_id_shared(id, old_id); } + fn http_request(&self, url: String, method: String, body: Option, header: String) { + http_request(url, method, body, header) + } + fn post_request(&self, url: String, body: String, header: String) { post_request(url, body, header) } @@ -560,6 +564,10 @@ impl UI { get_async_job_status() } + fn get_http_status(&self, url: String) -> Option { + get_async_http_status(url) + } + fn t(&self, name: String) -> String { crate::client::translate(name) } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d8a9996c0..313b6e562 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -4,7 +4,7 @@ use hbb_common::{ allow_err, bytes::Bytes, config::{ - self, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, HARD_SETTINGS, RENDEZVOUS_PORT, + self, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT, }, directories_next, futures::future::join_all, @@ -65,6 +65,7 @@ lazy_static::lazy_static! { id: "".to_owned(), })); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); + static ref ASYNC_HTTP_STATUS : Arc>> = Arc::new(Mutex::new(HashMap::new())); static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); } @@ -421,6 +422,16 @@ pub fn set_socks(proxy: String, username: String, password: String) { .ok(); } +#[inline] +pub fn get_proxy_status() -> bool { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_proxy_status(); + + // Currently, only the desktop version has proxy settings. + #[cfg(any(target_os = "android", target_os = "ios"))] + return false; +} + #[cfg(any(target_os = "android", target_os = "ios"))] pub fn set_socks(_: String, _: String, _: String) {} @@ -708,6 +719,28 @@ pub fn change_id(id: String) { }); } +#[inline] +pub fn http_request(url: String, method: String, body: Option, header: String) { + // Respond to concurrent requests for resources + let current_request = ASYNC_HTTP_STATUS.clone(); + current_request.lock().unwrap().insert(url.clone()," ".to_owned()); + std::thread::spawn(move || { + let res = match crate::http_request_sync(url.clone(), method, body, header) { + Err(err) => { log::error!("{}", err); err.to_string() }, + Ok(text) => text, + }; + current_request.lock().unwrap().insert(url,res); + }); +} +#[inline] +pub fn get_async_http_status(url: String) -> Option { + match ASYNC_HTTP_STATUS.lock().unwrap().get(&url) { + None => {None} + Some(_str) => {Some(_str.to_string())} + } +} + + #[inline] pub fn post_request(url: String, body: String, header: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); From 3811f410760a12c7770cec376bed5c108f227562 Mon Sep 17 00:00:00 2001 From: Sahil Yeole <73148455+sahilyeole@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:56:02 +0530 Subject: [PATCH 02/35] Feat: Follow remote cursor and window focus | Auto display switch (#7717) * feat: auto switch display on follow remote cursor Signed-off-by: Sahil Yeole * feat: auto switch display on follow remote window focus Signed-off-by: Sahil Yeole * fix build and remove unused imports Signed-off-by: Sahil Yeole * fix build Signed-off-by: Sahil Yeole * fix build Signed-off-by: Sahil Yeole * fix linux get_focused_window_id Signed-off-by: Sahil Yeole * lock show remote cursor when follow remote cursor is enabled Signed-off-by: Sahil Yeole * fix config Signed-off-by: Sahil Yeole * prevent auto display switch on show all display and displays as individual windows Signed-off-by: Sahil Yeole * fix options Signed-off-by: Sahil Yeole * fix options Signed-off-by: Sahil Yeole * remove unused function Signed-off-by: Sahil Yeole * remove unwraps and improve iterations Signed-off-by: Sahil Yeole * set updateCursorPos to false to avoid interrupting remote cursor Signed-off-by: Sahil Yeole * update lang Signed-off-by: Sahil Yeole * fix web build Signed-off-by: Sahil Yeole * update checks for options and enable in view mode Signed-off-by: Sahil Yeole * use focused display index for window focus service Signed-off-by: Sahil Yeole * use window center for windows display focused Signed-off-by: Sahil Yeole * remove unused imports Signed-off-by: Sahil Yeole * fix build Signed-off-by: Sahil Yeole * use libxdo instead of xdotool Signed-off-by: Sahil Yeole * fix multi monitor check Signed-off-by: Sahil Yeole * enable show cursor when follow cursor is default Signed-off-by: Sahil Yeole * remove show_all_displays,use runtime state instead Signed-off-by: Sahil Yeole * fix show cursor lock state on default Signed-off-by: Sahil Yeole * remove view mode with follow options Signed-off-by: Sahil Yeole * fix build Signed-off-by: Sahil Yeole * use separate message for follow current display Signed-off-by: Sahil Yeole * fix options Signed-off-by: Sahil Yeole * sciter support for follow remote cursor and window Signed-off-by: Sahil Yeole * add check for ui session handlers count Signed-off-by: Sahil Yeole * use cached displays and remove peer info write Signed-off-by: Sahil Yeole * No follow options when show all displays Signed-off-by: Sahil Yeole * No follow options when multi ui session Signed-off-by: Sahil Yeole * turn off follow options when not used|prevent msgs Signed-off-by: Sahil Yeole * use window center for switch in linux Signed-off-by: Sahil Yeole * use subbed display count to prevent switch msgs Signed-off-by: Sahil Yeole * fix build Signed-off-by: Sahil Yeole * fix web build Signed-off-by: Sahil Yeole * move subbed displays count Signed-off-by: Sahil Yeole * fix build Signed-off-by: Sahil Yeole * add noperms for window focus Signed-off-by: Sahil Yeole * add subscribe for window focus Signed-off-by: Sahil Yeole * remove window_focus message and unsub on multi ui Signed-off-by: Sahil Yeole * add multi ui session field Signed-off-by: Sahil Yeole --------- Signed-off-by: Sahil Yeole Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/lib/common/shared_state.dart | 25 ++++++ .../lib/common/widgets/setting_widgets.dart | 2 + flutter/lib/common/widgets/toolbar.dart | 81 ++++++++++++++++++- .../lib/desktop/widgets/remote_toolbar.dart | 20 ++++- flutter/lib/mobile/pages/remote_page.dart | 23 +++++- flutter/lib/models/model.dart | 27 ++++++- flutter/lib/web/bridge.dart | 4 + libs/hbb_common/protos/message.proto | 3 + libs/hbb_common/src/config.rs | 19 +++++ src/client.rs | 26 ++++++ src/client/io_loop.rs | 4 +- src/flutter.rs | 15 ++++ src/flutter_ffi.rs | 8 ++ src/lang/ar.rs | 2 + src/lang/bg.rs | 2 + src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/el.rs | 2 + src/lang/en.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/et.rs | 2 + src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/he.rs | 2 + src/lang/hr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/lt.rs | 2 + src/lang/lv.rs | 2 + src/lang/nb.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + src/lib.rs | 8 +- src/platform/linux.rs | 62 +++++++++++++- src/platform/macos.rs | 23 +++++- src/platform/windows.rs | 20 ++++- src/server.rs | 11 +++ src/server/connection.rs | 70 +++++++++++++++- src/server/display_service.rs | 1 - src/server/input_service.rs | 62 ++++++++++++-- src/ui/header.tis | 13 ++- src/ui/remote.rs | 4 + src/ui_session_interface.rs | 8 ++ 66 files changed, 597 insertions(+), 26 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 2aa9353f9..c62ad671e 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -168,6 +168,29 @@ class ShowRemoteCursorState { static RxBool find(String id) => Get.find(tag: tag(id)); } +class ShowRemoteCursorLockState { + static String tag(String id) => 'show_remote_cursor_lock_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + class KeyboardEnabledState { static String tag(String id) => 'keyboard_enabled_$id'; @@ -315,6 +338,7 @@ initSharedStates(String id) { CurrentDisplayState.init(id); KeyboardEnabledState.init(id); ShowRemoteCursorState.init(id); + ShowRemoteCursorLockState.init(id); RemoteCursorMovedState.init(id); FingerprintState.init(id); PeerBoolOption.init(id, 'zoom-cursor', () => false); @@ -327,6 +351,7 @@ removeSharedStates(String id) { BlockInputState.delete(id); CurrentDisplayState.delete(id); ShowRemoteCursorState.delete(id); + ShowRemoteCursorLockState.delete(id); KeyboardEnabledState.delete(id); RemoteCursorMovedState.delete(id); FingerprintState.delete(id); diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index c6c8f6443..56340ec54 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -212,6 +212,8 @@ List<(String, String)> otherDefaultSettings() { if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar), if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'), ('Show remote cursor', 'show_remote_cursor'), + ('Follow remote cursor', 'follow_remote_cursor'), + ('Follow remote window focus', 'follow_remote_window'), if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'), ('Show quality monitor', 'show_quality_monitor'), ('Mute', 'disable_audio'), diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index e38e3fb75..a37111541 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -371,7 +371,7 @@ Future>> toolbarCodec( ]; } -Future> toolbarDisplayToggle( +Future> toolbarCursor( BuildContext context, String id, FFI ffi) async { List v = []; final ffiModel = ffi.ffiModel; @@ -384,12 +384,17 @@ Future> toolbarDisplayToggle( !ffi.canvasModel.cursorEmbedded && !pi.isWayland) { final state = ShowRemoteCursorState.find(id); + final lockState = ShowRemoteCursorLockState.find(id); final enabled = !ffiModel.viewOnly; final option = 'show-remote-cursor'; + if (pi.currentDisplay == kAllDisplayValue || + bind.sessionIsMultiUiSession(sessionId: sessionId)) { + lockState.value = false; + } v.add(TToggleMenu( child: Text(translate('Show remote cursor')), value: state.value, - onChanged: enabled + onChanged: enabled && !lockState.value ? (value) async { if (value == null) return; await bind.sessionToggleOption( @@ -399,6 +404,67 @@ Future> toolbarDisplayToggle( } : null)); } + // follow remote cursor + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland && + versionCmp(pi.version, "1.2.4") >= 0 && + pi.displays.length > 1 && + pi.currentDisplay != kAllDisplayValue && + !bind.sessionIsMultiUiSession(sessionId: sessionId)) { + final option = 'follow-remote-cursor'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + final showCursorOption = 'show-remote-cursor'; + final showCursorState = ShowRemoteCursorState.find(id); + final showCursorLockState = ShowRemoteCursorLockState.find(id); + final showCursorEnabled = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + showCursorLockState.value = value; + if (value && !showCursorEnabled) { + await bind.sessionToggleOption( + sessionId: sessionId, value: showCursorOption); + showCursorState.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + } + v.add(TToggleMenu( + child: Text(translate('Follow remote cursor')), + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + showCursorLockState.value = value; + if (!showCursorEnabled) { + await bind.sessionToggleOption( + sessionId: sessionId, value: showCursorOption); + showCursorState.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + } + })); + } + // follow remote window focus + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland && + versionCmp(pi.version, "1.2.4") >= 0 && + pi.displays.length > 1 && + pi.currentDisplay != kAllDisplayValue && + !bind.sessionIsMultiUiSession(sessionId: sessionId)) { + final option = 'follow-remote-window'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + v.add(TToggleMenu( + child: Text(translate('Follow remote window focus')), + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + })); + } // zoom cursor final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? ''; if (!isMobile && @@ -417,6 +483,17 @@ Future> toolbarDisplayToggle( }, )); } + return v; +} + +Future> toolbarDisplayToggle( + BuildContext context, String id, FFI ffi) async { + List v = []; + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final perms = ffiModel.permissions; + final sessionId = ffi.sessionId; + // show quality monitor final option = 'show-quality-monitor'; v.add(TToggleMenu( diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index fd048337c..aa1b346fa 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1045,7 +1045,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { @override Widget build(BuildContext context) { _screenAdjustor.updateScreen(); - menuChildrenGetter() { final menuChildren = [ _screenAdjustor.adjustWindow(context), @@ -1069,6 +1068,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, ), Divider(), + cursorToggles(), + Divider(), toggles(), ]; // privacy mode @@ -1212,6 +1213,23 @@ class _DisplayMenuState extends State<_DisplayMenu> { }); } + cursorToggles() { + return futureBuilder( + future: toolbarCursor(context, id, ffi), + hasData: (data) { + final v = data as List; + if (v.isEmpty) return Offstage(); + return Column( + children: v + .map((e) => CkbMenuButton( + value: e.value, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList()); + }); + } + toggles() { return futureBuilder( future: toolbarDisplayToggle(context, id, ffi), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index a6f4fd389..7cdb7471c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -836,6 +836,7 @@ void showOptions( List> imageQualityRadios = await toolbarImageQuality(context, id, gFFI); List> codecRadios = await toolbarCodec(context, id, gFFI); + List cursorToggles = await toolbarCursor(context, id, gFFI); List displayToggles = await toolbarDisplayToggle(context, id, gFFI); @@ -876,8 +877,23 @@ void showOptions( })), if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), ]; + final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList(); + final cursorTogglesList = cursorToggles + .asMap() + .entries + .map((e) => Obx(() => CheckboxListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + value: rxCursorToggleValues[e.key].value, + onChanged: (v) { + e.value.onChanged?.call(v); + if (v != null) rxCursorToggleValues[e.key].value = v; + }, + title: e.value.child))) + .toList(); + final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); - final toggles = displayToggles + final displayTogglesList = displayToggles .asMap() .entries .map((e) => Obx(() => CheckboxListTile( @@ -890,6 +906,11 @@ void showOptions( }, title: e.value.child))) .toList(); + final toggles = [ + ...cursorTogglesList, + if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border), + ...displayTogglesList, + ]; Widget privacyModeWidget = Offstage(); if (privacyModeList.length > 1) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4da7d54cd..65e29b2fa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -367,6 +367,8 @@ class FfiModel with ChangeNotifier { } } else if (name == 'sync_peer_option') { _handleSyncPeerOption(evt, peerId); + } else if (name == 'follow_current_display') { + handleFollowCurrentDisplay(evt, sessionId, peerId); } else { debugPrint('Unknown event name: $name'); } @@ -440,7 +442,7 @@ class FfiModel with ChangeNotifier { } } - updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) { + updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) { final newRect = displaysRect(); if (newRect == null) { return; @@ -1040,9 +1042,30 @@ class FfiModel with ChangeNotifier { json.encode(_pi.platformAdditions); } + handleFollowCurrentDisplay( + Map evt, SessionID sessionId, String peerId) async { + if (evt['display_idx'] != null) { + if (pi.currentDisplay == kAllDisplayValue) { + return; + } + _pi.currentDisplay = int.parse(evt['display_idx']); + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + bind.sessionSwitchDisplay( + isDesktop: isDesktop, + sessionId: sessionId, + value: Int32List.fromList([_pi.currentDisplay]), + ); + } + notifyListeners(); + } + // Directly switch to the new display without waiting for the response. switchToNewDisplay(int display, SessionID sessionId, String peerId, - {bool updateCursorPos = true}) { + {bool updateCursorPos = false}) { // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. parent.target?.recordingModel.onClose(); // no need to wait for the response diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 91e7e9711..1ccc266ac 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -346,6 +346,10 @@ class RustdeskImpl { return mode == kKeyLegacyMode; } + bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) { + return false; + } + Future sessionSetCustomImageQuality( {required UuidValue sessionId, required int value, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 555b1df43..483e12c13 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -603,6 +603,8 @@ message OptionMessage { // Resolution custom_resolution = 13; // BoolOption support_windows_specific_session = 14; // starting from 15 please, do not use removed fields + BoolOption follow_remote_cursor = 15; + BoolOption follow_remote_window = 16; } message TestDelay { @@ -765,6 +767,7 @@ message Misc { uint32 selected_sid = 35; DisplayResolution change_display_resolution = 36; MessageQuery message_query = 37; + int32 follow_current_display = 38; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 4acfb2cdf..1660fbb92 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -281,6 +281,10 @@ pub struct PeerConfig { pub enable_file_transfer: EnableFileTransfer, #[serde(flatten)] pub show_quality_monitor: ShowQualityMonitor, + #[serde(flatten)] + pub follow_remote_cursor: FollowRemoteCursor, + #[serde(flatten)] + pub follow_remote_window: FollowRemoteWindow, #[serde( default, deserialize_with = "deserialize_string", @@ -353,6 +357,8 @@ impl Default for PeerConfig { disable_clipboard: Default::default(), enable_file_transfer: Default::default(), show_quality_monitor: Default::default(), + follow_remote_cursor: Default::default(), + follow_remote_window: Default::default(), keyboard_mode: Default::default(), view_only: Default::default(), reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), @@ -1258,6 +1264,19 @@ serde_field_bool!( default_show_remote_cursor, "ShowRemoteCursor::default_show_remote_cursor" ); +serde_field_bool!( + FollowRemoteCursor, + "follow_remote_cursor", + default_follow_remote_cursor, + "FollowRemoteCursor::default_follow_remote_cursor" +); + +serde_field_bool!( + FollowRemoteWindow, + "follow_remote_window", + default_follow_remote_window, + "FollowRemoteWindow::default_follow_remote_window" +); serde_field_bool!( ShowQualityMonitor, "show_quality_monitor", diff --git a/src/client.rs b/src/client.rs index 5cf16c7ce..c765c02cf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1456,6 +1456,22 @@ impl LoginConfigHandler { BoolOption::No }) .into(); + } else if name == "follow-remote-cursor" { + config.follow_remote_cursor.v = !config.follow_remote_cursor.v; + option.follow_remote_cursor = (if config.follow_remote_cursor.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "follow-remote-window" { + config.follow_remote_window.v = !config.follow_remote_window.v; + option.follow_remote_window = (if config.follow_remote_window.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); } else if name == "disable-audio" { config.disable_audio.v = !config.disable_audio.v; option.disable_audio = (if config.disable_audio.v { @@ -1601,6 +1617,12 @@ impl LoginConfigHandler { if view_only || self.get_toggle_option("show-remote-cursor") { msg.show_remote_cursor = BoolOption::Yes.into(); } + if self.get_toggle_option("follow-remote-cursor") { + msg.follow_remote_cursor = BoolOption::Yes.into(); + } + if self.get_toggle_option("follow-remote-window") { + msg.follow_remote_window = BoolOption::Yes.into(); + } if !view_only && self.get_toggle_option("lock-after-session-end") { msg.lock_after_session_end = BoolOption::Yes.into(); } @@ -1692,6 +1714,10 @@ impl LoginConfigHandler { self.config.allow_swap_key.v } else if name == "view-only" { self.config.view_only.v + } else if name == "follow-remote-cursor" { + self.config.follow_remote_cursor.v + } else if name == "follow-remote-window" { + self.config.follow_remote_window.v } else { !self.get_option(name).is_empty() } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a8fa4b67b..0af3f92d7 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1504,7 +1504,9 @@ impl Remote { log::info!("update supported encoding:{:?}", e); self.handler.lc.write().unwrap().supported_encoding = e; } - + Some(misc::Union::FollowCurrentDisplay(d_idx)) => { + self.handler.set_current_display(d_idx); + } _ => {} }, Some(message::Union::TestDelay(t)) => { diff --git a/src/flutter.rs b/src/flutter.rs index aee4ff67b..ce706b447 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -882,6 +882,21 @@ impl InvokeUiSession for FlutterHandler { ); } + fn is_multi_ui_session(&self) -> bool { + self.session_handlers.read().unwrap().len() > 1 + } + + fn set_current_display(&self, disp_idx: i32) { + if self.is_multi_ui_session() { + return; + } + self.push_event( + "follow_current_display", + &[("display_idx", &disp_idx.to_string())], + &[], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 016263e74..518dd47dd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -213,6 +213,14 @@ pub fn session_refresh(session_id: SessionID, display: usize) { } } +pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.is_multi_ui_session()) + } else { + SyncReturn(false) + } +} + pub fn session_record_screen( session_id: SessionID, start: bool, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 2b3d9faec..626f1bcfb 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 6384083d6..9aa3ddac9 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8cf9738c4..2ac1fbbae 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 25851db83..26d2f7b49 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -604,5 +604,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "打开 Web 控制台以执行更多操作"), ("allow-only-conn-window-open-tip", "仅当 RustDesk 窗口打开时允许连接"), ("no_need_privacy_mode_no_physical_displays_tip", "没有物理显示器,没必要使用隐私模式。"), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 10c78f118..62ca5b4ac 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Více na webové konzoli"), ("allow-only-conn-window-open-tip", "Povolit připojení pouze v případě, že je otevřené okno RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Žádné fyzické displeje, není třeba používat režim soukromí."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9e28f4b4d..d05c0cd9b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ed97679b2..8c61ba866 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Mehr über Webkonsole"), ("allow-only-conn-window-open-tip", "Verbindung nur zulassen, wenn das RustDesk-Fenster geöffnet ist"), ("no_need_privacy_mode_no_physical_displays_tip", "Keine physischen Bildschirme; keine Notwendigkeit, den Datenschutzmodus zu verwenden."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 8fcb403b9..22953dd74 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 35e4eff53..4026f0cd9 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -222,5 +222,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "More on web console"), ("allow-only-conn-window-open-tip", "Only allow connection if RustDesk window is open"), ("no_need_privacy_mode_no_physical_displays_tip", "No physical displays, no need to use the privacy mode."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0e38c6d9f..44a1d01a0 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 92ebb67e9..a1ca3ff9e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Más en consola web"), ("allow-only-conn-window-open-tip", "Permitir la conexión solo si la ventana RusDesk está abierta"), ("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 7bb359cd8..6dad57845 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8cef3ff08..872f8f5d0 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4353b5889..43d21b6d0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index d9b3d0c54..e5b4429fe 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index def491c52..cf65fe093 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -602,5 +602,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Više na web konzoli"), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 6cd18ce80..5c568c32d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 696e75b81..cca611212 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index bb27633ac..cf08daab9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Altre info sulla console web"), ("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"), ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6d1164115..7d154e650 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 95c003de6..09a4f8101 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9ef2113cb..8642012df 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 5f21af1c1..8864408a3 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 6e0fd5a84..2afb32aae 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Vairāk par tīmekļa konsoli"), ("allow-only-conn-window-open-tip", "Atļaut savienojumu tikai tad, ja ir atvērts RustDesk logs"), ("no_need_privacy_mode_no_physical_displays_tip", "Nav fizisku displeju, nav jāizmanto privātuma režīms."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index e6d6b3e7b..2bc2f4834 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 23f7b721a..026cc92b8 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Meer over de webconsole"), ("allow-only-conn-window-open-tip", "Alleen verbindingen toestaan als het RustDesk-venster geopend is"), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9f8124b15..7f4d5de84 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Więcej w konsoli web"), ("allow-only-conn-window-open-tip", "Zezwalaj na połączenie tylko wtedy, gdy okno RustDesk jest otwarte"), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d483a7ed3..70baf3248 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cf71f8a57..c7a0126a8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index abf0e078c..eca66abc0 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ab9fcc0b1..a1e5a7c6b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Больше в веб-консоли"), ("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 3c63acdf5..450956ef1 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Viac na webovej konzole"), ("allow-only-conn-window-open-tip", "Povoliť pripojenie iba vtedy, ak je otvorené okno aplikácie RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Žiadne fyzické displeje, nie je potrebné používať režim ochrany osobných údajov."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6559c0cf1..4c6e4dcb3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1a1f9c291..dba37a298 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 18918f645..c1ddb69e1 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c2a0a73f3..8bc643257 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 3a2a007db..86dbdd4f1 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 05285579d..558b7ed12 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 0b2de53d5..bf8d03f0c 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index c7ae7e427..5181d3074 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "打開 Web 控制台以進行更多操作"), ("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 6d18e7a70..4c668cec1 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Детальніше про веб-консоль"), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 117442756..a70679062 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 105e96fa8..2bdef3bbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,9 @@ mod keyboard; /// cbindgen:ignore pub mod platform; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; +pub use platform::{ + get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, start_os_service, +}; #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore mod server; @@ -36,15 +38,15 @@ pub mod flutter; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] pub mod flutter_ffi; use common::*; +mod auth_2fa; #[cfg(feature = "cli")] pub mod cli; #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] pub mod core_main; -mod lang; mod custom_server; +mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -mod auth_2fa; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 3c9b6c57f..aac7033da 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -11,7 +11,7 @@ use hbb_common::{ config::Config, libc::{c_char, c_int, c_long, c_void}, log, - message_proto::Resolution, + message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, }; use std::{ @@ -53,6 +53,20 @@ extern "C" { screen_num: *mut c_int, ) -> c_int; fn xdo_new(display: *const c_char) -> Xdo; + fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int; + fn xdo_get_window_location( + xdo: Xdo, + window: *mut c_void, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, + ) -> c_int; + fn xdo_get_window_size( + xdo: Xdo, + window: *mut c_void, + width: *mut c_int, + height: *mut c_int, + ) -> c_int; } #[link(name = "X11")] @@ -119,6 +133,50 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { pub fn reset_input_cache() {} +pub fn get_focused_display(displays: Vec) -> Option { + let mut res = None; + XDO.with(|xdo| { + if let Ok(xdo) = xdo.try_borrow_mut() { + if xdo.is_null() { + return; + } + let mut x: c_int = 0; + let mut y: c_int = 0; + let mut width: c_int = 0; + let mut height: c_int = 0; + let mut window: *mut c_void = std::ptr::null_mut(); + + unsafe { + if xdo_get_active_window(*xdo, &mut window) != 0 { + return; + } + if xdo_get_window_location( + *xdo, + window, + &mut x as _, + &mut y as _, + std::ptr::null_mut(), + ) != 0 + { + return; + } + if xdo_get_window_size(*xdo, window, &mut width as _, &mut height as _) != 0 { + return; + } + let center_x = x + width / 2; + let center_y = y + height / 2; + res = displays.iter().position(|d| { + center_x >= d.x + && center_x < d.x + d.width + && center_y >= d.y + && center_y < d.y + d.height + }); + } + } + }); + res +} + pub fn get_cursor() -> ResultType> { let mut res = None; DISPLAY.with(|conn| { @@ -1228,7 +1286,7 @@ mod desktop { if !home.is_empty() { assert_eq!(d.home, home); } else { - // + // } } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index e8e91ff2c..92991cdcf 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,8 +17,13 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::sysinfo::{Pid, Process, ProcessRefreshKind, System}; -use hbb_common::{anyhow::anyhow, bail, log, message_proto::Resolution}; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, log, + message_proto::{DisplayInfo, Resolution}, + sysinfo::{Pid, Process, ProcessRefreshKind, System}, +}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -302,6 +307,20 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { */ } +pub fn get_focused_display(displays: Vec) -> Option { + unsafe { + let main_screen: id = msg_send![class!(NSScreen), mainScreen]; + let screen: id = msg_send![main_screen, deviceDescription]; + let id: id = + msg_send![screen, objectForKey: NSString::alloc(nil).init_str("NSScreenNumber")]; + let display_name: u32 = msg_send![id, unsignedIntValue]; + + displays + .iter() + .position(|d| d.name == display_name.to_string()) + } +} + pub fn get_cursor() -> ResultType> { unsafe { let seed = CGSCurrentCursorSeed(); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 50a48b153..3072c5623 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,7 +12,7 @@ use hbb_common::{ bail, config::{self, Config}, log, - message_proto::{Resolution, WindowsSession}, + message_proto::{DisplayInfo, Resolution, WindowsSession}, sleep, timeout, tokio, }; use std::process::{Command, Stdio}; @@ -65,6 +65,24 @@ use windows_service::{ use winreg::enums::*; use winreg::RegKey; +pub fn get_focused_display(displays: Vec) -> Option { + unsafe { + let hWnd = GetForegroundWindow(); + let mut rect: RECT = mem::zeroed(); + if GetWindowRect(hWnd, &mut rect as *mut RECT) == 0 { + return None; + } + displays.iter().position(|display| { + let center_x = rect.left + (rect.right - rect.left) / 2; + let center_y = rect.top + (rect.bottom - rect.top) / 2; + center_x >= display.x + && center_x <= display.x + display.width + && center_y >= display.y + && center_y <= display.y + display.height + }) + } +} + pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { #[allow(invalid_value)] diff --git a/src/server.rs b/src/server.rs index 9345936e0..d54629345 100644 --- a/src/server.rs +++ b/src/server.rs @@ -50,6 +50,7 @@ pub const NAME: &'static str = ""; pub mod input_service { pub const NAME_CURSOR: &'static str = ""; pub const NAME_POS: &'static str = ""; +pub const NAME_WINDOW_FOCUS: &'static str = ""; } } } @@ -105,6 +106,7 @@ pub fn new() -> ServerPtr { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); + server.add_service(Box::new(input_service::new_window_focus())); } } Arc::new(RwLock::new(server)) @@ -354,6 +356,15 @@ impl Server { } } + fn get_subbed_displays_count(&self, conn_id: i32) -> usize { + self.services + .keys() + .filter(|k| { + Self::is_video_service_name(k) && self.services.get(*k).unwrap().is_subed(conn_id) + }) + .count() + } + fn capture_displays( &mut self, conn: ConnInner, diff --git a/src/server/connection.rs b/src/server/connection.rs index 9cd9221c9..05a0054d5 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -31,8 +31,7 @@ use hbb_common::platform::linux::run_cmds; use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::Config, - fs, - fs::can_enable_overwrite_detection, + fs::{self, can_enable_overwrite_detection}, futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, @@ -241,6 +240,9 @@ pub struct Connection { delayed_read_dir: Option<(String, bool)>, #[cfg(target_os = "macos")] retina: Retina, + follow_remote_cursor: bool, + follow_remote_window: bool, + multi_ui_session: bool, } impl ConnInner { @@ -348,6 +350,9 @@ impl Connection { network_delay: 0, lock_after_session_end: false, show_remote_cursor: false, + follow_remote_cursor: false, + follow_remote_window: false, + multi_ui_session: false, ip: "".to_owned(), disable_audio: false, #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -666,8 +671,14 @@ impl Connection { #[cfg(target_os = "macos")] conn.retina.set_displays(&_pi.displays); } - #[cfg(target_os = "macos")] Some(message::Union::CursorPosition(pos)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if conn.follow_remote_cursor { + conn.handle_cursor_switch_display(pos.clone()).await; + } + } + #[cfg(target_os = "macos")] if let Some(new_msg) = conn.retina.on_cursor_pos(&pos, conn.display_idx) { msg = Arc::new(new_msg); } @@ -1308,6 +1319,9 @@ impl Connection { if !self.show_remote_cursor { noperms.push(NAME_POS); } + if !self.follow_remote_window { + noperms.push(NAME_WINDOW_FOCUS); + } if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { noperms.push(super::clipboard_service::NAME); } @@ -2581,6 +2595,14 @@ impl Connection { } else { lock.capture_displays(self.inner.clone(), set, true, true); } + self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; + if self.follow_remote_window { + lock.subscribe( + NAME_WINDOW_FOCUS, + self.inner.clone(), + !self.multi_ui_session, + ); + } drop(lock); } } @@ -2766,6 +2788,24 @@ impl Connection { } } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.follow_remote_cursor.enum_value() { + if q != BoolOption::NotSet { + self.follow_remote_cursor = q == BoolOption::Yes; + } + } + if let Ok(q) = o.follow_remote_window.enum_value() { + if q != BoolOption::NotSet { + self.follow_remote_window = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_WINDOW_FOCUS, + self.inner.clone(), + self.follow_remote_window, + ); + } + } + } if let Ok(q) = o.disable_audio.enum_value() { if q != BoolOption::NotSet { self.disable_audio = q == BoolOption::Yes; @@ -3126,6 +3166,30 @@ impl Connection { self.inner.send(msg.into()); self.supported_encoding_flag = (true, not_use); } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_cursor_switch_display(&mut self, pos: CursorPosition) { + if self.multi_ui_session { + return; + } + let displays = super::display_service::get_sync_displays(); + let d_index = displays.iter().position(|d| { + let scale = d.scale; + pos.x >= d.x + && pos.y >= d.y + && (pos.x - d.x) as f64 * scale < d.width as f64 + && (pos.y - d.y) as f64 * scale < d.height as f64 + }); + if let Some(d_index) = d_index { + if self.display_idx != d_index { + let mut misc = Misc::new(); + misc.set_follow_current_display(d_index as i32); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 0c8263cbd..d3caa1362 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -258,7 +258,6 @@ pub(super) fn get_original_resolution( .into() } -#[cfg(target_os = "linux")] pub(super) fn get_sync_displays() -> Vec { SYNC_DISPLAYS.lock().unwrap().displays.clone() } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 81ca155d7..2b180a7c4 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -28,6 +28,7 @@ use std::{ use winapi::um::winuser::WHEEL_DELTA; const INVALID_CURSOR_POS: i32 = i32::MIN; +const INVALID_DISPLAY_IDX: i32 = -1; #[derive(Default)] struct StateCursor { @@ -74,6 +75,29 @@ impl StatePos { } } +#[derive(Default)] +struct StateWindowFocus { + display_idx: i32, +} + +impl super::service::Reset for StateWindowFocus { + fn reset(&mut self) { + self.display_idx = INVALID_DISPLAY_IDX; + } +} + +impl StateWindowFocus { + #[inline] + fn is_valid(&self) -> bool { + self.display_idx != INVALID_DISPLAY_IDX + } + + #[inline] + fn is_changed(&self, disp_idx: i32) -> bool { + self.is_valid() && self.display_idx != disp_idx + } +} + #[derive(Default, Clone, Copy)] struct Input { conn: i32, @@ -238,6 +262,7 @@ fn should_disable_numlock(evt: &KeyEvent) -> bool { pub const NAME_CURSOR: &'static str = "mouse_cursor"; pub const NAME_POS: &'static str = "mouse_pos"; +pub const NAME_WINDOW_FOCUS: &'static str = "window_focus"; #[derive(Clone)] pub struct MouseCursorService { pub sp: ServiceTmpl, @@ -277,6 +302,12 @@ pub fn new_pos() -> GenericService { svc.sp } +pub fn new_window_focus() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME_WINDOW_FOCUS.to_owned(), false); + GenericService::repeat::(&svc.clone(), 33, run_window_focus); + svc.sp +} + #[inline] fn update_last_cursor_pos(x: i32, y: i32) { let mut lock = LATEST_SYS_CURSOR_POS.lock().unwrap(); @@ -352,6 +383,22 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> Ok(()) } +fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> { + let displays = super::display_service::get_sync_displays(); + let disp_idx = crate::get_focused_display(displays); + if let Some(disp_idx) = disp_idx.map(|id| id as i32) { + if state.is_changed(disp_idx) { + let mut misc = Misc::new(); + misc.set_follow_current_display(disp_idx as i32); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + sp.send(msg_out); + } + state.display_idx = disp_idx; + } + Ok(()) +} + #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum KeysDown { RdevKey(RawKey), @@ -424,12 +471,15 @@ struct VirtualInputState { #[cfg(target_os = "macos")] impl VirtualInputState { fn new() -> Option { - VirtualInput::new(CGEventSourceStateID::CombinedSessionState, CGEventTapLocation::Session) - .map(|virtual_input| Self { - virtual_input, - capslock_down: false, - }) - .ok() + VirtualInput::new( + CGEventSourceStateID::CombinedSessionState, + CGEventTapLocation::Session, + ) + .map(|virtual_input| Self { + virtual_input, + capslock_down: false, + }) + .ok() } #[inline] diff --git a/src/ui/header.tis b/src/ui/header.tis index 23315af16..b40e664da 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -194,6 +194,8 @@ class Header: Reactor.Component { : ""}
{!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {(is_win && pi.platform == "Windows") && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} @@ -479,7 +481,7 @@ function toggleMenuState() { for (var el in $$(menu#keyboard-options>li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } - for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key", "i444"]) { + for (var id in ["show-remote-cursor", "follow-remote-cursor", "follow-remote-window", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key", "i444"]) { var el = self.select('#' + id); if (el) { var value = handler.get_toggle_option(id); @@ -538,6 +540,15 @@ handler.setMultipleWindowsSession = function(sessions) { }); } +handler.setCurrentDisplay = function(v) { + pi.current_display = v; + handler.switch_display(v); + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index c2e01c9f5..93a796f4b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -259,6 +259,10 @@ impl InvokeUiSession for SciterHandler { // Ignore for sciter version. } + fn set_current_display(&self, _disp_idx: i32) { + self.call("setCurrentDisplay", &make_args!(_disp_idx)); + } + fn set_multiple_windows_session(&self, sessions: Vec) { let mut v = Value::array(0); let mut sessions = sessions; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 61c11feb0..3e5b66049 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -192,6 +192,11 @@ impl Session { self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) } + #[cfg(feature = "flutter")] + pub fn is_multi_ui_session(&self) -> bool { + self.ui_handler.is_multi_ui_session() + } + pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -1378,6 +1383,9 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[cfg(all(feature = "vram", feature = "flutter"))] fn on_texture(&self, display: usize, texture: *mut c_void); fn set_multiple_windows_session(&self, sessions: Vec); + fn set_current_display(&self, disp_idx: i32); + #[cfg(feature = "flutter")] + fn is_multi_ui_session(&self) -> bool; } impl Deref for Session { From 23147f2328538ab6c73bcc8ad757dff3d0cace1a Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Thu, 25 Apr 2024 13:04:48 +0200 Subject: [PATCH 03/35] Update nl.rs (#7824) --- src/lang/nl.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 026cc92b8..4fd22b254 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -602,8 +602,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Everyone", "Iedereen"), ("ab_web_console_tip", "Meer over de webconsole"), ("allow-only-conn-window-open-tip", "Alleen verbindingen toestaan als het RustDesk-venster geopend is"), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("no_need_privacy_mode_no_physical_displays_tip", "Geen fysieke schermen, geen privémodus nodig."), + ("Follow remote cursor", "Volg de cursor op afstand"), + ("Follow remote window focus", "Volg de focus van het venster op afstand"), ].iter().cloned().collect(); } From 0e7e27f99dfb6c470a0080f6bf011814f299bbe3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 25 Apr 2024 20:16:48 +0800 Subject: [PATCH 04/35] update hwcodec, fix gpu/cpu stuck caused by nv codec (#7826) * Disable all nv codec encoding on windows except nv sdk encoding, because it doesn't use CUContext * Keep nv codec on linux, because I didn't reproduce the stuck on it * Add ffmpeg d3d11 vram decoding Signed-off-by: 21pages --- Cargo.lock | 4 ++-- libs/scrap/src/common/vram.rs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 340dc52f5..881ce84fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3038,8 +3038,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.3.3" -source = "git+https://github.com/21pages/hwcodec#eeebf980d4eb41daaf05090b097d5a59d688d3d8" +version = "0.4.1" +source = "git+https://github.com/21pages/hwcodec#17870c015a3f371339a91c5305d1e920bd8284e3" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index daeece519..157047ec2 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -17,7 +17,7 @@ use hbb_common::{ }; use hwcodec::{ common::{DataFormat, Driver, MAX_GOP}, - native::{ + vram::{ decode::{self, DecodeFrame, Decoder}, encode::{self, EncodeFrame, Encoder}, Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext, @@ -294,6 +294,10 @@ impl VRamDecoder { pub fn try_get(format: CodecFormat, luid: Option) -> Option { let v: Vec<_> = Self::available(format, luid); if v.len() > 0 { + // prefer ffmpeg + if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) { + return Some(ctx.clone()); + } Some(v[0].clone()) } else { None From 92748de7d434af3dc92ccd06da413dad29a8a45b Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 25 Apr 2024 15:17:25 +0300 Subject: [PATCH 05/35] Update ru.rs (#7827) --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a1e5a7c6b..2553c0925 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -603,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Больше в веб-консоли"), ("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Следовать за удалённым курсором"), + ("Follow remote window focus", "Следовать за фокусом удалённого окна"), ].iter().cloned().collect(); } From 4dfc82f68497b13083317fddfaf70e8099b92f01 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 25 Apr 2024 22:35:51 +0800 Subject: [PATCH 06/35] fix: msi, app icon (#7830) * fix: msi, app icon Signed-off-by: fufesou * refact: check if icon.ico exists Signed-off-by: fufesou --------- Signed-off-by: fufesou --- res/msi/.gitignore | 2 ++ res/msi/Package/Resources/icon.ico | Bin 99678 -> 0 bytes res/msi/preprocess.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) delete mode 100644 res/msi/Package/Resources/icon.ico diff --git a/res/msi/.gitignore b/res/msi/.gitignore index 44c377c55..d901aedc6 100644 --- a/res/msi/.gitignore +++ b/res/msi/.gitignore @@ -9,3 +9,5 @@ packages CustomActions/x64 CustomActions/*.user CustomActions/*.filters + +Package/Resources diff --git a/res/msi/Package/Resources/icon.ico b/res/msi/Package/Resources/icon.ico deleted file mode 100644 index eedb92614ca9591605fc624c7d1b64d7587188f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 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 Date: Fri, 26 Apr 2024 11:54:14 +0800 Subject: [PATCH 07/35] improe job --- res/job.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/res/job.py b/res/job.py index 1ddf0faa5..13ea9e81d 100755 --- a/res/job.py +++ b/res/job.py @@ -23,17 +23,21 @@ SECRET_KEY = os.getenv("SECRET_KEY") or "worldpeace2024" # The headers for API requests HEADERS = {"Authorization": f"Bearer {SECRET_KEY}"} -TIMEOUT = int(os.getenv("TIMEOUT") or "30") +SIGN_TIMEOUT = int(os.getenv("SIGN_TIMEOUT") or "30") +TIMEOUT = float(os.getenv("TIMEOUT") or "900") def create(task_name, file_path=None): if file_path is None: - response = requests.post(f"{BASE_URL}/tasks/{task_name}", headers=HEADERS) + response = requests.post( + f"{BASE_URL}/tasks/{task_name}", timeout=TIMEOUT, headers=HEADERS + ) else: with open(file_path, "rb") as f: files = {"file": f} response = requests.post( f"{BASE_URL}/tasks/{task_name}", + timeout=TIMEOUT, headers=HEADERS, files=files, ) @@ -44,19 +48,27 @@ def upload_file(task_id, file_path): with open(file_path, "rb") as f: files = {"file": f} response = requests.post( - f"{BASE_URL}/tasks/{task_id}/files", headers=HEADERS, files=files + f"{BASE_URL}/tasks/{task_id}/files", + timeout=TIMEOUT, + headers=HEADERS, + files=files, ) return get_json(response) def get_status(task_id): - response = requests.get(f"{BASE_URL}/tasks/{task_id}/status", headers=HEADERS) + response = requests.get( + f"{BASE_URL}/tasks/{task_id}/status", timeout=TIMEOUT, headers=HEADERS + ) return get_json(response) def download_files(task_id, output_dir, fn=None): response = requests.get( - f"{BASE_URL}/tasks/{task_id}/files", headers=HEADERS, stream=True + f"{BASE_URL}/tasks/{task_id}/files", + timeout=TIMEOUT, + headers=HEADERS, + stream=True, ) # Check if the request was successful @@ -73,7 +85,10 @@ def download_files(task_id, output_dir, fn=None): def download_one_file(task_id, file_id, output_dir): response = requests.get( - f"{BASE_URL}/tasks/{task_id}/files/{file_id}", headers=HEADERS, stream=True + f"{BASE_URL}/tasks/{task_id}/files/{file_id}", + timeout=TIMEOUT, + headers=HEADERS, + stream=True, ) # Check if the request was successful @@ -86,14 +101,19 @@ def download_one_file(task_id, file_id, output_dir): return response.ok -def fetch(): - response = requests.get(f"{BASE_URL}/tasks/fetch_task", headers=HEADERS) +def fetch(tag=None): + response = requests.get( + f"{BASE_URL}/tasks/fetch_task" + ("?tag=%s" % tag if tag else ""), + timeout=TIMEOUT, + headers=HEADERS, + ) return get_json(response) def update_status(task_id, status): response = requests.patch( f"{BASE_URL}/tasks/{task_id}/status", + timeout=TIMEOUT, headers=HEADERS, json=status, ) @@ -103,6 +123,7 @@ def update_status(task_id, status): def delete_task(task_id): response = requests.delete( f"{BASE_URL}/tasks/{task_id}", + timeout=TIMEOUT, headers=HEADERS, ) return get_json(response) @@ -135,7 +156,7 @@ def sign_one_file(file_path): task_id = res["id"] n = 0 while True: - if n >= TIMEOUT: + if n >= SIGN_TIMEOUT: delete_task(task_id) logging.error(f"Failed to sign {file_path}") break From 0828f747e395b6c2c168f6a4bccc41d879995947 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 26 Apr 2024 05:57:05 +0200 Subject: [PATCH 08/35] Update de.rs (#7831) --- src/lang/de.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 8c61ba866..ad7084213 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "Keine Einträge"), ("Invalid folder name", "Ungültiger Ordnername"), ("Socks5 Proxy", "SOCKS5-Proxy"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s)-Proxy"), + ("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S)-Proxy"), ("Discovered", "Im LAN erkannt"), ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), ("Remote ID", "Entfernte ID"), @@ -603,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Mehr über Webkonsole"), ("allow-only-conn-window-open-tip", "Verbindung nur zulassen, wenn das RustDesk-Fenster geöffnet ist"), ("no_need_privacy_mode_no_physical_displays_tip", "Keine physischen Bildschirme; keine Notwendigkeit, den Datenschutzmodus zu verwenden."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Dem entfernten Cursor folgen"), + ("Follow remote window focus", "Dem Fokus des entfernten Fensters folgen"), ].iter().cloned().collect(); } From 7e00d70f4d5174b61c337307a2430a4c0d62c68a Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Fri, 26 Apr 2024 06:57:15 +0300 Subject: [PATCH 09/35] Update lv.rs (#7832) --- src/lang/lv.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 2afb32aae..4b7a659e4 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -603,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Vairāk par tīmekļa konsoli"), ("allow-only-conn-window-open-tip", "Atļaut savienojumu tikai tad, ja ir atvērts RustDesk logs"), ("no_need_privacy_mode_no_physical_displays_tip", "Nav fizisku displeju, nav jāizmanto privātuma režīms."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Sekot attālajam kursoram"), + ("Follow remote window focus", "Sekot attālā loga fokusam"), ].iter().cloned().collect(); } From 9d3c823603eb62ca02f8a31b0486b059e3007a53 Mon Sep 17 00:00:00 2001 From: LelieL91 <48968075+LelieL91@users.noreply.github.com> Date: Fri, 26 Apr 2024 05:57:26 +0200 Subject: [PATCH 10/35] Update it.rs (#7833) Fix typo --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index cf08daab9..e5975bd42 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), ("Set Password", "Imposta password"), ("OS Password", "Password sistema operativo"), - ("install_tip", "A causa del controllo account uUtente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), + ("install_tip", "A causa del Controllo Account Utente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), ("Click to upgrade", "Aggiorna"), ("Click to download", "Download"), ("Click to update", "Aggiorna"), From 2b3f87d6f28e767b08d9fbdfbbd7b8a63abc55cb Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 26 Apr 2024 13:49:53 +0800 Subject: [PATCH 11/35] fix: msi, custom client, license (#7834) Signed-off-by: fufesou --- .../Package/{RustDesk License.rtf => License.rtf} | 0 res/msi/Package/Package.wxs | 2 +- res/msi/preprocess.py | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) rename res/msi/Package/{RustDesk License.rtf => License.rtf} (100%) diff --git a/res/msi/Package/RustDesk License.rtf b/res/msi/Package/License.rtf similarity index 100% rename from res/msi/Package/RustDesk License.rtf rename to res/msi/Package/License.rtf diff --git a/res/msi/Package/Package.wxs b/res/msi/Package/Package.wxs index 8361e2a12..e86494e85 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -16,7 +16,7 @@ - + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 7e6190ca7..d4ac7635d 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -437,6 +437,19 @@ def init_global_vars(dist_dir, app_name, args): return True +def update_license_file(app_name): + if app_name == "RustDesk": + return + license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf") + with open(license_file, "r") as f: + license_content = f.read() + license_content = license_content.replace("website rustdesk.com and other ", "") + license_content = license_content.replace("RustDesk", app_name) + license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE) + with open(license_file, "w") as f: + f.write(license_content) + + def replace_component_guids_in_wxs(): langs_dir = Path(sys.argv[0]).parent.joinpath("Package") for file_path in langs_dir.glob("**/*.wxs"): @@ -466,6 +479,8 @@ if __name__ == "__main__": if not init_global_vars(dist_dir, app_name, args): sys.exit(-1) + update_license_file(app_name) + if not gen_pre_vars(args, dist_dir): sys.exit(-1) From 09f87238dcb01dbe5845b1733db80146b5b6ecb1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 26 Apr 2024 18:15:16 +0800 Subject: [PATCH 12/35] vcpkg -> FLUTTER_ELINUX_COMMIT_ID, and remove FLUTTER_ELINUX_COMMIT --- .github/workflows/flutter-build.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 0168420f9..6d6e5b2b6 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -18,12 +18,10 @@ env: FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" # for arm64 linux because official Dart SDK does not work FLUTTER_ELINUX_VERSION: "3.16.9" - FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af" TAG_NAME: "${{ inputs.upload-tag }}" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2023.10.19 - # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50" + # vcpkg version: 2024.03.25 + VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff" VERSION: "1.2.4" NDK_VERSION: "r26b" #signing keys env variable checks @@ -1545,7 +1543,7 @@ jobs: git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux git fetch - git reset --hard ${{ env.FLUTTER_ELINUX_COMMIT_ID }} + git reset --hard ${{ env.FLUTTER_VERSION }} popd - uses: rustdesk-org/run-on-arch-action@amd64-support From 474e13f8b91069b1dc6c4b6e04cf8786e8777a21 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 26 Apr 2024 19:00:38 +0800 Subject: [PATCH 13/35] move ios ci to arm64 mac --- .github/workflows/build-macos-arm64.yml | 50 +++--- .github/workflows/flutter-build.yml | 145 +++++++----------- flutter/ios/Podfile.lock | 2 +- flutter/ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 5 files changed, 89 insertions(+), 112 deletions(-) diff --git a/.github/workflows/build-macos-arm64.yml b/.github/workflows/build-macos-arm64.yml index c8c984e1a..8142e6020 100644 --- a/.github/workflows/build-macos-arm64.yml +++ b/.github/workflows/build-macos-arm64.yml @@ -32,25 +32,39 @@ env: SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" jobs: - build-for-macOS-arm64: - name: build-for-macOS-arm64 + build-rustdesk-ios: runs-on: [self-hosted, macOS, ARM64] + strategy: + fail-fast: false steps: - #- name: Import the codesign cert - # if: env.MACOS_P12_BASE64 != null - # uses: apple-actions/import-codesign-certs@v1 - # continue-on-error: true - # with: - # p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} - # p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - # keychain: rustdesk - - #- name: Check sign and import sign key - # if: env.MACOS_P12_BASE64 != null - # run: | - # security default-keychain -s rustdesk.keychain - # security find-identity -v - - - name: Run + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v3 + + # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" + + - name: Install flutter rust bridge deps shell: bash run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + + - name: Build rustdesk lib + run: | + rustup target add ${{ matrix.job.target }} + cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + + - name: Build rustdesk + shell: bash + run: | + pushd flutter + # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign + # for easy debugging + flutter build ipa --release --no-codesign diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 6d6e5b2b6..d95268548 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -390,6 +390,60 @@ jobs: files: | rustdesk*-aarch64.dmg + build-rustdesk-ios: + if: ${{ inputs.upload-artifact }} + runs-on: [self-hosted, macOS, ARM64] + strategy: + fail-fast: false + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v3 + + # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + + - name: Build rustdesk lib + run: | + rustup target add ${{ matrix.job.target }} + cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + + - name: Build rustdesk + shell: bash + run: | + pushd flutter + # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign + # for easy debugging + flutter build ipa --release --no-codesign + + # - name: Upload Artifacts + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: actions/upload-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + # path: flutter/build/ios/ipa/*.ipa + + # - name: Publish ipa package + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flutter/build/ios/ipa/*.ipa + build-for-macOS: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] runs-on: ${{ matrix.job.os }} @@ -580,97 +634,6 @@ jobs: generate-bridge-linux: uses: ./.github/workflows/bridge.yml - build-rustdesk-ios: - if: ${{ inputs.upload-artifact }} - name: build rustdesk ios ipa ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - arch: aarch64, - target: aarch64-apple-ios, - os: macos-latest, - extra-build-features: "", - } - steps: - - name: Export GitHub Actions cache environment variables - uses: actions/github-script@v6 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Install dependencies - run: | - brew install nasm - - name: Checkout source code - uses: actions/checkout@v3 - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Setup vcpkg with Github Actions binary cache - uses: lukka/run-vcpkg@v11 - with: - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" - shell: bash - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - targets: ${{ matrix.job.target }} - components: "rustfmt" - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache-ios - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h - - - name: Build rustdesk lib - run: | - rustup target add ${{ matrix.job.target }} - cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - - - name: Build rustdesk - shell: bash - run: | - pushd flutter - # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign - # for easy debugging - flutter build ipa --release --no-codesign - - # - name: Upload Artifacts - # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' - # uses: actions/upload-artifact@master - # with: - # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk - # path: flutter/build/ios/ipa/*.ipa - - # - name: Publish ipa package - # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # flutter/build/ios/ipa/*.ipa - build-rustdesk-android: needs: [generate-bridge-linux] name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index a7380de27..6cb5c9cff 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -139,4 +139,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj index 630b11758..acc2a09e7 100644 --- a/flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -159,7 +159,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db2..5e31d3d34 100644 --- a/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Fri, 26 Apr 2024 19:07:16 +0800 Subject: [PATCH 14/35] fix ci --- .github/workflows/build-macos-arm64.yml | 1 - .github/workflows/flutter-build.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/build-macos-arm64.yml b/.github/workflows/build-macos-arm64.yml index 8142e6020..3416ec72d 100644 --- a/.github/workflows/build-macos-arm64.yml +++ b/.github/workflows/build-macos-arm64.yml @@ -58,7 +58,6 @@ jobs: - name: Build rustdesk lib run: | - rustup target add ${{ matrix.job.target }} cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - name: Build rustdesk diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index d95268548..5d99251ff 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -417,7 +417,6 @@ jobs: - name: Build rustdesk lib run: | - rustup target add ${{ matrix.job.target }} cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - name: Build rustdesk From 105a758914c4d934997931bad143755f73f021ba Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 26 Apr 2024 19:16:52 +0800 Subject: [PATCH 15/35] disable flutter build of ios, since ios sdk not installed yet --- .github/workflows/flutter-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 5d99251ff..c66e30bb0 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -420,6 +420,8 @@ jobs: cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - name: Build rustdesk + # ios sdk not installed on this machine, I will install it later after I am back home + if: false shell: bash run: | pushd flutter From 2626dcbc5f6e6b655e9e28f482c49ae20a24d726 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 26 Apr 2024 19:42:47 +0800 Subject: [PATCH 16/35] fix black screen issue when controlling the second screen on versions that lack multiple display support while using vram decoding (#7836) * avoid create unnecessary video decoder Signed-off-by: 21pages * controlled side uses the most frequent selected codec Signed-off-by: 21pages * fix black screen when control old version's second screen For versions that do not support multiple displays, the display parameter is always 0, need set type of current display Signed-off-by: 21pages --------- Signed-off-by: 21pages --- Cargo.lock | 4 +- .../lib/models/desktop_render_texture.dart | 53 ++++++++++--------- libs/scrap/src/common/codec.rs | 16 ++++-- libs/scrap/src/common/vram.rs | 6 +-- src/client.rs | 43 ++++++++------- 5 files changed, 69 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 881ce84fd..8f8f6f745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3038,8 +3038,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.4.1" -source = "git+https://github.com/21pages/hwcodec#17870c015a3f371339a91c5305d1e920bd8284e3" +version = "0.4.2" +source = "git+https://github.com/21pages/hwcodec#b8ed1e869b3456af6a5f77b7617f861d9a99dcdd" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index 80477fede..0578bb688 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:get/get.dart'; @@ -152,40 +153,36 @@ class TextureModel { TextureModel(this.parent); setTextureType({required int display, required bool gpuTexture}) { - debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture"); - var texture = _control[display]; - if (texture == null) { - texture = _Control(); - _control[display] = texture; + debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture"); + ensureControl(display); + _control[display]?.setTextureType(gpuTexture: gpuTexture); + // For versions that do not support multiple displays, the display parameter is always 0, need set type of current display + final ffi = parent.target; + if (ffi == null) return; + if (!ffi.ffiModel.pi.isSupportMultiDisplay) { + final currentDisplay = CurrentDisplayState.find(ffi.id).value; + if (currentDisplay != display) { + debugPrint( + "setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture"); + ensureControl(currentDisplay); + _control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture); + } } - texture.setTextureType(gpuTexture: gpuTexture); } setRgbaTextureId({required int display, required int id}) { - var ctl = _control[display]; - if (ctl == null) { - ctl = _Control(); - _control[display] = ctl; - } - ctl.setRgbaTextureId(id); + ensureControl(display); + _control[display]?.setRgbaTextureId(id); } setGpuTextureId({required int display, required int id}) { - var ctl = _control[display]; - if (ctl == null) { - ctl = _Control(); - _control[display] = ctl; - } - ctl.setGpuTextureId(id); + ensureControl(display); + _control[display]?.setGpuTextureId(id); } RxInt getTextureId(int display) { - var ctl = _control[display]; - if (ctl == null) { - ctl = _Control(); - _control[display] = ctl; - } - return ctl.textureID; + ensureControl(display); + return _control[display]!.textureID; } updateCurrentDisplay(int curDisplay) { @@ -241,4 +238,12 @@ class TextureModel { await texture.destroy(ffi); } } + + ensureControl(int display) { + var ctl = _control[display]; + if (ctl == null) { + ctl = _Control(); + _control[display] = ctl; + } + } } diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index fd283386b..97a444299 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -221,7 +221,6 @@ impl Encoder { let h265_useable = _all_support_h265_decoding && (h265vram_encoding || h265hw_encoding.is_some()); let mut name = ENCODE_CODEC_NAME.lock().unwrap(); - let mut preference = PreferCodec::Auto; let preferences: Vec<_> = decodings .iter() .filter(|(_, s)| { @@ -233,9 +232,20 @@ impl Encoder { }) .map(|(_, s)| s.prefer) .collect(); - if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { - preference = preferences[0].enum_value_or(PreferCodec::Auto); + // find the most frequent preference + let mut counts = Vec::new(); + for pref in &preferences { + match counts.iter_mut().find(|(p, _)| p == pref) { + Some((_, count)) => *count += 1, + None => counts.push((pref.clone(), 1)), + } } + let max_count = counts.iter().map(|(_, count)| *count).max().unwrap_or(0); + let (most_frequent, _) = counts + .into_iter() + .find(|(_, count)| *count == max_count) + .unwrap_or((PreferCodec::Auto.into(), 0)); + let preference = most_frequent.enum_value_or(PreferCodec::Auto); #[allow(unused_mut)] let mut auto_codec = CodecName::VP9; diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index 157047ec2..fb35ca7f9 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -24,8 +24,6 @@ use hwcodec::{ }, }; -const OUTPUT_SHARED_HANDLE: bool = false; - // https://www.reddit.com/r/buildapc/comments/d2m4ny/two_graphics_cards_two_monitors/ // https://www.reddit.com/r/techsupport/comments/t2v9u6/dual_monitor_setup_with_dual_gpu/ // https://cybersided.com/two-monitors-two-gpus/ @@ -331,8 +329,8 @@ impl VRamDecoder { } pub fn new(format: CodecFormat, luid: Option) -> ResultType { - log::info!("try create {format:?} vram decoder, luid: {luid:?}"); let ctx = Self::try_get(format, luid).ok_or(anyhow!("Failed to get decode context"))?; + log::info!("try create vram decoder: {ctx:?}"); match Decoder::new(ctx) { Ok(decoder) => Ok(Self { decoder }), Err(_) => { @@ -376,7 +374,7 @@ pub(crate) fn check_available_vram() -> String { gop: MAX_GOP as _, }; let encoders = encode::available(d); - let decoders = decode::available(OUTPUT_SHARED_HANDLE); + let decoders = decode::available(); let available = Available { e: encoders, d: decoders, diff --git a/src/client.rs b/src/client.rs index c765c02cf..c7fe88a79 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1580,7 +1580,10 @@ impl LoginConfigHandler { /// /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { - if self.conn_type.eq(&ConnType::PORT_FORWARD) || self.conn_type.eq(&ConnType::RDP) || self.conn_type.eq(&ConnType::FILE_TRANSFER) { + if self.conn_type.eq(&ConnType::PORT_FORWARD) + || self.conn_type.eq(&ConnType::RDP) + || self.conn_type.eq(&ConnType::FILE_TRANSFER) + { return None; } let mut msg = OptionMessage::new(); @@ -2110,7 +2113,7 @@ where std::thread::spawn(move || { #[cfg(windows)] sync_cpu_usage(); - let mut handler_controller_map = Vec::new(); + let mut handler_controller_map = HashMap::new(); // let mut count = Vec::new(); // let mut duration = std::time::Duration::ZERO; // let mut skip_beginning = Vec::new(); @@ -2141,17 +2144,18 @@ where let display = vf.display as usize; let start = std::time::Instant::now(); let format = CodecFormat::from(&vf); - if handler_controller_map.len() <= display { - for _i in handler_controller_map.len()..=display { - handler_controller_map.push(VideoHandlerController { - handler: VideoHandler::new(format, _i), + if !handler_controller_map.contains_key(&display) { + handler_controller_map.insert( + display, + VideoHandlerController { + handler: VideoHandler::new(format, display), count: 0, duration: std::time::Duration::ZERO, skip_beginning: 0, - }); - } + }, + ); } - if let Some(handler_controller) = handler_controller_map.get_mut(display) { + if let Some(handler_controller) = handler_controller_map.get_mut(&display) { let mut pixelbuffer = true; let mut tmp_chroma = None; match handler_controller.handler.handle_frame( @@ -2219,7 +2223,7 @@ where let mut should_update_supported = false; handler_controller_map .iter() - .map(|h| { + .map(|(_, h)| { if !h.handler.decoder.valid() || h.handler.fail_counter >= MAX_DECODE_FAIL_COUNTER { let mut lc = session.lc.write().unwrap(); let format = h.handler.decoder.format(); @@ -2238,21 +2242,20 @@ where } } MediaData::Reset(display) => { - if let Some(handler_controler) = handler_controller_map.get_mut(display) { + if let Some(handler_controler) = handler_controller_map.get_mut(&display) { handler_controler.handler.reset(None); } } MediaData::RecordScreen(start, display, w, h, id) => { log::info!("record screen command: start: {start}, display: {display}"); - if handler_controller_map.len() == 1 { - // Compatible with the sciter version(single ui session). - // For the sciter version, there're no multi-ui-sessions for one connection. - // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. - handler_controller_map[0] - .handler - .record_screen(start, w, h, id); - } else { - if let Some(handler_controler) = handler_controller_map.get_mut(display) + // Compatible with the sciter version(single ui session). + // For the sciter version, there're no multi-ui-sessions for one connection. + // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. + if let Some(handler_controler) = handler_controller_map.get_mut(&display) { + handler_controler.handler.record_screen(start, w, h, id); + } else if handler_controller_map.len() == 1 { + if let Some(handler_controler) = + handler_controller_map.values_mut().next() { handler_controler.handler.record_screen(start, w, h, id); } From 796b66b057047d3bc93846e4fe5f91561eabcdb9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 26 Apr 2024 22:34:11 +0800 Subject: [PATCH 17/35] fix: multi-window, init perms (#7839) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 65e29b2fa..8f1bfe1c2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -50,6 +50,8 @@ class CachedPeerData { Map peerInfo = {}; List> cursorDataList = []; Map lastCursorId = {}; + Map permissions = {}; + bool secure = false; bool direct = false; @@ -62,6 +64,7 @@ class CachedPeerData { 'peerInfo': peerInfo, 'cursorDataList': cursorDataList, 'lastCursorId': lastCursorId, + 'permissions': permissions, 'secure': secure, 'direct': direct, }); @@ -77,6 +80,9 @@ class CachedPeerData { data.cursorDataList.add(cursorData); } data.lastCursorId = map['lastCursorId']; + map['permissions'].forEach((key, value) { + data.permissions[key] = value; + }); data.secure = map['secure']; data.direct = map['direct']; return data; @@ -116,6 +122,10 @@ class FfiModel with ChangeNotifier { _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false; Map get permissions => _permissions; + setPermissions(Map permissions) { + _permissions.clear(); + _permissions.addAll(permissions); + } bool? get secure => _secure; @@ -138,6 +148,7 @@ class FfiModel with ChangeNotifier { FfiModel(this.parent) { clear(); sessionId = parent.target!.sessionId; + cachedPeerData.permissions = _permissions; } Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true); @@ -2342,6 +2353,7 @@ class FFI { debugPrint('Unreachable, the cached data cannot be decoded.'); return; } + ffiModel.setPermissions(data.permissions); await ffiModel.handleCachedPeerData(data, id); await sessionRefreshVideo(sessionId, ffiModel.pi); await bind.sessionRequestNewDisplayInitMsgs( From b863ea51adaa6d6a5562ae065e1ed8050b71c939 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sat, 27 Apr 2024 07:13:21 +0200 Subject: [PATCH 18/35] Update Italian language (#7838) --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index e5975bd42..d0546bb26 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -603,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Altre info sulla console web"), ("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"), ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Segui cursore remoto"), + ("Follow remote window focus", "Segui focus finestra remota"), ].iter().cloned().collect(); } From a6632632fafaa9b03c20e08b14cf5e0b6cd62f8e Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 27 Apr 2024 13:45:44 +0800 Subject: [PATCH 19/35] fix: multi-window, click-move (#7844) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 10 +- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 6 + .../lib/desktop/pages/remote_tab_page.dart | 229 ++++++++------- flutter/lib/models/input_model.dart | 273 ++++++++++++++++-- flutter/lib/models/model.dart | 61 +++- flutter/lib/utils/multi_window_manager.dart | 35 +++ 7 files changed, 481 insertions(+), 134 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 374ba1dce..1e7523e54 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -203,7 +203,7 @@ class _RawTouchGestureDetectorRegionState return; } if (!handleTouch) { - ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); + ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } } @@ -222,6 +222,9 @@ class _RawTouchGestureDetectorRegionState return; } if (handleTouch) { + if (isDesktop) { + ffi.cursorModel.trySetRemoteWindowCoords(); + } inputModel.sendMouse('down', MouseButtons.left); ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } else { @@ -241,13 +244,16 @@ class _RawTouchGestureDetectorRegionState if (lastDeviceKind != PointerDeviceKind.touch) { return; } - ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch); + ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); } onOneFingerPanEnd(DragEndDetails d) { if (lastDeviceKind != PointerDeviceKind.touch) { return; } + if (isDesktop) { + ffi.cursorModel.clearRemoteWindowCoords(); + } inputModel.sendMouse('up', MouseButtons.left); } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 090ca62a4..14182649e 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -60,6 +60,7 @@ const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetSessionIdList = "get_session_id_list"; +const String kWindowEventRemoteWindowCoords = "remote_window_coords"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventGetCachedSessionData = "get_cached_session_data"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index dd87e0939..6ec9fd360 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -774,6 +774,12 @@ class _DesktopHomePageState extends State final screenRect = parseParamScreenRect(args); await rustDeskWinManager.openMonitorSession( windowId, peerId, display, displayCount, screenRect); + } else if (call.method == kWindowEventRemoteWindowCoords) { + final windowId = int.tryParse(call.arguments); + if (windowId != null) { + return jsonEncode( + await rustDeskWinManager.getOtherRemoteWindowCoords(windowId)); + } } }); _uniLinksSubscription = listenUniLinks(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index cdca474ab..8f6a96f47 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; @@ -107,107 +108,7 @@ class _ConnectionTabPageState extends State { tabController.onRemoved = (_, id) => onRemoveId(id); - rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( - "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); - - dynamic returnValue; - // for simplify, just replace connectionId - if (call.method == kWindowEventNewRemoteDesktop) { - final args = jsonDecode(call.arguments); - final id = args['id']; - final switchUuid = args['switch_uuid']; - final sessionId = args['session_id']; - final tabWindowId = args['tab_window_id']; - final display = args['display']; - final displays = args['displays']; - final screenRect = parseParamScreenRect(args); - windowOnTop(windowId()); - tryMoveToScreenAndSetFullscreen(screenRect); - if (tabController.length == 0) { - // Show the hidden window. - if (isMacOS && stateGlobal.closeOnFullscreen == true) { - stateGlobal.setFullscreen(true); - } - // Reset the state - stateGlobal.closeOnFullscreen = null; - } - ConnectionTypeState.init(id); - _toolbarState.setShow( - bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y'); - tabController.add(TabInfo( - key: id, - label: id, - selectedIcon: selectedIcon, - unselectedIcon: unselectedIcon, - onTabCloseButton: () => tabController.closeBy(id), - page: RemotePage( - key: ValueKey(id), - id: id, - sessionId: sessionId == null ? null : SessionID(sessionId), - tabWindowId: tabWindowId, - display: display, - displays: displays?.cast(), - password: args['password'], - toolbarState: _toolbarState, - tabController: tabController, - switchUuid: switchUuid, - forceRelay: args['forceRelay'], - isSharedPassword: args['isSharedPassword'], - ), - )); - } else if (call.method == kWindowDisableGrabKeyboard) { - // ??? - } else if (call.method == "onDestroy") { - tabController.clear(); - } else if (call.method == kWindowActionRebuild) { - reloadCurrentWindow(); - } else if (call.method == kWindowEventActiveSession) { - final jumpOk = tabController.jumpToByKey(call.arguments); - if (jumpOk) { - windowOnTop(windowId()); - } - return jumpOk; - } else if (call.method == kWindowEventActiveDisplaySession) { - final args = jsonDecode(call.arguments); - final id = args['id']; - final display = args['display']; - final jumpOk = tabController.jumpToByKeyAndDisplay(id, display); - if (jumpOk) { - windowOnTop(windowId()); - } - return jumpOk; - } else if (call.method == kWindowEventGetRemoteList) { - return tabController.state.value.tabs - .map((e) => e.key) - .toList() - .join(','); - } else if (call.method == kWindowEventGetSessionIdList) { - return tabController.state.value.tabs - .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}') - .toList() - .join(';'); - } else if (call.method == kWindowEventGetCachedSessionData) { - // Ready to show new window and close old tab. - final args = jsonDecode(call.arguments); - final id = args['id']; - final close = args['close']; - try { - final remotePage = tabController.state.value.tabs - .firstWhere((tab) => tab.key == id) - .page as RemotePage; - returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString(); - } catch (e) { - debugPrint('Failed to get cached session data: $e'); - } - if (close && returnValue != null) { - closeSessionOnDispose[id] = false; - tabController.closeBy(id); - } - } - _update_remote_count(); - return returnValue; - }); + rustDeskWinManager.setMethodHandler(_remoteMethodHandler); if (!_isScreenRectSet) { Future.delayed(Duration.zero, () { restoreWindowPosition( @@ -499,4 +400,130 @@ class _ConnectionTabPageState extends State { _update_remote_count() => RemoteCountState.find().value = tabController.length; + + Future _remoteMethodHandler(call, fromWindowId) async { + print( + "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + dynamic returnValue; + // for simplify, just replace connectionId + if (call.method == kWindowEventNewRemoteDesktop) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final switchUuid = args['switch_uuid']; + final sessionId = args['session_id']; + final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; + final screenRect = parseParamScreenRect(args); + windowOnTop(windowId()); + tryMoveToScreenAndSetFullscreen(screenRect); + if (tabController.length == 0) { + // Show the hidden window. + if (isMacOS && stateGlobal.closeOnFullscreen == true) { + stateGlobal.setFullscreen(true); + } + // Reset the state + stateGlobal.closeOnFullscreen = null; + } + ConnectionTypeState.init(id); + _toolbarState.setShow( + bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y'); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(id), + page: RemotePage( + key: ValueKey(id), + id: id, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: args['password'], + toolbarState: _toolbarState, + tabController: tabController, + switchUuid: switchUuid, + forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], + ), + )); + } else if (call.method == kWindowDisableGrabKeyboard) { + // ??? + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + final jumpOk = tabController.jumpToByKey(call.arguments); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = tabController.jumpToByKeyAndDisplay(id, display); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventGetRemoteList) { + return tabController.state.value.tabs + .map((e) => e.key) + .toList() + .join(','); + } else if (call.method == kWindowEventGetSessionIdList) { + return tabController.state.value.tabs + .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}') + .toList() + .join(';'); + } else if (call.method == kWindowEventGetCachedSessionData) { + // Ready to show new window and close old tab. + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; + try { + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == id) + .page as RemotePage; + returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString(); + } catch (e) { + debugPrint('Failed to get cached session data: $e'); + } + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); + } + } else if (call.method == kWindowEventRemoteWindowCoords) { + final remotePage = + tabController.state.value.selectedTabInfo.page as RemotePage; + final ffi = remotePage.ffi; + final displayRect = ffi.ffiModel.displaysRect(); + if (displayRect != null) { + final wc = WindowController.fromWindowId(windowId()); + Rect? frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + } + if (frame != null) { + ffi.cursorModel.moveLocal(0, 0); + final coords = RemoteWindowCoords( + frame, + CanvasCoords.fromCanvasModel(ffi.canvasModel), + CursorCoords.fromCursorModel(ffi.cursorModel), + displayRect); + returnValue = jsonEncode(coords.toJson()); + } + } + } + _update_remote_count(); + return returnValue; + } } diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 6d52c4658..4ad4f9073 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -4,9 +4,12 @@ import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import '../../models/model.dart'; @@ -21,6 +24,128 @@ const _kMouseEventDown = 'mousedown'; const _kMouseEventUp = 'mouseup'; const _kMouseEventMove = 'mousemove'; +class CanvasCoords { + double x = 0; + double y = 0; + double scale = 1.0; + double scrollX = 0; + double scrollY = 0; + ScrollStyle scrollStyle = ScrollStyle.scrollauto; + Size size = Size.zero; + + CanvasCoords(); + + Map toJson() { + return { + 'x': x, + 'y': y, + 'scale': scale, + 'scrollX': scrollX, + 'scrollY': scrollY, + 'scrollStyle': + scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar', + 'size': { + 'w': size.width, + 'h': size.height, + } + }; + } + + static CanvasCoords fromJson(Map json) { + final model = CanvasCoords(); + model.x = json['x']; + model.y = json['y']; + model.scale = json['scale']; + model.scrollX = json['scrollX']; + model.scrollY = json['scrollY']; + model.scrollStyle = json['scrollStyle'] == 'scrollauto' + ? ScrollStyle.scrollauto + : ScrollStyle.scrollbar; + model.size = Size(json['size']['w'], json['size']['h']); + return model; + } + + static CanvasCoords fromCanvasModel(CanvasModel model) { + final coords = CanvasCoords(); + coords.x = model.x; + coords.y = model.y; + coords.scale = model.scale; + coords.scrollX = model.scrollX; + coords.scrollY = model.scrollY; + coords.scrollStyle = model.scrollStyle; + coords.size = model.size; + return coords; + } +} + +class CursorCoords { + Offset offset = Offset.zero; + + CursorCoords(); + + Map toJson() { + return { + 'offset_x': offset.dx, + 'offset_y': offset.dy, + }; + } + + static CursorCoords fromJson(Map json) { + final model = CursorCoords(); + model.offset = Offset(json['offset_x'], json['offset_y']); + return model; + } + + static CursorCoords fromCursorModel(CursorModel model) { + final coords = CursorCoords(); + coords.offset = model.offset; + return coords; + } +} + +class RemoteWindowCoords { + RemoteWindowCoords( + this.windowRect, this.canvas, this.cursor, this.remoteRect); + Rect windowRect; + CanvasCoords canvas; + CursorCoords cursor; + Rect remoteRect; + Offset relativeOffset = Offset.zero; + + Map toJson() { + return { + 'canvas': canvas.toJson(), + 'cursor': cursor.toJson(), + 'windowRect': rectToJson(windowRect), + 'remoteRect': rectToJson(remoteRect), + }; + } + + static Map rectToJson(Rect r) { + return { + 'l': r.left, + 't': r.top, + 'w': r.width, + 'h': r.height, + }; + } + + static Rect rectFromJson(Map json) { + return Rect.fromLTWH( + json['l'], + json['t'], + json['w'], + json['h'], + ); + } + + RemoteWindowCoords.fromJson(Map json) + : windowRect = rectFromJson(json['windowRect']), + canvas = CanvasCoords.fromJson(json['canvas']), + cursor = CursorCoords.fromJson(json['cursor']), + remoteRect = rectFromJson(json['remoteRect']); +} + extension ToString on MouseButtons { String get value { switch (this) { @@ -188,12 +313,17 @@ class InputModel { int _lastButtons = 0; Offset lastMousePos = Offset.zero; + bool _queryOtherWindowCoords = false; + Rect? _windowRect; + List _remoteWindowCoords = []; + late final SessionID sessionId; bool get keyboardPerm => parent.target!.ffiModel.keyboard; String get id => parent.target?.id ?? ''; String? get peerPlatform => parent.target?.ffiModel.pi.platform; bool get isViewOnly => parent.target!.ffiModel.viewOnly; + double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; InputModel(this.parent) { sessionId = parent.target!.sessionId; @@ -616,6 +746,9 @@ class InputModel { void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage ${e.kind}"); _stopFling = true; + if (isDesktop) _queryOtherWindowCoords = true; + _remoteWindowCoords = []; + _windowRect = null; if (isViewOnly) return; if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { @@ -628,6 +761,7 @@ class InputModel { } void onPointUpImage(PointerUpEvent e) { + if (isDesktop) _queryOtherWindowCoords = false; if (isViewOnly) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { @@ -638,11 +772,37 @@ class InputModel { void onPointMoveImage(PointerMoveEvent e) { if (isViewOnly) return; if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_queryOtherWindowCoords) { + Future.delayed(Duration.zero, () async { + _windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords); + }); + _queryOtherWindowCoords = false; + } if (isPhysicalMouse.value) { handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position); } } + static Future fillRemoteCoordsAndGetCurFrame( + List remoteWindowCoords) async { + final coords = + await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain(); + final wc = WindowController.fromWindowId(kWindowId!); + try { + final frame = await wc.getFrame(); + for (final c in coords) { + c.relativeOffset = Offset( + c.windowRect.left - frame.left, c.windowRect.top - frame.top); + remoteWindowCoords.add(c); + } + return frame; + } catch (e) { + // Unreachable code + debugPrint("Failed to get frame of window $kWindowId, it may be hidden"); + } + return null; + } + void onPointerSignalImage(PointerSignalEvent e) { if (isViewOnly) return; if (e is PointerScrollEvent) { @@ -843,43 +1003,107 @@ class InputModel { bool onExit = false, int buttons = kPrimaryMouseButton, }) { - y -= CanvasModel.topToEdge; - x -= CanvasModel.leftToEdge; - final canvasModel = parent.target!.canvasModel; final ffiModel = parent.target!.ffiModel; + CanvasCoords canvas = + CanvasCoords.fromCanvasModel(parent.target!.canvasModel); + Rect? rect = ffiModel.rect; + if (isMove) { - canvasModel.moveDesktopMouse(x, y); + if (_remoteWindowCoords.isNotEmpty && + _windowRect != null && + !_isInCurrentWindow(x, y)) { + final coords = + findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio); + if (coords != null) { + isMove = false; + canvas = coords.canvas; + rect = coords.remoteRect; + x -= coords.relativeOffset.dx / devicePixelRatio; + y -= coords.relativeOffset.dy / devicePixelRatio; + } + } } - final nearThr = 3; - var nearRight = (canvasModel.size.width - x) < nearThr; - var nearBottom = (canvasModel.size.height - y) < nearThr; - final rect = ffiModel.rect; + y -= CanvasModel.topToEdge; + x -= CanvasModel.leftToEdge; + if (isMove) { + parent.target!.canvasModel.moveDesktopMouse(x, y); + } + + return _handlePointerDevicePos( + kind, + x, + y, + isMove, + canvas, + rect, + evtType, + onExit: onExit, + buttons: buttons, + ); + } + + bool _isInCurrentWindow(double x, double y) { + final w = _windowRect!.width / devicePixelRatio; + final h = _windowRect!.width / devicePixelRatio; + return x >= 0 && y >= 0 && x <= w && y <= h; + } + + static RemoteWindowCoords? findRemoteCoords(double x, double y, + List remoteWindowCoords, double devicePixelRatio) { + x *= devicePixelRatio; + y *= devicePixelRatio; + for (final c in remoteWindowCoords) { + if (x >= c.relativeOffset.dx && + y >= c.relativeOffset.dy && + x <= c.relativeOffset.dx + c.windowRect.width && + y <= c.relativeOffset.dy + c.windowRect.height) { + return c; + } + } + return null; + } + + Point? _handlePointerDevicePos( + String kind, + double x, + double y, + bool moveInCanvas, + CanvasCoords canvas, + Rect? rect, + String evtType, { + bool onExit = false, + int buttons = kPrimaryMouseButton, + }) { if (rect == null) { return null; } - final imageWidth = rect.width * canvasModel.scale; - final imageHeight = rect.height * canvasModel.scale; - if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { - x += imageWidth * canvasModel.scrollX; - y += imageHeight * canvasModel.scrollY; + + final nearThr = 3; + var nearRight = (canvas.size.width - x) < nearThr; + var nearBottom = (canvas.size.height - y) < nearThr; + final imageWidth = rect.width * canvas.scale; + final imageHeight = rect.height * canvas.scale; + if (canvas.scrollStyle == ScrollStyle.scrollbar) { + x += imageWidth * canvas.scrollX; + y += imageHeight * canvas.scrollY; // boxed size is a center widget - if (canvasModel.size.width > imageWidth) { - x -= ((canvasModel.size.width - imageWidth) / 2); + if (canvas.size.width > imageWidth) { + x -= ((canvas.size.width - imageWidth) / 2); } - if (canvasModel.size.height > imageHeight) { - y -= ((canvasModel.size.height - imageHeight) / 2); + if (canvas.size.height > imageHeight) { + y -= ((canvas.size.height - imageHeight) / 2); } } else { - x -= canvasModel.x; - y -= canvasModel.y; + x -= canvas.x; + y -= canvas.y; } - x /= canvasModel.scale; - y /= canvasModel.scale; - if (canvasModel.scale > 0 && canvasModel.scale < 1) { - final step = 1.0 / canvasModel.scale - 1; + x /= canvas.scale; + y /= canvas.scale; + if (canvas.scale > 0 && canvas.scale < 1) { + final step = 1.0 / canvas.scale - 1; if (nearRight) { x += step; } @@ -902,8 +1126,7 @@ class InputModel { evtX = x.round(); evtY = y.round(); } catch (e) { - debugPrintStack( - label: 'canvasModel.scale value ${canvasModel.scale}, $e'); + debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e'); return null; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8f1bfe1c2..7edaa8f06 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1729,6 +1729,8 @@ class CursorModel with ChangeNotifier { double _displayOriginX = 0; double _displayOriginY = 0; DateTime? _firstUpdateMouseTime; + Rect? _windowRect; + List _remoteWindowCoords = []; bool gotMouseControl = true; DateTime _lastPeerMouse = DateTime.now() .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); @@ -1741,6 +1743,8 @@ class CursorModel with ChangeNotifier { double get x => _x - _displayOriginX; double get y => _y - _displayOriginY; + double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; + Offset get offset => Offset(_x, _y); double get hotx => _hotx; @@ -1810,15 +1814,13 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - updatePan(double dx, double dy, bool touchMode) { + updatePan(Offset delta, Offset localPosition, bool touchMode) { if (touchMode) { - final scale = parent.target?.canvasModel.scale ?? 1.0; - _x += dx / scale; - _y += dy / scale; - parent.target?.inputModel.moveMouse(_x, _y); - notifyListeners(); + _handleTouchMode(delta, localPosition); return; } + double dx = delta.dx; + double dy = delta.dy; if (parent.target?.imageModel.image == null) return; final scale = parent.target?.canvasModel.scale ?? 1.0; dx /= scale; @@ -1885,6 +1887,41 @@ class CursorModel with ChangeNotifier { notifyListeners(); } + bool _isInCurrentWindow(double x, double y) { + final w = _windowRect!.width / devicePixelRatio; + final h = _windowRect!.width / devicePixelRatio; + return x >= 0 && y >= 0 && x <= w && y <= h; + } + + _handleTouchMode(Offset delta, Offset localPosition) { + bool isMoved = false; + if (_remoteWindowCoords.isNotEmpty && + _windowRect != null && + !_isInCurrentWindow(localPosition.dx, localPosition.dy)) { + final coords = InputModel.findRemoteCoords(localPosition.dx, + localPosition.dy, _remoteWindowCoords, devicePixelRatio); + if (coords != null) { + double x2 = + (localPosition.dx - coords.relativeOffset.dx / devicePixelRatio) / + coords.canvas.scale; + double y2 = + (localPosition.dy - coords.relativeOffset.dy / devicePixelRatio) / + coords.canvas.scale; + x2 += coords.cursor.offset.dx; + y2 += coords.cursor.offset.dy; + parent.target?.inputModel.moveMouse(x2, y2); + isMoved = true; + } + } + if (!isMoved) { + final scale = parent.target?.canvasModel.scale ?? 1.0; + _x += delta.dx / scale; + _y += delta.dy / scale; + parent.target?.inputModel.moveMouse(_x, _y); + } + notifyListeners(); + } + updateCursorData(Map evt) async { final id = int.parse(evt['id']); final hotx = double.parse(evt['hotx']); @@ -2024,6 +2061,18 @@ class CursorModel with ChangeNotifier { deleteCustomCursor(k); } } + + trySetRemoteWindowCoords() { + Future.delayed(Duration.zero, () async { + _windowRect = + await InputModel.fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords); + }); + } + + clearRemoteWindowCoords() { + _windowRect = null; + _remoteWindowCoords.clear(); + } } class QualityMonitorData { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 0d157cb69..5aa59ee6a 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/input_model.dart'; /// must keep the order // ignore: constant_identifier_names @@ -431,6 +433,39 @@ class RustDeskMultiWindowManager { void unregisterActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.remove(callback); } + + // This function is called from the main window. + // It will query the active remote windows to get their coords. + Future> getOtherRemoteWindowCoords(int wId) async { + List coords = []; + for (final windowId in _remoteDesktopWindows) { + if (windowId != wId) { + if (_activeWindows.contains(windowId)) { + final res = await DesktopMultiWindow.invokeMethod( + windowId, kWindowEventRemoteWindowCoords, ''); + if (res != null) { + coords.add(res); + } + } + } + } + return coords; + } + + // This function is called from one remote window. + // Only the main window knows `_remoteDesktopWindows` and `_activeWindows`. + // So we need to call the main window to get the other remote windows' coords. + Future> getOtherRemoteWindowCoordsFromMain() async { + List coords = []; + // Call the main window to get the coords of other remote windows. + String res = await DesktopMultiWindow.invokeMethod( + kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString()); + List list = jsonDecode(res); + for (var item in list) { + coords.add(RemoteWindowCoords.fromJson(jsonDecode(item))); + } + return coords; + } } final rustDeskWinManager = RustDeskMultiWindowManager.instance; From b403a7a25dce68d02bdda3591f32f72e47f8efd2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 27 Apr 2024 15:01:21 +0800 Subject: [PATCH 20/35] fix: scroll percent is auto reset after detecting displays change (#7845) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7edaa8f06..486113eae 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1017,6 +1017,8 @@ class FfiModel with ChangeNotifier { } } } + parent.target!.canvasModel + .tryUpdateScrollStyle(Duration(milliseconds: 300), null); notifyListeners(); } @@ -1412,10 +1414,20 @@ class CanvasModel with ChangeNotifier { if (refreshMousePos) { parent.target?.inputModel.refreshMousePos(); } - if (style == kRemoteViewStyleOriginal && - _scrollStyle == ScrollStyle.scrollbar) { - updateScrollPercent(); + tryUpdateScrollStyle(Duration.zero, style); + } + + tryUpdateScrollStyle(Duration duration, String? style) async { + if (_scrollStyle != ScrollStyle.scrollbar) return; + style ??= await bind.sessionGetViewStyle(sessionId: sessionId); + if (style != kRemoteViewStyleOriginal) { + return; } + + _resetScroll(); + Future.delayed(duration, () async { + updateScrollPercent(); + }); } updateScrollStyle() async { From b022dcbb70e9a8b0a951f9e12a57784ede2e2a07 Mon Sep 17 00:00:00 2001 From: jxdv Date: Sat, 27 Apr 2024 08:40:24 +0000 Subject: [PATCH 21/35] update sk.rs (#7846) --- src/lang/sk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 450956ef1..15d1de1f6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -603,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Viac na webovej konzole"), ("allow-only-conn-window-open-tip", "Povoliť pripojenie iba vtedy, ak je otvorené okno aplikácie RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Žiadne fyzické displeje, nie je potrebné používať režim ochrany osobných údajov."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Nasledovať vzdialený kurzor"), + ("Follow remote window focus", "Nasledovať vzdialené zameranie okna"), ].iter().cloned().collect(); } From ade458b8202978f45979f86a6f1f925eaaa4734b Mon Sep 17 00:00:00 2001 From: jxdv Date: Sat, 27 Apr 2024 08:42:17 +0000 Subject: [PATCH 22/35] update cs.rs (#7847) --- src/lang/cs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 62ca5b4ac..b50e325fe 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -603,7 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Více na webové konzoli"), ("allow-only-conn-window-open-tip", "Povolit připojení pouze v případě, že je otevřené okno RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Žádné fyzické displeje, není třeba používat režim soukromí."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Follow remote cursor", "Sledovat dálkový kurzor"), + ("Follow remote window focus", "Sledovat zaměření vzdáleného okna"), ].iter().cloned().collect(); } From e9d9a656aba790804784652f42b9b62a46d60b36 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 27 Apr 2024 17:31:35 +0800 Subject: [PATCH 23/35] fix mouse wheel scroll up not work when controlling mac hidpi (#7848) Signed-off-by: 21pages --- src/server/connection.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/connection.rs b/src/server/connection.rs index 05a0054d5..d3a60b766 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3615,6 +3615,11 @@ impl Retina { #[inline] fn on_mouse_event(&mut self, e: &mut MouseEvent, current: usize) { + let evt_type = e.mask & 0x7; + if evt_type == crate::input::MOUSE_TYPE_WHEEL { + // x and y are always 0, +1 or -1 + return; + } let Some(d) = self.displays.get(current) else { return; }; From 45137d5506a83487d5b77578ed48267266e989df Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 27 Apr 2024 23:24:07 +0800 Subject: [PATCH 24/35] fix switch to setting page (#7849) Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 11 +- .../desktop/pages/desktop_setting_page.dart | 137 ++++++++++++------ .../lib/desktop/pages/desktop_tab_page.dart | 5 +- 3 files changed, 104 insertions(+), 49 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 6ec9fd360..0f190b181 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -153,7 +153,13 @@ class _DesktopHomePageState extends State size: 22, ), ), - onTap: () => DesktopSettingPage.switch2page(0), + onTap: () => { + if (DesktopSettingPage.tabKeys.isNotEmpty) + { + DesktopSettingPage.switch2page( + DesktopSettingPage.tabKeys[0]) + } + }, onHover: (value) => _editHover.value = value, ), ), @@ -347,7 +353,8 @@ class _DesktopHomePageState extends State ).marginOnly(right: 8, top: 4), ), ), - onTap: () => DesktopSettingPage.switch2page(0), + onTap: () => DesktopSettingPage.switch2page( + SettingsTabKey.safety), onHover: (value) => editHover.value = value, ), ], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 17998dd9a..ba61ef7aa 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -36,34 +36,57 @@ const double _kTitleFontSize = 20; const double _kContentFontSize = 15; const Color _accentColor = MyTheme.accent; const String _kSettingPageControllerTag = 'settingPageController'; -const String _kSettingPageIndexTag = 'settingPageIndex'; -const int _kPageCount = 6; +const String _kSettingPageTabKeyTag = 'settingPageTabKey'; class _TabInfo { + late final SettingsTabKey key; late final String label; late final IconData unselected; late final IconData selected; - _TabInfo(this.label, this.unselected, this.selected); + _TabInfo(this.key, this.label, this.unselected, this.selected); +} + +enum SettingsTabKey { + general, + safety, + network, + display, + plugin, + account, + about, } class DesktopSettingPage extends StatefulWidget { - final int initialPage; + final SettingsTabKey initialTabkey; + static final List tabKeys = [ + SettingsTabKey.general, + if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) + SettingsTabKey.safety, + if (!bind.isDisableSettings()) SettingsTabKey.network, + if (!bind.isIncomingOnly()) SettingsTabKey.display, + if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) + SettingsTabKey.plugin, + if (!bind.isDisableAccount()) SettingsTabKey.account, + SettingsTabKey.about, + ]; - const DesktopSettingPage({Key? key, required this.initialPage}) - : super(key: key); + DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key); @override State createState() => _DesktopSettingPageState(); - static void switch2page(int page) { - if (page >= _kPageCount) return; + static void switch2page(SettingsTabKey page) { try { + int index = tabKeys.indexOf(page); + if (index == -1) { + return; + } if (Get.isRegistered(tag: _kSettingPageControllerTag)) { DesktopTabPage.onAddSetting(initialPage: page); PageController controller = Get.find(tag: _kSettingPageControllerTag); - RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag); - selectedIndex.value = page; - controller.jumpToPage(page); + Rx selected = Get.find(tag: _kSettingPageTabKeyTag); + selected.value = page; + controller.jumpToPage(index); } else { DesktopTabPage.onAddSetting(initialPage: page); } @@ -76,7 +99,7 @@ class DesktopSettingPage extends StatefulWidget { class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { late PageController controller; - late RxInt selectedIndex; + late Rx selectedTab; @override bool get wantKeepAlive => true; @@ -84,14 +107,20 @@ class _DesktopSettingPageState extends State @override void initState() { super.initState(); - selectedIndex = - (widget.initialPage < _kPageCount ? widget.initialPage : 0).obs; - Get.put(selectedIndex, tag: _kSettingPageIndexTag); - controller = PageController(initialPage: widget.initialPage); + var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey); + if (initialIndex == -1) { + initialIndex = 0; + } + selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs; + Get.put>(selectedTab, tag: _kSettingPageTabKeyTag); + controller = PageController(initialPage: initialIndex); Get.put(controller, tag: _kSettingPageControllerTag); controller.addListener(() { if (controller.page != null) { - selectedIndex.value = controller.page!.toInt(); + int page = controller.page!.toInt(); + if (page < DesktopSettingPage.tabKeys.length) { + selectedTab.value = DesktopSettingPage.tabKeys[page]; + } } }); } @@ -100,26 +129,43 @@ class _DesktopSettingPageState extends State void dispose() { super.dispose(); Get.delete(tag: _kSettingPageControllerTag); - Get.delete(tag: _kSettingPageIndexTag); + Get.delete(tag: _kSettingPageTabKeyTag); } List<_TabInfo> _settingTabs() { - final List<_TabInfo> settingTabs = <_TabInfo>[ - _TabInfo('General', Icons.settings_outlined, Icons.settings), - if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) - _TabInfo('Security', Icons.enhanced_encryption_outlined, - Icons.enhanced_encryption), - if (!bind.isDisableSettings()) - _TabInfo('Network', Icons.link_outlined, Icons.link), - if (!bind.isIncomingOnly()) - _TabInfo( - 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows), - if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) - _TabInfo('Plugin', Icons.extension_outlined, Icons.extension), - if (!bind.isDisableAccount()) - _TabInfo('Account', Icons.person_outline, Icons.person), - _TabInfo('About', Icons.info_outline, Icons.info) - ]; + final List<_TabInfo> settingTabs = <_TabInfo>[]; + for (final tab in DesktopSettingPage.tabKeys) { + switch (tab) { + case SettingsTabKey.general: + settingTabs.add(_TabInfo( + tab, 'General', Icons.settings_outlined, Icons.settings)); + break; + case SettingsTabKey.safety: + settingTabs.add(_TabInfo(tab, 'Security', + Icons.enhanced_encryption_outlined, Icons.enhanced_encryption)); + break; + case SettingsTabKey.network: + settingTabs + .add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link)); + break; + case SettingsTabKey.display: + settingTabs.add(_TabInfo(tab, 'Display', + Icons.desktop_windows_outlined, Icons.desktop_windows)); + break; + case SettingsTabKey.plugin: + settingTabs.add(_TabInfo( + tab, 'Plugin', Icons.extension_outlined, Icons.extension)); + break; + case SettingsTabKey.account: + settingTabs.add( + _TabInfo(tab, 'Account', Icons.person_outline, Icons.person)); + break; + case SettingsTabKey.about: + settingTabs + .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info)); + break; + } + } return settingTabs; } @@ -198,26 +244,26 @@ class _DesktopSettingPageState extends State child: ListView( physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, - children: tabs - .asMap() - .entries - .map((tab) => _listItem(tab: tab.value, index: tab.key)) - .toList(), + children: tabs.map((tab) => _listItem(tab: tab)).toList(), )); } - Widget _listItem({required _TabInfo tab, required int index}) { + Widget _listItem({required _TabInfo tab}) { return Obx(() { - bool selected = index == selectedIndex.value; + bool selected = tab.key == selectedTab.value; return SizedBox( width: _kTabWidth, height: _kTabHeight, child: InkWell( onTap: () { - if (selectedIndex.value != index) { + if (selectedTab.value != tab.key) { + int index = DesktopSettingPage.tabKeys.indexOf(tab.key); + if (index == -1) { + return; + } controller.jumpToPage(index); } - selectedIndex.value = index; + selectedTab.value = tab.key; }, child: Row(children: [ Container( @@ -2064,8 +2110,9 @@ void changeSocks5Proxy() async { Expanded( child: TextField( decoration: InputDecoration( - errorText: proxyMsg.isNotEmpty ? proxyMsg : null, - hintText: translate('Default protocol and port are Socks5 and 1080'), + errorText: proxyMsg.isNotEmpty ? proxyMsg : null, + hintText: translate( + 'Default protocol and port are Socks5 and 1080'), ), controller: proxyController, autofocus: true, diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 46d15be54..99a28206d 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -17,7 +17,8 @@ class DesktopTabPage extends StatefulWidget { @override State createState() => _DesktopTabPageState(); - static void onAddSetting({int initialPage = 0}) { + static void onAddSetting( + {SettingsTabKey initialPage = SettingsTabKey.general}) { try { DesktopTabController tabController = Get.find(); tabController.add(TabInfo( @@ -27,7 +28,7 @@ class DesktopTabPage extends StatefulWidget { unselectedIcon: Icons.build_outlined, page: DesktopSettingPage( key: const ValueKey(kTabLabelSettingPage), - initialPage: initialPage, + initialTabkey: initialPage, ))); } catch (e) { debugPrintStack(label: '$e'); From 3bbec4081a3be4fa107895e31d25a53a7559328a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 10:44:14 +0800 Subject: [PATCH 25/35] add gtk modules --- appimage/AppImageBuilder-aarch64.yml | 2 ++ appimage/AppImageBuilder-x86_64.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index a285fc767..bfe5348f5 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -54,6 +54,8 @@ AppDir: - libwayland-cursor0 - libwayland-egl1 - libpulse0 + - packagekit-gtk3-module + - libcanberra-gtk-module exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 62c674a0c..7a4550efb 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -57,6 +57,8 @@ AppDir: - libwayland-cursor0 - libwayland-egl1 - libpulse0 + - packagekit-gtk3-module + - libcanberra-gtk-module exclude: - humanity-icon-theme - hicolor-icon-theme From e084ff4f7b4ecd350584b360c2f64940885fd0f1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 11:43:53 +0800 Subject: [PATCH 26/35] x64 mac --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index c66e30bb0..3c70f0d42 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -454,7 +454,7 @@ jobs: job: - { target: x86_64-apple-darwin, - os: macos-latest, + os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel extra-build-args: "", arch: x86_64, } From 30ad142868d8c1b1573dff22fa4bb4ad02331310 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 12:14:00 +0800 Subject: [PATCH 27/35] change to github m1 instead of selfhost m1 --- .github/workflows/flutter-build.yml | 113 ++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 3c70f0d42..ca38598b3 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -320,6 +320,8 @@ jobs: ./rustdesk-*.tar.gz build-for-macOS-arm64: + # use build-for-macOS instead + if: false name: build-for-macOS-arm64 runs-on: [self-hosted, macOS, ARM64] steps: @@ -392,6 +394,99 @@ jobs: build-rustdesk-ios: if: ${{ inputs.upload-artifact }} + name: build rustdesk ios ipa ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-apple-ios, + os: macos-13, + extra-build-features: "", + } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Install dependencies + run: | + brew install nasm + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" + shell: bash + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache-ios + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + + - name: Build rustdesk lib + run: | + rustup target add ${{ matrix.job.target }} + cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + + - name: Build rustdesk + shell: bash + run: | + pushd flutter + # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign + # for easy debugging + flutter build ipa --release --no-codesign + + # - name: Upload Artifacts + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: actions/upload-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + # path: flutter/build/ios/ipa/*.ipa + + # - name: Publish ipa package + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flutter/build/ios/ipa/*.ipa + + + build-rustdesk-ios-selfhost: + #if: ${{ inputs.upload-artifact }} + if: false runs-on: [self-hosted, macOS, ARM64] strategy: fail-fast: false @@ -458,6 +553,12 @@ jobs: extra-build-args: "", arch: x86_64, } + - { + target: aarch64-apple-darwin, + os: macos-latest, + extra-build-args: "", + arch: aarch64, + } steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -559,14 +660,14 @@ jobs: CREATE_DMG="$(command -v create-dmg)" CREATE_DMG="$(readlink -f "$CREATE_DMG")" sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" - create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-x64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - name: Upload unsigned macOS app if: env.UPLOAD_ARTIFACT == 'true' uses: actions/upload-artifact@master with: - name: rustdesk-unsigned-macos-x64 - path: rustdesk-${{ env.VERSION }}-x64.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed + name: rustdesk-unsigned-macos-${{ matrix.job.arch }} + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed - name: Codesign app and create signed dmg if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true' @@ -605,20 +706,20 @@ jobs: publish_unsigned_mac: needs: - build-for-macOS - - build-for-macOS-arm64 + #- build-for-macOS-arm64 runs-on: ubuntu-latest if: ${{ inputs.upload-artifact }} steps: - name: Download artifacts uses: actions/download-artifact@master with: - name: rustdesk-unsigned-macos-x64 + name: rustdesk-unsigned-macos-x86_64 path: ./ - name: Download Artifacts uses: actions/download-artifact@master with: - name: rustdesk-unsigned-macos-arm64 + name: rustdesk-unsigned-macos-aarch64 path: ./ - name: Combine unsigned macos app From 22e58d7623de21df032fc56d7e31eab477c4f1fd Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 28 Apr 2024 12:17:39 +0800 Subject: [PATCH 28/35] fix: test if valid server (#7856) * fix: #7853, test if valid server Signed-off-by: fufesou * refact comments Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/hbb_common/src/socket_client.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index aaafeb861..668a07d7d 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -54,6 +54,15 @@ pub fn increase_port(host: T, offset: i32) -> String { pub fn test_if_valid_server(host: &str) -> String { info!("Testing server validity for host: {}", host); use std::net::ToSocketAddrs; + + let host = if !host.contains("://") { + // We just add a scheme for testing the domain and port parts, + // we don't care about the scheme, so "http://" is used for simple. + format!("http://{}", host) + } else { + host.to_string() + }; + // Even if the current network type is a proxy type, // the system DNS should be used to resolve the proxy server address. host.into_proxy_scheme() @@ -249,6 +258,11 @@ mod tests { // on Linux, "1" is resolved to "0.0.0.1" assert!(test_if_valid_server("1.1.1.1").is_empty()); assert!(test_if_valid_server("1.1.1.1:1").is_empty()); + assert!(test_if_valid_server("abcd.com:1").is_empty()); + assert!(test_if_valid_server("http://abcd.com:1").is_empty()); + assert!(test_if_valid_server("https://abcd.com:1").is_empty()); + assert!(test_if_valid_server("socks5://abcd.com:1").is_empty()); + assert!(test_if_valid_server("https://1.1.1.1:1").is_empty()); } #[test] From 99d7752e250e56d5f3d1c6d0e4ccb2a357477d2b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 12:58:15 +0800 Subject: [PATCH 29/35] try libcanberra-gtk3-module --- .github/workflows/flutter-build.yml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index ca38598b3..b1287150b 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -319,7 +319,7 @@ jobs: ./SignOutput/rustdesk-*.exe ./rustdesk-*.tar.gz - build-for-macOS-arm64: + build-for-macOS-arm64-selfhost: # use build-for-macOS instead if: false name: build-for-macOS-arm64 diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index bfe5348f5..e78b0f286 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -55,7 +55,7 @@ AppDir: - libwayland-egl1 - libpulse0 - packagekit-gtk3-module - - libcanberra-gtk-module + - libcanberra-gtk3-module exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 7a4550efb..b1bd2ee3f 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -58,7 +58,7 @@ AppDir: - libwayland-egl1 - libpulse0 - packagekit-gtk3-module - - libcanberra-gtk-module + - libcanberra-gtk3-module exclude: - humanity-icon-theme - hicolor-icon-theme From bd717349a7fd1449c3f048992353da290f3153b3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Apr 2024 13:26:55 +0800 Subject: [PATCH 30/35] update hwcodec, fix screen jitter (#7857) Signed-off-by: 21pages --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f8f6f745..cc68ce8ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3038,8 +3038,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.4.2" -source = "git+https://github.com/21pages/hwcodec#b8ed1e869b3456af6a5f77b7617f861d9a99dcdd" +version = "0.4.3" +source = "git+https://github.com/21pages/hwcodec#db7c2d4afcb4947bfb452213ef7e9ba647578b43" dependencies = [ "bindgen 0.59.2", "cc", From 1dfbaa1e02e70fc0d7e08153b06ab513cc8a10b7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 28 Apr 2024 14:22:21 +0800 Subject: [PATCH 31/35] fix: test if valid server, control if try test with proxy (#7858) * fix: test if valid server, control if try test with proxy Signed-off-by: fufesou * fix: build Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 8 +-- .../desktop/pages/desktop_setting_page.dart | 7 ++- flutter/lib/mobile/widgets/dialog.dart | 9 --- flutter/lib/web/bridge.dart | 5 +- libs/hbb_common/src/socket_client.rs | 56 ++++++++++--------- src/flutter_ffi.rs | 6 +- src/ui.rs | 6 +- src/ui/index.tis | 6 +- src/ui_interface.rs | 4 +- 9 files changed, 55 insertions(+), 52 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 3106aaaaf..40c4d46e0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2978,16 +2978,16 @@ Future setServerConfig( } // id if (config.idServer.isNotEmpty && errMsgs != null) { - errMsgs[0].value = - translate(await bind.mainTestIfValidServer(server: config.idServer)); + errMsgs[0].value = translate(await bind.mainTestIfValidServer( + server: config.idServer, testWithProxy: true)); if (errMsgs[0].isNotEmpty) { return false; } } // relay if (config.relayServer.isNotEmpty && errMsgs != null) { - errMsgs[1].value = - translate(await bind.mainTestIfValidServer(server: config.relayServer)); + errMsgs[1].value = translate(await bind.mainTestIfValidServer( + server: config.relayServer, testWithProxy: true)); if (errMsgs[1].isNotEmpty) { return false; } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ba61ef7aa..fab7b56d7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2079,7 +2079,12 @@ void changeSocks5Proxy() async { password = pwdController.text.trim(); if (proxy.isNotEmpty) { - proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy)); + String domainPort = proxy; + if (domainPort.contains('://')) { + domainPort = domainPort.split('://')[1]; + } + proxyMsg = translate(await bind.mainTestIfValidServer( + server: domainPort, testWithProxy: false)); if (proxyMsg.isEmpty) { // ignore } else { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 68d253d7b..391bec669 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -283,12 +283,3 @@ void setPrivacyModeDialog( ); }, backDismiss: true, clickMaskDismiss: true); } - -Future validateAsync(String value) async { - value = value.trim(); - if (value.isEmpty) { - return null; - } - final res = await bind.mainTestIfValidServer(server: value); - return res.isEmpty ? null : res; -} diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 1ccc266ac..87a478508 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -680,7 +680,8 @@ class RustdeskImpl { return Future(() => js.context.callMethod('setByName', ['options', json])); } - Future mainTestIfValidServer({required String server, dynamic hint}) { + Future mainTestIfValidServer( + {required String server, required bool testWithProxy, dynamic hint}) { // TODO: implement return Future.value(''); } @@ -788,7 +789,7 @@ class RustdeskImpl { throw UnimplementedError(); } - Future mainGetHttpStatus({required String url, dynamic hint}){ + Future mainGetHttpStatus({required String url, dynamic hint}) { throw UnimplementedError(); } diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 668a07d7d..0264b70dd 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -1,13 +1,11 @@ use crate::{ config::{Config, NetworkType}, - proxy::IntoProxyScheme, tcp::FramedStream, udp::FramedSocket, ResultType, }; use anyhow::Context; use std::net::SocketAddr; -use log::info; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; @@ -51,25 +49,28 @@ pub fn increase_port(host: T, offset: i32) -> String { host } -pub fn test_if_valid_server(host: &str) -> String { - info!("Testing server validity for host: {}", host); +pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String { + let host = check_port(host, 0); use std::net::ToSocketAddrs; - let host = if !host.contains("://") { - // We just add a scheme for testing the domain and port parts, - // we don't care about the scheme, so "http://" is used for simple. - format!("http://{}", host) + if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() { + test_if_valid_server_for_proxy_(&host) } else { - host.to_string() - }; + match host.to_socket_addrs() { + Err(err) => err.to_string(), + Ok(_) => "".to_owned(), + } + } +} - // Even if the current network type is a proxy type, - // the system DNS should be used to resolve the proxy server address. - host.into_proxy_scheme() - .and_then(|scheme| scheme.get_host_and_port()) - .and_then(|domain| domain.to_socket_addrs().map_err(Into::into)) - .map(|_| "".to_owned()) // on success, return an empty string - .unwrap_or_else(|e| e.to_string()) // on error, convert the error into a string +#[inline] +pub fn test_if_valid_server_for_proxy_(host: &str) -> String { + // `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing, + // it can be used for both `socks` and `http` proxy. + match &host.into_target_addr() { + Err(err) => err.to_string(), + Ok(_) => "".to_owned(), + } } pub trait IsResolvedSocketAddr { @@ -254,15 +255,20 @@ mod tests { #[test] fn test_test_if_valid_server() { - assert!(!test_if_valid_server("a").is_empty()); + assert!(!test_if_valid_server("a", false).is_empty()); // on Linux, "1" is resolved to "0.0.0.1" - assert!(test_if_valid_server("1.1.1.1").is_empty()); - assert!(test_if_valid_server("1.1.1.1:1").is_empty()); - assert!(test_if_valid_server("abcd.com:1").is_empty()); - assert!(test_if_valid_server("http://abcd.com:1").is_empty()); - assert!(test_if_valid_server("https://abcd.com:1").is_empty()); - assert!(test_if_valid_server("socks5://abcd.com:1").is_empty()); - assert!(test_if_valid_server("https://1.1.1.1:1").is_empty()); + assert!(test_if_valid_server("1.1.1.1", false).is_empty()); + assert!(test_if_valid_server("1.1.1.1:1", false).is_empty()); + assert!(test_if_valid_server("abcd.com", false).is_empty()); + assert!(test_if_valid_server("abcd.com:1", false).is_empty()); + + // with proxy + // `:0` indicates `let host = check_port(host, 0);` is called. + assert!(test_if_valid_server_for_proxy_("a:0").is_empty()); + assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty()); + assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty()); + assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty()); + assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty()); } #[test] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 518dd47dd..a41e8b7a1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -809,8 +809,8 @@ pub fn main_set_options(json: String) { } } -pub fn main_test_if_valid_server(server: String) -> String { - test_if_valid_server(server) +pub fn main_test_if_valid_server(server: String, test_with_proxy: bool) -> String { + test_if_valid_server(server, test_with_proxy) } pub fn main_set_socks(proxy: String, username: String, password: String) { @@ -895,7 +895,7 @@ pub fn main_get_api_server() -> String { } pub fn main_http_request(url: String, method: String, body: Option, header: String) { - http_request(url,method, body, header) + http_request(url, method, body, header) } pub fn main_get_local_option(key: String) -> SyncReturn { diff --git a/src/ui.rs b/src/ui.rs index 2e9ea2f91..16a7b1a65 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -272,8 +272,8 @@ impl UI { m } - fn test_if_valid_server(&self, host: String) -> String { - test_if_valid_server(host) + fn test_if_valid_server(&self, host: String, test_with_proxy: bool) -> String { + test_if_valid_server(host, test_with_proxy) } fn get_sound_inputs(&self) -> Value { @@ -689,7 +689,7 @@ impl sciter::EventHandler for UI { fn forget_password(String); fn set_peer_option(String, String, String); fn get_license(); - fn test_if_valid_server(String); + fn test_if_valid_server(String, bool); fn get_sound_inputs(); fn set_options(Value); fn set_option(String, String); diff --git a/src/ui/index.tis b/src/ui/index.tis index f202d0fad..260e23e95 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -441,11 +441,11 @@ class MyIdMenu: Reactor.Component { var key = (res.key || "").trim(); if (id == old_id && relay == old_relay && key == old_key && api == old_api) return; if (id) { - var err = handler.test_if_valid_server(id); + var err = handler.test_if_valid_server(id, true); if (err) return translate("ID Server") + ": " + err; } if (relay) { - var err = handler.test_if_valid_server(relay); + var err = handler.test_if_valid_server(relay, true); if (err) return translate("Relay Server") + ": " + err; } if (api) { @@ -476,7 +476,7 @@ class MyIdMenu: Reactor.Component { var password = (res.password || "").trim(); if (proxy == old_proxy && username == old_username && password == old_password) return; if (proxy) { - var err = handler.test_if_valid_server(proxy); + var err = handler.test_if_valid_server(proxy, false); if (err) return translate("Server") + ": " + err; } handler.set_socks(proxy, username, password); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 313b6e562..af8655a41 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -280,8 +280,8 @@ pub fn get_options() -> String { } #[inline] -pub fn test_if_valid_server(host: String) -> String { - hbb_common::socket_client::test_if_valid_server(&host) +pub fn test_if_valid_server(host: String, test_with_proxy: bool) -> String { + hbb_common::socket_client::test_if_valid_server(&host, test_with_proxy) } #[inline] From b4c51f3d416d52fc468496bd9fafed9f67c3fe3a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 20:24:39 +0800 Subject: [PATCH 32/35] add libnsl2 for fedora 40 --- appimage/AppImageBuilder-aarch64.yml | 1 + appimage/AppImageBuilder-x86_64.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index e78b0f286..5724b524d 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -56,6 +56,7 @@ AppDir: - libpulse0 - packagekit-gtk3-module - libcanberra-gtk3-module + - libnsl2 exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index b1bd2ee3f..3cabfbb0f 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -59,6 +59,7 @@ AppDir: - libpulse0 - packagekit-gtk3-module - libcanberra-gtk3-module + - libnsl2 exclude: - humanity-icon-theme - hicolor-icon-theme From 7e263af75f4974f3d30b94f7ff3b9410f036c885 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 21:08:49 +0800 Subject: [PATCH 33/35] VideoConnCount for future use --- src/ipc.rs | 12 +++++++++++- src/server/connection.rs | 2 +- src/ui_cm_interface.rs | 3 ++- src/ui_interface.rs | 36 +++++++++++++++++++++++------------- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/ipc.rs b/src/ipc.rs index 3ee18be43..7957b8db6 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -233,6 +233,7 @@ pub enum Data { ControlledSessionCount(usize), CmErr(String), CheckHwcodec, + VideoConnCount(Option), } #[tokio::main(flavor = "current_thread")] @@ -382,6 +383,15 @@ async fn handle(data: Data, stream: &mut Connection) { log::info!("socks updated"); } }, + Data::VideoConnCount(None) => { + let n = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|x| x.1 == crate::server::AuthConnType::Remote) + .count(); + allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await); + } Data::Config((name, value)) => match value { None => { let value; @@ -905,7 +915,7 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { } pub fn get_proxy_status() -> bool { - Config::get_socks().is_some() + Config::get_socks().is_some() } #[tokio::main(flavor = "current_thread")] pub async fn test_rendezvous_server() -> ResultType<()> { diff --git a/src/server/connection.rs b/src/server/connection.rs index d3a60b766..1dc9e1d1b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -69,7 +69,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref AUTHED_CONNS: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c4cdbf784..7b09640f5 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -78,8 +78,9 @@ struct IpcTaskRunner { lazy_static::lazy_static! { static ref CLIENTS: RwLock> = Default::default(); - static ref CLICK_TIME: AtomicI64 = AtomicI64::new(0); } + +static CLICK_TIME: AtomicI64 = AtomicI64::new(0); #[derive(Clone)] pub struct ConnectionManager { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index af8655a41..c6309f599 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -3,9 +3,7 @@ use hbb_common::password_security; use hbb_common::{ allow_err, bytes::Bytes, - config::{ - self, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT, - }, + config::{self, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT}, directories_next, futures::future::join_all, log, @@ -22,6 +20,7 @@ use serde_derive::Serialize; use std::process::Child; use std::{ collections::HashMap, + sync::atomic::{AtomicUsize, Ordering}, sync::{Arc, Mutex}, }; @@ -69,6 +68,8 @@ lazy_static::lazy_static! { static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); } +pub static VIDEO_CONN_COUNT: AtomicUsize = AtomicUsize::new(0); + #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref OPTION_SYNCED: Arc> = Default::default(); @@ -426,7 +427,7 @@ pub fn set_socks(proxy: String, username: String, password: String) { pub fn get_proxy_status() -> bool { #[cfg(not(any(target_os = "android", target_os = "ios")))] return ipc::get_proxy_status(); - + // Currently, only the desktop version has proxy settings. #[cfg(any(target_os = "android", target_os = "ios"))] return false; @@ -723,24 +724,29 @@ pub fn change_id(id: String) { pub fn http_request(url: String, method: String, body: Option, header: String) { // Respond to concurrent requests for resources let current_request = ASYNC_HTTP_STATUS.clone(); - current_request.lock().unwrap().insert(url.clone()," ".to_owned()); + current_request + .lock() + .unwrap() + .insert(url.clone(), " ".to_owned()); std::thread::spawn(move || { - let res = match crate::http_request_sync(url.clone(), method, body, header) { - Err(err) => { log::error!("{}", err); err.to_string() }, - Ok(text) => text, - }; - current_request.lock().unwrap().insert(url,res); + let res = match crate::http_request_sync(url.clone(), method, body, header) { + Err(err) => { + log::error!("{}", err); + err.to_string() + } + Ok(text) => text, + }; + current_request.lock().unwrap().insert(url, res); }); } #[inline] pub fn get_async_http_status(url: String) -> Option { match ASYNC_HTTP_STATUS.lock().unwrap().get(&url) { - None => {None} - Some(_str) => {Some(_str.to_string())} + None => None, + Some(_str) => Some(_str.to_string()), } } - #[inline] pub fn post_request(url: String, body: String, header: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); @@ -1116,6 +1122,9 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { + VIDEO_CONN_COUNT.store(n, Ordering::Relaxed); + } Ok(Some(ipc::Data::OnlineStatus(Some((mut x, _c))))) => { if x > 0 { x = 1 @@ -1145,6 +1154,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver Date: Sun, 28 Apr 2024 21:12:04 +0800 Subject: [PATCH 34/35] change to libnsl --- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 5724b524d..aea104383 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -56,7 +56,7 @@ AppDir: - libpulse0 - packagekit-gtk3-module - libcanberra-gtk3-module - - libnsl2 + - libnsl exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 3cabfbb0f..48188953f 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -59,7 +59,7 @@ AppDir: - libpulse0 - packagekit-gtk3-module - libcanberra-gtk3-module - - libnsl2 + - libnsl exclude: - humanity-icon-theme - hicolor-icon-theme From 96ec1e937f6aaf252b5c6e71a9fe21a4b063e59d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 28 Apr 2024 23:06:37 +0800 Subject: [PATCH 35/35] libnsl.so.1 is included in libc6(already added), but it did not work, we still need to install libnsl in fedora, however, installing libnsl on x64 works, but on arm64 crashed (though no librustdesk.so any more) --- appimage/AppImageBuilder-aarch64.yml | 1 - appimage/AppImageBuilder-x86_64.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index aea104383..e78b0f286 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -56,7 +56,6 @@ AppDir: - libpulse0 - packagekit-gtk3-module - libcanberra-gtk3-module - - libnsl exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 48188953f..b1bd2ee3f 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -59,7 +59,6 @@ AppDir: - libpulse0 - packagekit-gtk3-module - libcanberra-gtk3-module - - libnsl exclude: - humanity-icon-theme - hicolor-icon-theme