From d878222fc185eb2c235bf2fbc461deed05be71f7 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Sun, 7 Aug 2022 00:24:57 +0200 Subject: [PATCH 01/14] rustdesk-server doctor --- Cargo.toml | 2 ++ src/utils.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d652980..8d51a11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ http = "0.2" flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] } ipnetwork = "0.20" local-ip-address = "0.4" +dns-lookup = "1.0.8" +ping = "0.4.0" [build-dependencies] hbb_common = { path = "libs/hbb_common" } diff --git a/src/utils.rs b/src/utils.rs index 7eb7a58..d851cf1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,11 @@ +use dns_lookup::{lookup_addr, lookup_host}; use hbb_common::{bail, ResultType}; use sodiumoxide::crypto::sign; -use std::env; -use std::process; -use std::str; +use std::{ + env, + net::{IpAddr, TcpStream}, + process, str, +}; fn print_help() { println!( @@ -10,7 +13,8 @@ fn print_help() { rustdesk-util [command]\n Available Commands: genkeypair Generate a new keypair - validatekeypair [public key] [secret key] Validate an existing keypair" + validatekeypair [public key] [secret key] Validate an existing keypair + doctor [rustdesk-server] Check for server connection problems" ); process::exit(0x0001); } @@ -68,6 +72,79 @@ fn validate_keypair(pk: &str, sk: &str) -> ResultType<()> { Ok(()) } +fn doctor_tcp(address: std::net::IpAddr, port: &str, desc: &str) { + let start = std::time::Instant::now(); + let conn = format!("{}:{}", address, port); + if let Ok(_stream) = TcpStream::connect(conn.as_str()) { + let elapsed = std::time::Instant::now().duration_since(start); + println!( + "TCP Port {} ({}): OK in {} ms", + port, + desc, + elapsed.as_millis() + ); + } else { + println!("TCP Port {} ({}): ERROR", port, desc); + } +} + +fn doctor_ip(server_ip_address: std::net::IpAddr, server_address: Option<&str>) { + println!("\nChecking IP address: {}", server_ip_address); + println!("Is IPV4: {}", server_ip_address.is_ipv4()); + println!("Is IPV6: {}", server_ip_address.is_ipv6()); + + // reverse dns lookup + // TODO: (check) doesn't seem to do reverse lookup on OSX... + let reverse = lookup_addr(&server_ip_address).unwrap(); + if server_address.is_some() { + if reverse == server_address.unwrap() { + println!("Reverse DNS lookup: '{}' MATCHES server address", reverse); + } else { + println!( + "Reverse DNS lookup: '{}' DOESN'T MATCH server address '{}'", + reverse, + server_address.unwrap() + ); + } + } + + // TODO: ICMP ping? + + // port check TCP (UDP is hard to check) + doctor_tcp(server_ip_address, "21114", "API"); + doctor_tcp(server_ip_address, "21115", "hbbs extra port for nat test"); + doctor_tcp(server_ip_address, "21116", "hbbs"); + doctor_tcp(server_ip_address, "21117", "hbbr tcp"); + doctor_tcp(server_ip_address, "21118", "hbbs websocket"); + doctor_tcp(server_ip_address, "21119", "hbbr websocket"); + + // TODO: key check +} + +fn doctor(server_address_unclean: &str) { + let server_address3 = server_address_unclean.trim(); + let server_address2 = server_address3.to_lowercase(); + let server_address = server_address2.as_str(); + println!("Checking server: {}\n", server_address); + let server_ipaddr = server_address.parse::(); + if server_ipaddr.is_err() { + // the passed string is not an ip address + let ips: Vec = lookup_host(server_address).unwrap(); + println!("Found {} IP addresses: ", ips.iter().count()); + + for ip in ips.iter() { + println!(" - {}", ip); + } + + for ip in ips.iter() { + doctor_ip(*ip, Some(server_address)); + } + } else { + // user requested an ip address + doctor_ip(server_ipaddr.unwrap(), None); + } +} + fn main() { let args: Vec<_> = env::args().collect(); if args.len() <= 1 { @@ -88,6 +165,12 @@ fn main() { } println!("Key pair is VALID"); } + "doctor" => { + if args.len() <= 2 { + error_then_help("You must supply the rustdesk-server address"); + } + doctor(args[2].as_str()); + } _ => print_help(), } } From cbadfcdfb19a43df995d736948648b2efc5ab826 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Mon, 8 Aug 2022 12:10:30 +0200 Subject: [PATCH 02/14] readme update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 18ecd43..2c84543 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ We use these environment variables: | DB_URL | yes | path for database file | | KEY_PUB | yes | public part of the key pair | | KEY_PRIV | yes | private part of the key pair | +| RUST_LOG | yes | set debug level (error|warn|info|debug|trace) | ### Secret management in S6-overlay based images From bf3e9471a6710ca19da2eff38f5cb35151edf787 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Tue, 9 Aug 2022 10:07:01 +0200 Subject: [PATCH 03/14] proposed modifications --- src/utils.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index d851cf1..7b36aed 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -132,13 +132,10 @@ fn doctor(server_address_unclean: &str) { let ips: Vec = lookup_host(server_address).unwrap(); println!("Found {} IP addresses: ", ips.iter().count()); - for ip in ips.iter() { - println!(" - {}", ip); - } + ips.iter().for_each(|ip| println!(" - {ip}")); + + ips.iter().for_each(|ip| doctor_ip(*ip, Some(server_address))); - for ip in ips.iter() { - doctor_ip(*ip, Some(server_address)); - } } else { // user requested an ip address doctor_ip(server_ipaddr.unwrap(), None); From 506b0b5364f3072b34544919aaa581b323dc4f4d Mon Sep 17 00:00:00 2001 From: Miguel Agueda <53227622+miguelagve@users.noreply.github.com> Date: Sat, 3 Sep 2022 03:03:32 +0200 Subject: [PATCH 04/14] Update README.md for SELinux comment Added comment noting the changes required to make the containers work on a system using SELinux --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2c84543..e0272e1 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ docker run --name hbbr --net=host -v "$PWD/data:/root" -d rustdesk/rustdesk-serv or without --net=host, but P2P direct connection can not work. +For systems using SELinux, replacing `/root` by `/root:z` is required for the containers to run correctly. Alternatively, SELinux container separation can be disabled completely adding the option `--security-opt label=disable`. + ```bash docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbs -r docker run --name hbbr -p 21117:21117 -p 21119:21119 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbr From 1b440b61e7f83f289454e9eec4cc6b488b10ec08 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Mon, 5 Sep 2022 09:36:03 +0200 Subject: [PATCH 05/14] Artifacts in zip should be executables --- .github/workflows/build.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 461f80c..4ef98a9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -59,6 +59,9 @@ jobs: # - name: Run tests # run: cargo test --verbose + - name: Exec chmod + run: chmod -v a+x target/${{ matrix.job.target }}/release/* + - name: Publish Artifacts uses: actions/upload-artifact@v3 with: @@ -92,6 +95,9 @@ jobs: name: binaries-${{ matrix.job.name }} path: ${{ matrix.job.name }} + - name: Exec chmod + run: chmod -v a+x ${{ matrix.job.name }}/* + - name: Pack files (${{ matrix.job.name }}) run: | sudo apt update From 4baab9618386a8a5d9dd86290a597d508d1f0ad1 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Mon, 5 Sep 2022 11:54:39 +0200 Subject: [PATCH 06/14] hbbr can use ENV from docker --- docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run index eae3611..c7e1b19 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run @@ -1,3 +1,3 @@ -#!/command/execlineb -P -posix-cd /data +#!/command/with-contenv sh +cd /data /usr/bin/hbbr From c16101a44c9279bc9b12cb8d4d619ebc239b3749 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Mon, 5 Sep 2022 20:30:50 +0200 Subject: [PATCH 07/14] env variables doc --- README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e0272e1..8254884 100644 --- a/README.md +++ b/README.md @@ -173,16 +173,14 @@ services: restart: unless-stopped ``` -We use these environment variables: +For this container image, you can use these environment variables, **in addition** to the ones specified in the following **ENV variables** section: | variable | optional | description | | --- | --- | --- | | RELAY | no | the IP address/DNS name of the machine running this container | | ENCRYPTED_ONLY | yes | if set to **"1"** unencrypted connection will not be accepted | -| DB_URL | yes | path for database file | | KEY_PUB | yes | public part of the key pair | | KEY_PRIV | yes | private part of the key pair | -| RUST_LOG | yes | set debug level (error|warn|info|debug|trace) | ### Secret management in S6-overlay based images @@ -316,3 +314,28 @@ These packages are meant for the following distributions: - Ubuntu 18.04 LTS - Debian 11 bullseye - Debian 10 buster + +## ENV variables + +hbbs and hbbr can be configured using these ENV variables. +You can specify the variables as usual or use an `.env` file. + +| variable | binary | description | +| --- | --- | --- | +| ALWAYS_USE_RELAY | hbbs | if set to **"Y"** disallows direct peer connection | +| DB_URL | hbbs | path for database file | +| DOWNGRADE_START_CHECK | hbbr | delay (in seconds) before downgrade check | +| DOWNGRADE_THRESHOLD | hbbr | threshold of downgrade check (bit/ms) | +| KEY | hbbs/hbbr | if set force the use of a specific key, if set to **"_"** force the use of any key | +| LIMIT_SPEED | hbbr | speed limit (in Mb/s) | +| LOCAL_IP | hbbs | hbbs IP address used in hole-punching | +| MASK | hbbs | network+mask of LAN IPs | +| PORT | hbbs/hbbr | listening port (21116 for hbbs - 21117 for hbbr) | +| RELAY_SERVERS | hbbs | IP address/DNS name of the machines running hbbr (separated by comma) | +| RENDEZVOUS_SERVERS | hbbs | IP address/DNS name of the machines running hbbs (separated by comma) | +| RMEM | hbbs | UDP recv buffer size | +| RUST_LOG | all | set debug level (error|warn|info|debug|trace) | +| SINGLE_BANDWIDTH | hbbr | max bandwidth for a single connection (in Mb/s) | +| SOFTWARE_URL hbbs | hbbs | download url of RustDesk newest version | +| TEST_HBBS | hbbs | IP address of hbbs for avoiding udp socket failure error | +| TOTAL_BANDWIDTH | hbbr | max total bandwidth (in Mb/s) | From 099aaa6b5510db6678e61ab30ef7f0b193f7a36a Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 3 Oct 2022 20:52:44 +0800 Subject: [PATCH 08/14] query_onlines: trivial refactor Signed-off-by: fufesou --- src/rendezvous_server.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index b93e04f..9cea15d 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -759,15 +759,11 @@ impl RendezvousServer { ) -> ResultType<()> { let mut states = BytesMut::zeroed((peers.len() + 7) / 8); for i in 0..peers.len() { - let peer_id = &peers[i]; - // bytes index from left to right - let states_idx = i / 8; - let bit_idx = 7 - i % 8; - if let Some(peer) = self.pm.get_in_memory(&peer_id).await { - let (elapsed, _) = { - let r = peer.read().await; - (r.last_reg_time.elapsed().as_millis() as i32, r.socket_addr) - }; + if let Some(peer) = self.pm.get_in_memory(&peers[i]).await { + let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i32; + // bytes index from left to right + let states_idx = i / 8; + let bit_idx = 7 - i % 8; if elapsed < REG_TIMEOUT { states[states_idx] |= 0x01 << bit_idx; } From 24620c0a07b6f1a2b76154ce50905db837326aef Mon Sep 17 00:00:00 2001 From: Jivin Date: Sun, 20 Nov 2022 09:55:17 -0500 Subject: [PATCH 09/14] fix build issue 'error: non-binding let on a synchronization lock' --- libs/hbb_common/src/config.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index ea7dd6d..c3a502b 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -693,7 +693,7 @@ const PEERS: &str = "peers"; impl PeerConfig { pub fn load(id: &str) -> PeerConfig { - let _ = CONFIG.read().unwrap(); // for lock + let _unused = CONFIG.read().unwrap(); // for lock match confy::load_path(&Self::path(id)) { Ok(config) => config, Err(err) => { @@ -704,7 +704,7 @@ impl PeerConfig { } pub fn store(&self, id: &str) { - let _ = CONFIG.read().unwrap(); // for lock + let _unused = CONFIG.read().unwrap(); // for lock if let Err(err) = confy::store_path(Self::path(id), self) { log::error!("Failed to store config: {}", err); } @@ -845,7 +845,7 @@ pub struct LanPeers { impl LanPeers { pub fn load() -> LanPeers { - let _ = CONFIG.read().unwrap(); // for lock + let _unused = CONFIG.read().unwrap(); // for lock match confy::load_path(&Config::file_("_lan_peers")) { Ok(peers) => peers, Err(err) => { From 650f2410ed0133a1527619dbb1ca34be071ad666 Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Mon, 5 Sep 2022 11:54:39 +0200 Subject: [PATCH 10/14] hbbr can use ENV from docker --- docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run index eae3611..c7e1b19 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run @@ -1,3 +1,3 @@ -#!/command/execlineb -P -posix-cd /data +#!/command/with-contenv sh +cd /data /usr/bin/hbbr From 29b45dddb455be6501cb58b2c82abeaaa8c95bad Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Mon, 5 Sep 2022 20:30:50 +0200 Subject: [PATCH 11/14] env variables doc --- README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e0272e1..8254884 100644 --- a/README.md +++ b/README.md @@ -173,16 +173,14 @@ services: restart: unless-stopped ``` -We use these environment variables: +For this container image, you can use these environment variables, **in addition** to the ones specified in the following **ENV variables** section: | variable | optional | description | | --- | --- | --- | | RELAY | no | the IP address/DNS name of the machine running this container | | ENCRYPTED_ONLY | yes | if set to **"1"** unencrypted connection will not be accepted | -| DB_URL | yes | path for database file | | KEY_PUB | yes | public part of the key pair | | KEY_PRIV | yes | private part of the key pair | -| RUST_LOG | yes | set debug level (error|warn|info|debug|trace) | ### Secret management in S6-overlay based images @@ -316,3 +314,28 @@ These packages are meant for the following distributions: - Ubuntu 18.04 LTS - Debian 11 bullseye - Debian 10 buster + +## ENV variables + +hbbs and hbbr can be configured using these ENV variables. +You can specify the variables as usual or use an `.env` file. + +| variable | binary | description | +| --- | --- | --- | +| ALWAYS_USE_RELAY | hbbs | if set to **"Y"** disallows direct peer connection | +| DB_URL | hbbs | path for database file | +| DOWNGRADE_START_CHECK | hbbr | delay (in seconds) before downgrade check | +| DOWNGRADE_THRESHOLD | hbbr | threshold of downgrade check (bit/ms) | +| KEY | hbbs/hbbr | if set force the use of a specific key, if set to **"_"** force the use of any key | +| LIMIT_SPEED | hbbr | speed limit (in Mb/s) | +| LOCAL_IP | hbbs | hbbs IP address used in hole-punching | +| MASK | hbbs | network+mask of LAN IPs | +| PORT | hbbs/hbbr | listening port (21116 for hbbs - 21117 for hbbr) | +| RELAY_SERVERS | hbbs | IP address/DNS name of the machines running hbbr (separated by comma) | +| RENDEZVOUS_SERVERS | hbbs | IP address/DNS name of the machines running hbbs (separated by comma) | +| RMEM | hbbs | UDP recv buffer size | +| RUST_LOG | all | set debug level (error|warn|info|debug|trace) | +| SINGLE_BANDWIDTH | hbbr | max bandwidth for a single connection (in Mb/s) | +| SOFTWARE_URL hbbs | hbbs | download url of RustDesk newest version | +| TEST_HBBS | hbbs | IP address of hbbs for avoiding udp socket failure error | +| TOTAL_BANDWIDTH | hbbr | max total bandwidth (in Mb/s) | From f626f82a941ed33717025b1eda2e03b3bf9b90fc Mon Sep 17 00:00:00 2001 From: Paolo Asperti Date: Sat, 26 Nov 2022 00:11:55 +0100 Subject: [PATCH 12/14] test windows build win build action win build action win build action win build action win build action win build action win build action win build action win build action win build action win build action --- .github/workflows/build.yaml | 76 +++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4ef98a9..e37f85b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -44,9 +44,11 @@ jobs: - name: Install toolchain uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: "1.62" override: true default: true + components: rustfmt + profile: minimal target: ${{ matrix.job.target }} - name: Build @@ -56,59 +58,95 @@ jobs: args: --release --all-features --target=${{ matrix.job.target }} use-cross: true - # - name: Run tests - # run: cargo test --verbose - - name: Exec chmod run: chmod -v a+x target/${{ matrix.job.target }}/release/* - name: Publish Artifacts uses: actions/upload-artifact@v3 with: - name: binaries-${{ matrix.job.name }} + name: binaries-linux-${{ matrix.job.name }} path: | target/${{ matrix.job.target }}/release/hbbr target/${{ matrix.job.target }}/release/hbbs target/${{ matrix.job.target }}/release/rustdesk-utils if-no-files-found: error + build-win: + name: Build - windows + runs-on: windows-2019 + + steps: + + - name: Checkout + uses: actions/checkout@v3 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + override: true + default: true + components: rustfmt + profile: minimal + target: x86_64-pc-windows-msvc + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release --all-features --target=x86_64-pc-windows-msvc + use-cross: true + + - name: Publish Artifacts + uses: actions/upload-artifact@v3 + with: + name: binaries-windows-x86_64 + path: | + target\x86_64-pc-windows-msvc\release\hbbr.exe + target\x86_64-pc-windows-msvc\release\hbbs.exe + target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe + if-no-files-found: error + # github (draft) release with all binaries release: name: Github release - needs: build + needs: + - build + - build-win runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: job: - - { name: "amd64" } - - { name: "arm64v8" } - - { name: "armv7" } - - { name: "i386" } + - { os: "linux", name: "amd64" } + - { os: "linux", name: "arm64v8" } + - { os: "linux", name: "armv7" } + - { os: "linux", name: "i386" } + - { os: "windows", name: "x86_64" } steps: - - name: Download binaries (${{ matrix.job.name }}) + - name: Download binaries (${{ matrix.job.os }} - ${{ matrix.job.name }}) uses: actions/download-artifact@v3 with: - name: binaries-${{ matrix.job.name }} + name: binaries-${{ matrix.job.os }}-${{ matrix.job.name }} path: ${{ matrix.job.name }} - name: Exec chmod run: chmod -v a+x ${{ matrix.job.name }}/* - - name: Pack files (${{ matrix.job.name }}) + - name: Pack files (${{ matrix.job.os }} - ${{ matrix.job.name }}) run: | sudo apt update DEBIAN_FRONTEND=noninteractive sudo apt install -y zip - zip ${{ matrix.job.name }}/rustdesk-server-linux-${{ matrix.job.name }}.zip ${{ matrix.job.name }}/hbbr ${{ matrix.job.name }}/hbbs ${{ matrix.job.name }}/rustdesk-utils + zip ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}.zip ${{ matrix.job.name }}/* - - name: Create Release (${{ matrix.job.name }}) + - name: Create Release (${{ matrix.job.os }} - (${{ matrix.job.name }}) uses: softprops/action-gh-release@v1 with: draft: true - files: ${{ matrix.job.name }}/rustdesk-server-linux-${{ matrix.job.name }}.zip + files: ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}.zip # docker build and push of single-arch images docker: @@ -133,7 +171,7 @@ jobs: - name: Download binaries uses: actions/download-artifact@v3 with: - name: binaries-${{ matrix.job.name }} + name: binaries-linux-${{ matrix.job.name }} path: docker/rootfs/usr/bin - name: Make binaries executable @@ -253,7 +291,7 @@ jobs: - name: Download binaries uses: actions/download-artifact@v3 with: - name: binaries-${{ matrix.job.name }} + name: binaries-linux-${{ matrix.job.name }} path: docker-classic/ - name: Make binaries executable @@ -320,7 +358,7 @@ jobs: - name: Download binaries uses: actions/download-artifact@v3 with: - name: binaries-${{ matrix.job.name }} + name: binaries-linux-${{ matrix.job.name }} path: debian-build/${{ matrix.job.name }}/bin - name: Build package for ${{ matrix.job.name }} arch From 2314783d4284a94711e48620e8fd9f315d1154dc Mon Sep 17 00:00:00 2001 From: Huabing Zhou Date: Fri, 6 Jan 2023 10:40:26 +0800 Subject: [PATCH 13/14] sync rustdesk's hbb_common here --- Cargo.lock | 30 ++ libs/hbb_common/Cargo.toml | 7 +- libs/hbb_common/protos/message.proto | 144 ++++++- libs/hbb_common/src/config.rs | 498 +++++++++++++++++++---- libs/hbb_common/src/fs.rs | 338 +++++++++++++-- libs/hbb_common/src/lib.rs | 152 ++++++- libs/hbb_common/src/password_security.rs | 242 +++++++++++ libs/hbb_common/src/platform/linux.rs | 157 +++++++ libs/hbb_common/src/platform/mod.rs | 2 + libs/hbb_common/src/socket_client.rs | 194 +++++++-- libs/hbb_common/src/tcp.rs | 144 ++++--- libs/hbb_common/src/udp.rs | 13 +- src/rendezvous_server.rs | 2 +- src/version.rs | 3 +- 14 files changed, 1692 insertions(+), 234 deletions(-) create mode 100644 libs/hbb_common/src/password_security.rs create mode 100644 libs/hbb_common/src/platform/linux.rs create mode 100644 libs/hbb_common/src/platform/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 9c7bc1d..f63cb48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,9 @@ name = "bytes" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -458,6 +461,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dns-lookup" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872" +dependencies = [ + "cfg-if", + "libc", + "socket2 0.4.4", + "winapi", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -738,6 +753,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "chrono", "confy", "directories-next", "dirs-next", @@ -748,6 +764,7 @@ dependencies = [ "lazy_static", "log", "mac_address", + "machine-uid", "protobuf", "protobuf-codegen", "quinn", @@ -778,6 +795,7 @@ dependencies = [ "chrono", "clap", "deadpool", + "dns-lookup", "flexi_logger", "hbb_common", "headers", @@ -790,6 +808,7 @@ dependencies = [ "machine-uid", "minreq", "once_cell", + "ping", "regex", "rust-ini", "serde", @@ -1440,6 +1459,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ping" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69044d1c00894fc1f43d9485aadb6ab6e68df90608fa52cf1074cda6420c6b76" +dependencies = [ + "rand", + "socket2 0.4.4", + "thiserror", +] + [[package]] name = "pkg-config" version = "0.3.25" diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 4b28fc1..59f0896 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -11,7 +11,7 @@ protobuf = { version = "3.1", features = ["with-bytes"] } tokio = { version = "1.20", features = ["full"] } tokio-util = { version = "0.7", features = ["full"] } futures = "0.3" -bytes = "1.2" +bytes = { version = "1.2", features = ["serde"] } log = "0.4" env_logger = "0.9" socket2 = { version = "0.3", features = ["reuseport"] } @@ -30,15 +30,18 @@ filetime = "0.2" sodiumoxide = "0.2" regex = "1.4" tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } +chrono = "0.4" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" +machine-uid = "0.2" [features] quic = [] +flatpak = [] [build-dependencies] -protobuf-codegen = "3.1" +protobuf-codegen = { version = "3.1" } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["winuser"] } diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 15ee971..b127ac3 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -1,13 +1,13 @@ syntax = "proto3"; package hbb; -message VP9 { +message EncodedVideoFrame { bytes data = 1; bool key = 2; int64 pts = 3; } -message VP9s { repeated VP9 frames = 1; } +message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } message RGB { bool compress = 1; } @@ -19,9 +19,11 @@ message YUV { message VideoFrame { oneof union { - VP9s vp9s = 6; + EncodedVideoFrames vp9s = 6; RGB rgb = 7; YUV yuv = 8; + EncodedVideoFrames h264s = 10; + EncodedVideoFrames h265s = 11; } int64 timestamp = 9; } @@ -38,6 +40,7 @@ message DisplayInfo { int32 height = 4; string name = 5; bool online = 6; + bool cursor_embedded = 7; } message PortForward { @@ -61,10 +64,21 @@ message LoginRequest { PortForward port_forward = 8; } bool video_ack_required = 9; + uint64 session_id = 10; + string version = 11; } message ChatMessage { string text = 1; } +message Features { + bool privacy_mode = 1; +} + +message SupportedEncoding { + bool h264 = 1; + bool h265 = 2; +} + message PeerInfo { string username = 1; string hostname = 2; @@ -74,6 +88,8 @@ message PeerInfo { bool sas_enabled = 6; string version = 7; int32 conn_id = 8; + Features features = 9; + SupportedEncoding encoding = 10; } message LoginResponse { @@ -90,6 +106,13 @@ message MouseEvent { repeated ControlKey modifiers = 4; } +enum KeyboardMode{ + Legacy = 0; + Map = 1; + Translate = 2; + Auto = 3; +} + enum ControlKey { Unknown = 0; Alt = 1; @@ -183,6 +206,7 @@ message KeyEvent { string seq = 6; } repeated ControlKey modifiers = 8; + KeyboardMode mode = 9; } message CursorData { @@ -252,6 +276,7 @@ message FileAction { FileRemoveFile remove_file = 6; ReadAllFiles all_files = 7; FileTransferCancel cancel = 8; + FileTransferSendConfirmRequest send_confirm = 9; } } @@ -263,14 +288,24 @@ message FileResponse { FileTransferBlock block = 2; FileTransferError error = 3; FileTransferDone done = 4; + FileTransferDigest digest = 5; } } +message FileTransferDigest { + int32 id = 1; + sint32 file_num = 2; + uint64 last_modified = 3; + uint64 file_size = 4; + bool is_upload = 5; +} + message FileTransferBlock { int32 id = 1; sint32 file_num = 2; bytes data = 3; bool compressed = 4; + uint32 blk_id = 5; } message FileTransferError { @@ -283,6 +318,16 @@ message FileTransferSendRequest { int32 id = 1; string path = 2; bool include_hidden = 3; + int32 file_num = 4; +} + +message FileTransferSendConfirmRequest { + int32 id = 1; + sint32 file_num = 2; + oneof union { + bool skip = 3; + uint32 offset_blk = 4; + } } message FileTransferDone { @@ -294,6 +339,7 @@ message FileTransferReceiveRequest { int32 id = 1; string path = 2; // path written to repeated FileEntry files = 3; + int32 file_num = 4; } message FileRemoveDir { @@ -315,38 +361,31 @@ message FileDirCreate { // main logic from freeRDP message CliprdrMonitorReady { - int32 conn_id = 1; } message CliprdrFormat { - int32 conn_id = 1; int32 id = 2; string format = 3; } message CliprdrServerFormatList { - int32 conn_id = 1; repeated CliprdrFormat formats = 2; } message CliprdrServerFormatListResponse { - int32 conn_id = 1; int32 msg_flags = 2; } message CliprdrServerFormatDataRequest { - int32 conn_id = 1; int32 requested_format_id = 2; } message CliprdrServerFormatDataResponse { - int32 conn_id = 1; int32 msg_flags = 2; bytes format_data = 3; } message CliprdrFileContentsRequest { - int32 conn_id = 1; int32 stream_id = 2; int32 list_index = 3; int32 dw_flags = 4; @@ -358,7 +397,6 @@ message CliprdrFileContentsRequest { } message CliprdrFileContentsResponse { - int32 conn_id = 1; int32 msg_flags = 3; int32 stream_id = 4; bytes requested_data = 5; @@ -382,6 +420,7 @@ message SwitchDisplay { sint32 y = 3; int32 width = 4; int32 height = 5; + bool cursor_embedded = 6; } message PermissionInfo { @@ -390,6 +429,8 @@ message PermissionInfo { Clipboard = 2; Audio = 3; File = 4; + Restart = 5; + Recording = 6; } Permission permission = 1; @@ -403,6 +444,20 @@ enum ImageQuality { Best = 4; } +message VideoCodecState { + enum PerferCodec { + Auto = 0; + VPX = 1; + H264 = 2; + H265 = 3; + } + + int32 score_vpx = 1; + int32 score_h264 = 2; + int32 score_h265 = 3; + PerferCodec perfer = 4; +} + message OptionMessage { enum BoolOption { NotSet = 0; @@ -418,16 +473,15 @@ message OptionMessage { BoolOption disable_audio = 7; BoolOption disable_clipboard = 8; BoolOption enable_file_transfer = 9; -} - -message OptionResponse { - OptionMessage opt = 1; - string error = 2; + VideoCodecState video_codec_state = 10; + int32 custom_fps = 11; } message TestDelay { int64 time = 1; bool from_client = 2; + uint32 last_delay = 3; + uint32 target_bitrate = 4; } message PublicKey { @@ -447,6 +501,57 @@ message AudioFrame { int64 timestamp = 2; } +// Notify peer to show message box. +message MessageBox { + // Message type. Refer to flutter/lib/commom.dart/msgBox(). + string msgtype = 1; + string title = 2; + // English + string text = 3; + // If not empty, msgbox provides a button to following the link. + // The link here can't be directly http url. + // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). + string link = 4; +} + +message BackNotification { + // no need to consider block input by someone else + enum BlockInputState { + BlkStateUnknown = 0; + BlkOnSucceeded = 2; + BlkOnFailed = 3; + BlkOffSucceeded = 4; + BlkOffFailed = 5; + } + enum PrivacyModeState { + PrvStateUnknown = 0; + // Privacy mode on by someone else + PrvOnByOther = 2; + // Privacy mode is not supported on the remote side + PrvNotSupported = 3; + // Privacy mode on by self + PrvOnSucceeded = 4; + // Privacy mode on by self, but denied + PrvOnFailedDenied = 5; + // Some plugins are not found + PrvOnFailedPlugin = 6; + // Privacy mode on by self, but failed + PrvOnFailed = 7; + // Privacy mode off by self + PrvOffSucceeded = 8; + // Ctrl + P + PrvOffByPeer = 9; + // Privacy mode off by self, but failed + PrvOffFailed = 10; + PrvOffUnknown = 11; + } + + oneof union { + PrivacyModeState privacy_mode_state = 1; + BlockInputState block_input_state = 2; + } +} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -456,8 +561,12 @@ message Misc { AudioFormat audio_format = 8; string close_reason = 9; bool refresh_video = 10; - OptionResponse option_response = 11; bool video_received = 12; + BackNotification back_notification = 13; + bool restart_remote_device = 14; + bool uac = 15; + bool foreground_window_elevated = 16; + bool stop_service = 17; } } @@ -481,5 +590,6 @@ message Message { FileResponse file_response = 18; Misc misc = 19; Cliprdr cliprdr = 20; + MessageBox message_box = 21; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index c3a502b..1d427a2 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,22 +1,35 @@ -use crate::log; -use directories_next::ProjectDirs; -use rand::Rng; -use serde_derive::{Deserialize, Serialize}; -use sodiumoxide::crypto::sign; use std::{ collections::HashMap, fs, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock}, time::SystemTime, }; +use anyhow::Result; +use rand::Rng; +use regex::Regex; +use serde as de; +use serde_derive::{Deserialize, Serialize}; +use sodiumoxide::base64; +use sodiumoxide::crypto::sign; + +use crate::{ + log, + password_security::{ + decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, + encrypt_vec_or_original, + }, +}; + pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; pub const CONNECT_TIMEOUT: u64 = 18_000; +pub const READ_TIMEOUT: u64 = 30_000; pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; -const SERIAL: i32 = 1; +const SERIAL: i32 = 3; +const PASSWORD_ENC_VERSION: &'static str = "00"; // 128x128 #[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII= @@ -38,12 +51,27 @@ lazy_static::lazy_static! { pub static ref ONLINE: Arc>> = Default::default(); pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); + static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); + static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); } -#[cfg(any(target_os = "android", target_os = "ios"))] + lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { pub static ref APP_HOME_DIR: Arc> = Default::default(); } + +// #[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ + ("rustdesk docs home", "https://rustdesk.com/docs/en/"), + ("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ]); +} + const CHARS: &'static [char] = &[ '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', @@ -54,9 +82,30 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; +pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; +macro_rules! serde_field_string { + ($default_func:ident, $de_func:ident, $default_expr:expr) => { + fn $default_func() -> String { + $default_expr + } + + fn $de_func<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s: &str = de::Deserialize::deserialize(deserializer)?; + Ok(if s.is_empty() { + Self::$default_func() + } else { + s.to_owned() + }) + } + }; +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum NetworkType { Direct, @@ -66,13 +115,15 @@ pub enum NetworkType { #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct Config { #[serde(default)] - pub id: String, + pub id: String, // use + #[serde(default)] + enc_id: String, // store #[serde(default)] password: String, #[serde(default)] salt: String, #[serde(default)] - pub key_pair: (Vec, Vec), // sk, pk + key_pair: (Vec, Vec), // sk, pk #[serde(default)] key_confirmed: bool, #[serde(default)] @@ -107,7 +158,7 @@ pub struct Config2 { pub options: HashMap, } -#[derive(Debug, Default, Serialize, Deserialize, Clone)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct PeerConfig { #[serde(default)] pub password: Vec, @@ -117,9 +168,20 @@ pub struct PeerConfig { pub size_ft: Size, #[serde(default)] pub size_pf: Size, - #[serde(default)] - pub view_style: String, // original (default), scale - #[serde(default)] + #[serde( + default = "PeerConfig::default_view_style", + deserialize_with = "PeerConfig::deserialize_view_style" + )] + pub view_style: String, + #[serde( + default = "PeerConfig::default_scroll_style", + deserialize_with = "PeerConfig::deserialize_scroll_style" + )] + pub scroll_style: String, + #[serde( + default = "PeerConfig::default_image_quality", + deserialize_with = "PeerConfig::deserialize_image_quality" + )] pub image_quality: String, #[serde(default)] pub custom_image_quality: Vec, @@ -139,12 +201,21 @@ pub struct PeerConfig { pub disable_clipboard: bool, #[serde(default)] pub enable_file_transfer: bool, - - // the other scalar value must before this #[serde(default)] + pub show_quality_monitor: bool, + #[serde(default)] + pub keyboard_mode: String, + + // The other scalar value must before this + #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] pub options: HashMap, + // Various data for flutter ui + #[serde(default)] + pub ui_flutter: HashMap, #[serde(default)] pub info: PeerInfoSerde, + #[serde(default)] + pub transfer: TransferSerde, } #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] @@ -157,6 +228,14 @@ pub struct PeerInfoSerde { pub platform: String, } +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct TransferSerde { + #[serde(default)] + pub write_jobs: Vec, + #[serde(default)] + pub read_jobs: Vec, +} + fn patch(path: PathBuf) -> PathBuf { if let Some(_tmp) = path.to_str() { #[cfg(windows)] @@ -188,7 +267,17 @@ fn patch(path: PathBuf) -> PathBuf { impl Config2 { fn load() -> Config2 { - Config::load_::("2") + let mut config = Config::load_::("2"); + if let Some(mut socks) = config.socks { + let (password, _, store) = + decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); + socks.password = password; + config.socks = Some(socks); + if store { + config.store(); + } + } + config } pub fn file() -> PathBuf { @@ -196,7 +285,12 @@ impl Config2 { } fn store(&self) { - Config::store_(self, "2"); + let mut config = self.clone(); + if let Some(mut socks) = config.socks { + socks.password = encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); + config.socks = Some(socks); + } + Config::store_(&config, "2"); } pub fn get() -> Config2 { @@ -227,6 +321,11 @@ pub fn load_path(path: PathBuf, cfg: T) -> crate::ResultType<()> { + Ok(confy::store_path(path, cfg)?) +} + impl Config { fn load_( suffix: &str, @@ -235,24 +334,68 @@ impl Config { log::debug!("Configuration path: {}", file.display()); let cfg = load_path(file); if suffix.is_empty() { - log::debug!("{:?}", cfg); + log::trace!("{:?}", cfg); } cfg } fn store_(config: &T, suffix: &str) { let file = Self::file_(suffix); - if let Err(err) = confy::store_path(file, config) { + if let Err(err) = store_path(file, config) { log::error!("Failed to store config: {}", err); } } fn load() -> Config { - Config::load_::("") + let mut config = Config::load_::(""); + let mut store = false; + let (password, _, store1) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); + config.password = password; + store |= store1; + let mut id_valid = false; + let (id, encrypted, store2) = decrypt_str_or_original(&config.enc_id, PASSWORD_ENC_VERSION); + if encrypted { + config.id = id; + id_valid = true; + store |= store2; + } else { + if crate::get_modified_time(&Self::file_("")) + .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation + .unwrap_or(crate::get_exe_time()) + < crate::get_exe_time() + { + if !config.id.is_empty() + && config.enc_id.is_empty() + && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 + { + id_valid = true; + store = true; + } + } + } + if !id_valid { + for _ in 0..3 { + if let Some(id) = Config::get_auto_id() { + config.id = id; + store = true; + break; + } else { + log::error!("Failed to generate new id"); + } + } + } + if store { + config.store(); + } + config } fn store(&self) { - Config::store_(self, ""); + let mut config = self.clone(); + config.password = encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); + config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION); + config.id = "".to_owned(); + Config::store_(&config, ""); } pub fn file() -> PathBuf { @@ -264,15 +407,22 @@ impl Config { Config::with_extension(Self::path(name)) } + pub fn is_empty(&self) -> bool { + (self.id.is_empty() && self.enc_id.is_empty()) || self.key_pair.0.is_empty() + } + pub fn get_home() -> PathBuf { #[cfg(any(target_os = "android", target_os = "ios"))] return Self::path(APP_HOME_DIR.read().unwrap().as_str()); - if let Some(path) = dirs_next::home_dir() { - patch(path) - } else if let Ok(path) = std::env::current_dir() { - path - } else { - std::env::temp_dir() + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if let Some(path) = dirs_next::home_dir() { + patch(path) + } else if let Ok(path) = std::env::current_dir() { + path + } else { + std::env::temp_dir() + } } } @@ -283,17 +433,22 @@ impl Config { path.push(p); return path; } - #[cfg(not(target_os = "macos"))] - let org = ""; - #[cfg(target_os = "macos")] - let org = ORG.read().unwrap().clone(); - // /var/root for root - if let Some(project) = ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) { - let mut path = patch(project.config_dir().to_path_buf()); - path.push(p); - return path; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + #[cfg(not(target_os = "macos"))] + let org = ""; + #[cfg(target_os = "macos")] + let org = ORG.read().unwrap().clone(); + // /var/root for root + if let Some(project) = + directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) + { + let mut path = patch(project.config_dir().to_path_buf()); + path.push(p); + return path; + } + return "".into(); } - return "".into(); } #[allow(unreachable_code)] @@ -356,8 +511,12 @@ impl Config { } #[inline] - pub fn get_any_listen_addr() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) + pub fn get_any_listen_addr(is_ipv4: bool) -> SocketAddr { + if is_ipv4 { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } else { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) + } } pub fn get_rendezvous_server() -> String { @@ -472,22 +631,25 @@ impl Config { .to_string(), ); } - let mut id = 0u32; + #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Ok(Some(ma)) = mac_address::get_mac_address() { - for x in &ma.bytes()[2..] { - id = (id << 8) | (*x as u32); + { + let mut id = 0u32; + if let Ok(Some(ma)) = mac_address::get_mac_address() { + for x in &ma.bytes()[2..] { + id = (id << 8) | (*x as u32); + } + id = id & 0x1FFFFFFF; + Some(id.to_string()) + } else { + None } - id = id & 0x1FFFFFFF; - Some(id.to_string()) - } else { - None } } - pub fn get_auto_password() -> String { + pub fn get_auto_password(length: usize) -> String { let mut rng = rand::thread_rng(); - (0..6) + (0..length) .map(|_| CHARS[rng.gen::() % CHARS.len()]) .collect() } @@ -525,24 +687,26 @@ impl Config { config.store(); } - pub fn set_key_pair(pair: (Vec, Vec)) { - let mut config = CONFIG.write().unwrap(); - if config.key_pair == pair { - return; - } - config.key_pair = pair; - config.store(); - } - pub fn get_key_pair() -> (Vec, Vec) { // lock here to make sure no gen_keypair more than once - let mut config = CONFIG.write().unwrap(); + // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function + let mut lock = KEY_PAIR.lock().unwrap(); + if let Some(p) = lock.as_ref() { + return p.clone(); + } + let mut config = Config::load_::(""); if config.key_pair.0.is_empty() { let (pk, sk) = sign::gen_keypair(); - config.key_pair = (sk.0.to_vec(), pk.0.into()); - config.store(); + let key_pair = (sk.0.to_vec(), pk.0.into()); + config.key_pair = key_pair.clone(); + std::thread::spawn(|| { + let mut config = CONFIG.write().unwrap(); + config.key_pair = key_pair; + config.store(); + }); } - config.key_pair.clone() + *lock = Some(config.key_pair.clone()); + return config.key_pair; } pub fn get_id() -> String { @@ -608,7 +772,7 @@ impl Config { log::info!("id updated from {} to {}", id, new_id); } - pub fn set_password(password: &str) { + pub fn set_permanent_password(password: &str) { let mut config = CONFIG.write().unwrap(); if password == config.password { return; @@ -617,13 +781,8 @@ impl Config { config.store(); } - pub fn get_password() -> String { - let mut password = CONFIG.read().unwrap().password.clone(); - if password.is_empty() { - password = Config::get_auto_password(); - Config::set_password(&password); - } - password + pub fn get_permanent_password() -> String { + CONFIG.read().unwrap().password.clone() } pub fn set_salt(salt: &str) { @@ -638,7 +797,7 @@ impl Config { pub fn get_salt() -> String { let mut salt = CONFIG.read().unwrap().salt.clone(); if salt.is_empty() { - salt = Config::get_auto_password(); + salt = Config::get_auto_password(6); Config::set_salt(&salt); } salt @@ -693,9 +852,30 @@ const PEERS: &str = "peers"; impl PeerConfig { pub fn load(id: &str) -> PeerConfig { - let _unused = CONFIG.read().unwrap(); // for lock + let _lock = CONFIG.read().unwrap(); match confy::load_path(&Self::path(id)) { - Ok(config) => config, + Ok(config) => { + let mut config: PeerConfig = config; + let mut store = false; + let (password, _, store2) = + decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); + config.password = password; + store = store || store2; + config.options.get_mut("rdp_password").map(|v| { + let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); + *v = password; + store = store || store2; + }); + config.options.get_mut("os-password").map(|v| { + let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); + *v = password; + store = store || store2; + }); + if store { + config.store(id); + } + config + } Err(err) => { log::error!("Failed to load config: {}", err); Default::default() @@ -704,8 +884,18 @@ impl PeerConfig { } pub fn store(&self, id: &str) { - let _unused = CONFIG.read().unwrap(); // for lock - if let Err(err) = confy::store_path(Self::path(id), self) { + let _lock = CONFIG.read().unwrap(); + let mut config = self.clone(); + config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); + config + .options + .get_mut("rdp_password") + .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); + config + .options + .get_mut("os-password") + .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); + if let Err(err) = store_path(Self::path(id), config) { log::error!("Failed to store config: {}", err); } } @@ -715,7 +905,17 @@ impl PeerConfig { } fn path(id: &str) -> PathBuf { - let path: PathBuf = [PEERS, id].iter().collect(); + let id_encoded: String; + + //If the id contains invalid chars, encode it + let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap(); + if forbidden_paths.is_match(id) { + id_encoded = + "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str(); + } else { + id_encoded = id.to_string(); + } + let path: PathBuf = [PEERS, id_encoded.as_str()].iter().collect(); Config::with_extension(Config::path(path)) } @@ -738,11 +938,22 @@ impl PeerConfig { .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") .to_owned(); - let c = PeerConfig::load(&id); + + let id_decoded_string: String; + if id.starts_with("base64_") && id.len() != 7 { + let id_decoded = base64::decode(&id[7..], base64::Variant::Original) + .unwrap_or(Vec::new()); + id_decoded_string = + String::from_utf8_lossy(&id_decoded).as_ref().to_owned(); + } else { + id_decoded_string = id; + } + + let c = PeerConfig::load(&id_decoded_string); if c.info.platform.is_empty() { fs::remove_file(&p).ok(); } - (id, t, c) + (id_decoded_string, t, c) }) .filter(|p| !p.2.info.platform.is_empty()) .collect(); @@ -752,6 +963,33 @@ impl PeerConfig { } Default::default() } + + serde_field_string!( + default_view_style, + deserialize_view_style, + "original".to_owned() + ); + serde_field_string!( + default_scroll_style, + deserialize_scroll_style, + "scrollauto".to_owned() + ); + serde_field_string!( + default_image_quality, + deserialize_image_quality, + "balanced".to_owned() + ); + + fn deserialize_options<'de, D>(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let mut mp: HashMap = de::Deserialize::deserialize(deserializer)?; + if !mp.contains_key("codec-preference") { + mp.insert("codec-preference".to_owned(), "auto".to_owned()); + } + Ok(mp) + } } #[derive(Debug, Default, Serialize, Deserialize, Clone)] @@ -759,11 +997,16 @@ pub struct LocalConfig { #[serde(default)] remote_id: String, // latest used one #[serde(default)] + kb_layout_type: String, + #[serde(default)] size: Size, #[serde(default)] pub fav: Vec, #[serde(default)] options: HashMap, + // Various data for flutter ui + #[serde(default)] + ui_flutter: HashMap, } impl LocalConfig { @@ -775,6 +1018,16 @@ impl LocalConfig { Config::store_(self, "_local"); } + pub fn get_kb_layout_type() -> String { + LOCAL_CONFIG.read().unwrap().kb_layout_type.clone() + } + + pub fn set_kb_layout_type(kb_layout_type: String) { + let mut config = LOCAL_CONFIG.write().unwrap(); + config.kb_layout_type = kb_layout_type; + config.store(); + } + pub fn get_size() -> Size { LOCAL_CONFIG.read().unwrap().size } @@ -835,17 +1088,59 @@ impl LocalConfig { config.store(); } } + + pub fn get_flutter_config(k: &str) -> String { + if let Some(v) = LOCAL_CONFIG.read().unwrap().ui_flutter.get(k) { + v.clone() + } else { + "".to_owned() + } + } + + pub fn set_flutter_config(k: String, v: String) { + let mut config = LOCAL_CONFIG.write().unwrap(); + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.ui_flutter.get(&k) { + if v2.is_none() { + config.ui_flutter.remove(&k); + } else { + config.ui_flutter.insert(k, v); + } + config.store(); + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct DiscoveryPeer { + #[serde(default)] + pub id: String, + #[serde(default)] + pub username: String, + #[serde(default)] + pub hostname: String, + #[serde(default)] + pub platform: String, + #[serde(default)] + pub online: bool, + #[serde(default)] + pub ip_mac: HashMap, +} + +impl DiscoveryPeer { + pub fn is_same_peer(&self, other: &DiscoveryPeer) -> bool { + self.id == other.id && self.username == other.username + } } #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LanPeers { - #[serde(default)] - pub peers: String, + pub peers: Vec, } impl LanPeers { pub fn load() -> LanPeers { - let _unused = CONFIG.read().unwrap(); // for lock + let _lock = CONFIG.read().unwrap(); match confy::load_path(&Config::file_("_lan_peers")) { Ok(peers) => peers, Err(err) => { @@ -855,9 +1150,11 @@ impl LanPeers { } } - pub fn store(peers: String) { - let f = LanPeers { peers }; - if let Err(err) = confy::store_path(Config::file_("_lan_peers"), f) { + pub fn store(peers: &Vec) { + let f = LanPeers { + peers: peers.clone(), + }; + if let Err(err) = store_path(Config::file_("_lan_peers"), f) { log::error!("Failed to store lan peers: {}", err); } } @@ -871,9 +1168,40 @@ impl LanPeers { } } +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct HwCodecConfig { + #[serde(default)] + pub options: HashMap, +} + +impl HwCodecConfig { + pub fn load() -> HwCodecConfig { + Config::load_::("_hwcodec") + } + + pub fn store(&self) { + Config::store_(self, "_hwcodec"); + } + + pub fn remove() { + std::fs::remove_file(Config::file_("_hwcodec")).ok(); + } + + /// refresh current global HW_CODEC_CONFIG, usually uesd after HwCodecConfig::remove() + pub fn refresh() { + *HW_CODEC_CONFIG.write().unwrap() = HwCodecConfig::load(); + log::debug!("HW_CODEC_CONFIG refreshed successfully"); + } + + pub fn get() -> HwCodecConfig { + return HW_CODEC_CONFIG.read().unwrap().clone(); + } +} + #[cfg(test)] mod tests { use super::*; + #[test] fn test_serialize() { let cfg: Config = Default::default(); diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 69cd348..fec8b86 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -1,13 +1,17 @@ -use crate::{bail, message_proto::*, ResultType}; +#[cfg(windows)] +use std::os::windows::prelude::*; use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use serde_derive::{Deserialize, Serialize}; +use tokio::{fs::File, io::*}; + +use crate::{bail, get_version_number, message_proto::*, ResultType, Stream}; // https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html use crate::{ compress::{compress, decompress}, config::{Config, COMPRESS_LEVEL}, }; -#[cfg(windows)] -use std::os::windows::prelude::*; -use tokio::{fs::File, io::*}; pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType { let mut dir = FileDirectory { @@ -184,16 +188,63 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType bool { + return Path::new(file_path).exists(); +} + +#[inline] +pub fn can_enable_overwrite_detection(version: i64) -> bool { + version >= get_version_number("1.1.10") +} + #[derive(Default)] pub struct TransferJob { - id: i32, - path: PathBuf, - files: Vec, - file_num: i32, + pub id: i32, + pub remote: String, + pub path: PathBuf, + pub show_hidden: bool, + pub is_remote: bool, + pub is_last_job: bool, + pub file_num: i32, + pub files: Vec, + file: Option, total_size: u64, finished_size: u64, transferred: u64, + enable_overwrite_detection: bool, + file_confirmed: bool, + // indicating the last file is skipped + file_skipped: bool, + file_is_waiting: bool, + default_overwrite_strategy: Option, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct TransferJobMeta { + #[serde(default)] + pub id: i32, + #[serde(default)] + pub remote: String, + #[serde(default)] + pub to: String, + #[serde(default)] + pub show_hidden: bool, + #[serde(default)] + pub file_num: i32, + #[serde(default)] + pub is_remote: bool, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct RemoveJobMeta { + #[serde(default)] + pub path: String, + #[serde(default)] + pub is_remote: bool, + #[serde(default)] + pub no_confirm: bool, } #[inline] @@ -219,25 +270,54 @@ fn is_compressed_file(name: &str) -> bool { } impl TransferJob { - pub fn new_write(id: i32, path: String, files: Vec) -> Self { + pub fn new_write( + id: i32, + remote: String, + path: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + files: Vec, + enable_overwrite_detection: bool, + ) -> Self { + log::info!("new write {}", path); let total_size = files.iter().map(|x| x.size as u64).sum(); Self { id, + remote, path: get_path(&path), + file_num, + show_hidden, + is_remote, files, total_size, + enable_overwrite_detection, ..Default::default() } } - pub fn new_read(id: i32, path: String, include_hidden: bool) -> ResultType { - let files = get_recursive_files(&path, include_hidden)?; + pub fn new_read( + id: i32, + remote: String, + path: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + enable_overwrite_detection: bool, + ) -> ResultType { + log::info!("new read {}", path); + let files = get_recursive_files(&path, show_hidden)?; let total_size = files.iter().map(|x| x.size as u64).sum(); Ok(Self { id, + remote, path: get_path(&path), + file_num, + show_hidden, + is_remote, files, total_size, + enable_overwrite_detection, ..Default::default() }) } @@ -302,7 +382,7 @@ impl TransferJob { } } - pub async fn write(&mut self, block: FileTransferBlock, raw: Option<&[u8]>) -> ResultType<()> { + pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { if block.id != self.id { bail!("Wrong id"); } @@ -324,25 +404,20 @@ impl TransferJob { let path = format!("{}.download", get_string(&path)); self.file = Some(File::create(&path).await?); } - let data = if let Some(data) = raw { - data - } else { - &block.data - }; if block.compressed { - let tmp = decompress(data); + let tmp = decompress(&block.data); self.file.as_mut().unwrap().write_all(&tmp).await?; self.finished_size += tmp.len() as u64; } else { - self.file.as_mut().unwrap().write_all(data).await?; - self.finished_size += data.len() as u64; + self.file.as_mut().unwrap().write_all(&block.data).await?; + self.finished_size += block.data.len() as u64; } - self.transferred += data.len() as u64; + self.transferred += block.data.len() as u64; Ok(()) } #[inline] - fn join(&self, name: &str) -> PathBuf { + pub fn join(&self, name: &str) -> PathBuf { if name.is_empty() { self.path.clone() } else { @@ -350,7 +425,7 @@ impl TransferJob { } } - pub async fn read(&mut self) -> ResultType> { + pub async fn read(&mut self, stream: &mut Stream) -> ResultType> { let file_num = self.file_num as usize; if file_num >= self.files.len() { self.file.take(); @@ -361,13 +436,26 @@ impl TransferJob { match File::open(self.join(&name)).await { Ok(file) => { self.file = Some(file); + self.file_confirmed = false; + self.file_is_waiting = false; } Err(err) => { self.file_num += 1; + self.file_confirmed = false; + self.file_is_waiting = false; return Err(err.into()); } } } + if self.enable_overwrite_detection { + if !self.file_confirmed() { + if !self.file_is_waiting() { + self.send_current_digest(stream).await?; + self.set_file_is_waiting(true); + } + return Ok(None); + } + } const BUF_SIZE: usize = 128 * 1024; let mut buf: Vec = Vec::with_capacity(BUF_SIZE); unsafe { @@ -380,6 +468,8 @@ impl TransferJob { Err(err) => { self.file_num += 1; self.file = None; + self.file_confirmed = false; + self.file_is_waiting = false; return Err(err.into()); } Ok(n) => { @@ -394,6 +484,8 @@ impl TransferJob { if offset == 0 { self.file_num += 1; self.file = None; + self.file_confirmed = false; + self.file_is_waiting = false; } else { self.finished_size += offset as u64; if !is_compressed_file(name) { @@ -413,6 +505,139 @@ impl TransferJob { ..Default::default() })) } + + async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> { + let mut msg = Message::new(); + let mut resp = FileResponse::new(); + let meta = self.file.as_ref().unwrap().metadata().await?; + let last_modified = meta + .modified()? + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(); + resp.set_digest(FileTransferDigest { + id: self.id, + file_num: self.file_num, + last_modified, + file_size: meta.len(), + ..Default::default() + }); + msg.set_file_response(resp); + stream.send(&msg).await?; + log::info!( + "id: {}, file_num:{}, digest message is sent. waiting for confirm. msg: {:?}", + self.id, + self.file_num, + msg + ); + Ok(()) + } + + pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option) { + self.default_overwrite_strategy = overwrite_strategy; + } + + pub fn default_overwrite_strategy(&self) -> Option { + self.default_overwrite_strategy + } + + pub fn set_file_confirmed(&mut self, file_confirmed: bool) { + log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed); + self.file_confirmed = file_confirmed; + self.file_skipped = false; + } + + pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) { + self.file_is_waiting = file_is_waiting; + } + + #[inline] + pub fn file_is_waiting(&self) -> bool { + self.file_is_waiting + } + + #[inline] + pub fn file_confirmed(&self) -> bool { + self.file_confirmed + } + + /// Indicating whether the last file is skipped + #[inline] + pub fn file_skipped(&self) -> bool { + self.file_skipped + } + + /// Indicating whether the whole task is skipped + #[inline] + pub fn job_skipped(&self) -> bool { + self.file_skipped() && self.files.len() == 1 + } + + /// Check whether the job is completed after `read` returns `None` + /// This is a helper function which gives additional lifecycle when the job reads `None`. + /// If returns `true`, it means we can delete the job automatically. `False` otherwise. + /// + /// [`Note`] + /// Conditions: + /// 1. Files are not waiting for confirmation by peers. + #[inline] + pub fn job_completed(&self) -> bool { + // has no error, Condition 2 + if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) { + return true; + } + return false; + } + + /// Get job error message, useful for getting status when job had finished + pub fn job_error(&self) -> Option { + if self.job_skipped() { + return Some("skipped".to_string()); + } + None + } + + pub fn set_file_skipped(&mut self) -> bool { + log::debug!("skip file {} in job {}", self.file_num, self.id); + self.file.take(); + self.set_file_confirmed(false); + self.set_file_is_waiting(false); + self.file_num += 1; + self.file_skipped = true; + true + } + + pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool { + if self.file_num() != r.file_num { + log::info!("file num truncated, ignoring"); + } else { + match r.union { + Some(file_transfer_send_confirm_request::Union::Skip(s)) => { + if s { + self.set_file_skipped(); + } else { + self.set_file_confirmed(true); + } + } + Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => { + self.set_file_confirmed(true); + } + _ => {} + } + } + true + } + + #[inline] + pub fn gen_meta(&self) -> TransferJobMeta { + TransferJobMeta { + id: self.id, + remote: self.remote.to_string(), + to: self.path.to_string_lossy().to_string(), + file_num: self.file_num, + show_hidden: self.show_hidden, + is_remote: self.is_remote, + } + } } #[inline] @@ -453,12 +678,22 @@ pub fn new_block(block: FileTransferBlock) -> Message { } #[inline] -pub fn new_receive(id: i32, path: String, files: Vec) -> Message { +pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message { + let mut msg_out = Message::new(); + let mut action = FileAction::new(); + action.set_send_confirm(r); + msg_out.set_file_action(action); + msg_out +} + +#[inline] +pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec) -> Message { let mut action = FileAction::new(); action.set_receive(FileTransferReceiveRequest { id, path, files: files.into(), + file_num, ..Default::default() }); let mut msg_out = Message::new(); @@ -467,12 +702,14 @@ pub fn new_receive(id: i32, path: String, files: Vec) -> Message { } #[inline] -pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message { +pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message { + log::info!("new send: {},id : {}", path, id); let mut action = FileAction::new(); action.set_send(FileTransferSendRequest { id, path, include_hidden, + file_num, ..Default::default() }); let mut msg_out = Message::new(); @@ -509,7 +746,10 @@ pub async fn handle_read_jobs( ) -> ResultType<()> { let mut finished = Vec::new(); for job in jobs.iter_mut() { - match job.read().await { + if job.is_last_job { + continue; + } + match job.read(stream).await { Err(err) => { stream .send(&new_error(job.id(), err, job.file_num())) @@ -519,8 +759,19 @@ pub async fn handle_read_jobs( stream.send(&new_block(block)).await?; } Ok(None) => { - finished.push(job.id()); - stream.send(&new_done(job.id(), job.file_num())).await?; + if job.job_completed() { + finished.push(job.id()); + let err = job.job_error(); + if err.is_some() { + stream + .send(&new_error(job.id(), err.unwrap(), job.file_num())) + .await?; + } else { + stream.send(&new_done(job.id(), job.file_num())).await?; + } + } else { + // waiting confirmation. + } } } } @@ -566,3 +817,34 @@ pub fn transform_windows_path(entries: &mut Vec) { } } +pub enum DigestCheckResult { + IsSame, + NeedConfirm(FileTransferDigest), + NoSuchFile, +} + +#[inline] +pub fn is_write_need_confirmation( + file_path: &str, + digest: &FileTransferDigest, +) -> ResultType { + let path = Path::new(file_path); + if path.exists() && path.is_file() { + let metadata = std::fs::metadata(path)?; + let modified_time = metadata.modified()?; + let remote_mt = Duration::from_secs(digest.last_modified); + let local_mt = modified_time.duration_since(UNIX_EPOCH)?; + if remote_mt == local_mt && digest.file_size == metadata.len() { + return Ok(DigestCheckResult::IsSame); + } + Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { + id: digest.id, + file_num: digest.file_num, + last_modified: local_mt.as_secs(), + file_size: metadata.len(), + ..Default::default() + })) + } else { + Ok(DigestCheckResult::NoSuchFile) + } +} diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 5f23e46..85e0100 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -1,15 +1,16 @@ pub mod compress; -#[path = "./protos/message.rs"] -pub mod message_proto; -#[path = "./protos/rendezvous.rs"] -pub mod rendezvous_proto; +pub mod platform; +pub mod protos; pub use bytes; +use config::Config; pub use futures; pub use protobuf; +pub use protos::message as message_proto; +pub use protos::rendezvous as rendezvous_proto; use std::{ fs::File, io::{self, BufRead}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, path::Path, time::{self, SystemTime, UNIX_EPOCH}, }; @@ -27,6 +28,7 @@ pub use anyhow::{self, bail}; pub use futures_util; pub mod config; pub mod fs; +pub use lazy_static; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use mac_address; pub use rand; @@ -35,6 +37,9 @@ pub use sodiumoxide; pub use tokio_socks; pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::TargetAddr; +pub mod password_security; +pub use chrono; +pub use directories_next; #[cfg(feature = "quic")] pub type Stream = quic::Connection; @@ -61,6 +66,21 @@ macro_rules! allow_err { } else { } }; + + ($e:expr, $($arg:tt)*) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}, {}:{}:{}:{}", + err, + format_args!($($arg)*), + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; } #[inline] @@ -97,13 +117,31 @@ impl AddrMangle { } bytes[..(16 - n_padding)].to_vec() } - _ => { - panic!("Only support ipv4"); + SocketAddr::V6(addr_v6) => { + let mut x = addr_v6.ip().octets().to_vec(); + let port: [u8; 2] = addr_v6.port().to_le_bytes(); + x.push(port[0]); + x.push(port[1]); + x } } } pub fn decode(bytes: &[u8]) -> SocketAddr { + if bytes.len() > 16 { + if bytes.len() != 18 { + return Config::get_any_listen_addr(false); + } + #[allow(invalid_value)] + let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + tmp.copy_from_slice(&bytes[16..]); + let port = u16::from_le_bytes(tmp); + #[allow(invalid_value)] + let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + tmp.copy_from_slice(&bytes[..16]); + let ip = std::net::Ipv6Addr::from(tmp); + return SocketAddr::new(IpAddr::V6(ip), port); + } let mut padded = [0u8; 16]; padded[..bytes.len()].copy_from_slice(&bytes); let number = u128::from_le_bytes(padded); @@ -156,19 +194,23 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { + use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); for line in read_lines("Cargo.toml").unwrap() { if let Ok(line) = line { let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect(); if ab.len() == 2 && ab[0] == "version" { - use std::io::prelude::*; - file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes()) + file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) .ok(); - file.sync_all().ok(); break; } } } + // generate build date + let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); + file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes()) + .ok(); + file.sync_all().ok(); } fn read_lines

(filename: P) -> io::Result>> @@ -199,6 +241,40 @@ pub fn get_modified_time(path: &std::path::Path) -> SystemTime { .unwrap_or(UNIX_EPOCH) } +pub fn get_created_time(path: &std::path::Path) -> SystemTime { + std::fs::metadata(&path) + .map(|m| m.created().unwrap_or(UNIX_EPOCH)) + .unwrap_or(UNIX_EPOCH) +} + +pub fn get_exe_time() -> SystemTime { + std::env::current_exe().map_or(UNIX_EPOCH, |path| { + let m = get_modified_time(&path); + let c = get_created_time(&path); + if m > c { + m + } else { + c + } + }) +} + +pub fn get_uuid() -> Vec { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(id) = machine_uid::get() { + return id.into(); + } + Config::get_key_pair().1 +} + +#[inline] +pub fn get_time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) as _ +} + #[cfg(test)] mod tests { use super::*; @@ -206,5 +282,61 @@ mod tests { fn test_mangle() { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8::1]:8080".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + } + + #[test] + fn test_allow_err() { + allow_err!(Err("test err") as Result<(), &str>); + allow_err!( + Err("test err with msg") as Result<(), &str>, + "prompt {}", + "failed" + ); + } +} + +#[inline] +pub fn is_ipv4_str(id: &str) -> bool { + regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ipv6_str(id: &str) -> bool { + regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ip_str(id: &str) -> bool { + is_ipv4_str(id) || is_ipv6_str(id) +} + +#[cfg(test)] +mod test_lib { + use super::*; + + #[test] + fn test_ipv6() { + assert_eq!(is_ipv6_str("1:2:3"), true); + assert_eq!(is_ipv6_str("[ab:2:3]:12"), true); + assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true); + assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false); + assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false); + assert_eq!(is_ipv6_str("1.1.1.1"), false); + assert_eq!(is_ip_str("1.1.1.1"), true); + assert_eq!(is_ipv6_str("1:2:"), false); + assert_eq!(is_ipv6_str("1:2::0"), true); + assert_eq!(is_ipv6_str("[1:2::0]:1"), true); + assert_eq!(is_ipv6_str("[1:2::0]:"), false); + assert_eq!(is_ipv6_str("1:2::0]:1"), false); } } diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs new file mode 100644 index 0000000..6029069 --- /dev/null +++ b/libs/hbb_common/src/password_security.rs @@ -0,0 +1,242 @@ +use crate::config::Config; +use sodiumoxide::base64; +use std::sync::{Arc, RwLock}; + +lazy_static::lazy_static! { + pub static ref TEMPORARY_PASSWORD:Arc> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length()))); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum VerificationMethod { + OnlyUseTemporaryPassword, + OnlyUsePermanentPassword, + UseBothPasswords, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApproveMode { + Both, + Password, + Click, +} + +// Should only be called in server +pub fn update_temporary_password() { + *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length()); +} + +// Should only be called in server +pub fn temporary_password() -> String { + TEMPORARY_PASSWORD.read().unwrap().clone() +} + +fn verification_method() -> VerificationMethod { + let method = Config::get_option("verification-method"); + if method == "use-temporary-password" { + VerificationMethod::OnlyUseTemporaryPassword + } else if method == "use-permanent-password" { + VerificationMethod::OnlyUsePermanentPassword + } else { + VerificationMethod::UseBothPasswords // default + } +} + +pub fn temporary_password_length() -> usize { + let length = Config::get_option("temporary-password-length"); + if length == "8" { + 8 + } else if length == "10" { + 10 + } else { + 6 // default + } +} + +pub fn temporary_enabled() -> bool { + verification_method() != VerificationMethod::OnlyUsePermanentPassword +} + +pub fn permanent_enabled() -> bool { + verification_method() != VerificationMethod::OnlyUseTemporaryPassword +} + +pub fn has_valid_password() -> bool { + temporary_enabled() && !temporary_password().is_empty() + || permanent_enabled() && !Config::get_permanent_password().is_empty() +} + +pub fn approve_mode() -> ApproveMode { + let mode = Config::get_option("approve-mode"); + if mode == "password" { + ApproveMode::Password + } else if mode == "click" { + ApproveMode::Click + } else { + ApproveMode::Both + } +} + +pub fn hide_cm() -> bool { + approve_mode() == ApproveMode::Password + && verification_method() == VerificationMethod::OnlyUsePermanentPassword + && !Config::get_option("allow-hide-cm").is_empty() +} + +const VERSION_LEN: usize = 2; + +pub fn encrypt_str_or_original(s: &str, version: &str) -> String { + if decrypt_str_or_original(s, version).1 { + log::error!("Duplicate encryption!"); + return s.to_owned(); + } + if version == "00" { + if let Ok(s) = encrypt(s.as_bytes()) { + return version.to_owned() + &s; + } + } + s.to_owned() +} + +// String: password +// bool: whether decryption is successful +// bool: whether should store to re-encrypt when load +pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) { + if s.len() > VERSION_LEN { + let version = &s[..VERSION_LEN]; + if version == "00" { + if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) { + return ( + String::from_utf8_lossy(&v).to_string(), + true, + version != current_version, + ); + } + } + } + + (s.to_owned(), false, !s.is_empty()) +} + +pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec { + if decrypt_vec_or_original(v, version).1 { + log::error!("Duplicate encryption!"); + return v.to_owned(); + } + if version == "00" { + if let Ok(s) = encrypt(v) { + let mut version = version.to_owned().into_bytes(); + version.append(&mut s.into_bytes()); + return version; + } + } + v.to_owned() +} + +// Vec: password +// bool: whether decryption is successful +// bool: whether should store to re-encrypt when load +pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) { + if v.len() > VERSION_LEN { + let version = String::from_utf8_lossy(&v[..VERSION_LEN]); + if version == "00" { + if let Ok(v) = decrypt(&v[VERSION_LEN..]) { + return (v, true, version != current_version); + } + } + } + + (v.to_owned(), false, !v.is_empty()) +} + +fn encrypt(v: &[u8]) -> Result { + if v.len() > 0 { + symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) + } else { + Err(()) + } +} + +fn decrypt(v: &[u8]) -> Result, ()> { + if v.len() > 0 { + base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) + } else { + Err(()) + } +} + +fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result, ()> { + use sodiumoxide::crypto::secretbox; + use std::convert::TryInto; + + let mut keybuf = crate::get_uuid(); + keybuf.resize(secretbox::KEYBYTES, 0); + let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); + let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); + + if encrypt { + Ok(secretbox::seal(data, &nonce, &key)) + } else { + secretbox::open(data, &nonce, &key) + } +} + +mod test { + + #[test] + fn test() { + use super::*; + + let version = "00"; + + println!("test str"); + let data = "Hello World"; + let encrypted = encrypt_str_or_original(data, version); + let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); + println!("data: {}", data); + println!("encrypted: {}", encrypted); + println!("decrypted: {}", decrypted); + assert_eq!(data, decrypted); + assert_eq!(version, &encrypted[..2]); + assert_eq!(succ, true); + assert_eq!(store, false); + let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); + assert_eq!(store, true); + assert_eq!(decrypt_str_or_original(&decrypted, version).1, false); + assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted); + + println!("test vec"); + let data: Vec = vec![1, 2, 3, 4, 5, 6]; + let encrypted = encrypt_vec_or_original(&data, version); + let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); + println!("data: {:?}", data); + println!("encrypted: {:?}", encrypted); + println!("decrypted: {:?}", decrypted); + assert_eq!(data, decrypted); + assert_eq!(version.as_bytes(), &encrypted[..2]); + assert_eq!(store, false); + assert_eq!(succ, true); + let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); + assert_eq!(store, true); + assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false); + assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted); + + println!("test original"); + let data = version.to_string() + "Hello World"; + let (decrypted, succ, store) = decrypt_str_or_original(&data, version); + assert_eq!(data, decrypted); + assert_eq!(store, true); + assert_eq!(succ, false); + let verbytes = version.as_bytes(); + let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6]; + let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); + assert_eq!(data, decrypted); + assert_eq!(store, true); + assert_eq!(succ, false); + let (_, succ, store) = decrypt_str_or_original("", version); + assert_eq!(store, false); + assert_eq!(succ, false); + let (_, succ, store) = decrypt_vec_or_original(&vec![], version); + assert_eq!(store, false); + assert_eq!(succ, false); + } +} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs new file mode 100644 index 0000000..4c6375d --- /dev/null +++ b/libs/hbb_common/src/platform/linux.rs @@ -0,0 +1,157 @@ +use crate::ResultType; + +lazy_static::lazy_static! { + pub static ref DISTRO: Disto = Disto::new(); +} + +pub struct Disto { + pub name: String, + pub version_id: String, +} + +impl Disto { + fn new() -> Self { + let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned()) + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + let version_id = + run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned()) + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + Self { name, version_id } + } +} + +pub fn get_display_server() -> String { + let mut session = get_values_of_seat0([0].to_vec())[0].clone(); + if session.is_empty() { + // loginctl has not given the expected output. try something else. + if let Ok(sid) = std::env::var("XDG_SESSION_ID") { + // could also execute "cat /proc/self/sessionid" + session = sid.to_owned(); + } + if session.is_empty() { + session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default(); + } + } + + get_display_server_of_session(&session) +} + +fn get_display_server_of_session(session: &str) -> String { + let mut display_server = if let Ok(output) = + run_loginctl(Some(vec!["show-session", "-p", "Type", session])) + // Check session type of the session + { + let display_server = String::from_utf8_lossy(&output.stdout) + .replace("Type=", "") + .trim_end() + .into(); + if display_server == "tty" { + // If the type is tty... + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session])) + // Get the tty number + { + let tty: String = String::from_utf8_lossy(&output.stdout) + .replace("TTY=", "") + .trim_end() + .into(); + if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) + // And check if Xorg is running on that tty + { + if xorg_results.trim_end().to_string() != "" { + // If it is, manually return "x11", otherwise return tty + return "x11".to_owned(); + } + } + } + } + display_server + } else { + "".to_owned() + }; + if display_server.is_empty() { + // loginctl has not given the expected output. try something else. + if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { + display_server = sestype; + } + } + // If the session is not a tty, then just return the type as usual + display_server +} + +pub fn get_values_of_seat0(indices: Vec) -> Vec { + if let Ok(output) = run_loginctl(None) { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("seat0") { + if let Some(sid) = line.split_whitespace().nth(0) { + if is_active(sid) { + return indices + .into_iter() + .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned()) + .collect::>(); + } + } + } + } + } + + // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 + if let Ok(output) = run_loginctl(None) { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(sid) = line.split_whitespace().nth(0) { + let d = get_display_server_of_session(sid); + if is_active(sid) && d != "tty" { + return indices + .into_iter() + .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned()) + .collect::>(); + } + } + } + } + + return indices + .iter() + .map(|_x| "".to_owned()) + .collect::>(); +} + +fn is_active(sid: &str) -> bool { + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { + String::from_utf8_lossy(&output.stdout).contains("active") + } else { + false + } +} + +pub fn run_cmds(cmds: String) -> ResultType { + let output = std::process::Command::new("sh") + .args(vec!["-c", &cmds]) + .output()?; + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +#[cfg(not(feature = "flatpak"))] +fn run_loginctl(args: Option>) -> std::io::Result { + let mut cmd = std::process::Command::new("loginctl"); + if let Some(a) = args { + return cmd.args(a).output(); + } + cmd.output() +} + +#[cfg(feature = "flatpak")] +fn run_loginctl(args: Option>) -> std::io::Result { + let mut l_args = String::from("loginctl"); + if let Some(a) = args { + l_args = format!("{} {}", l_args, a.join(" ")); + } + std::process::Command::new("flatpak-spawn") + .args(vec![String::from("--host"), l_args]) + .output() +} diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs new file mode 100644 index 0000000..8daba25 --- /dev/null +++ b/libs/hbb_common/src/platform/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "linux")] +pub mod linux; diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 72ab73f..b7cb137 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -9,31 +9,15 @@ use std::net::SocketAddr; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -fn to_socket_addr(host: &str) -> ResultType { - use std::net::ToSocketAddrs; - host.to_socket_addrs()? - .filter(|x| x.is_ipv4()) - .next() - .context("Failed to solve") -} - -pub fn get_target_addr(host: &str) -> ResultType> { - let addr = match Config::get_network_type() { - NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?, - NetworkType::ProxySocks => host.into_target_addr()?, - } - .to_owned(); - Ok(addr) -} - pub fn test_if_valid_server(host: &str) -> String { let mut host = host.to_owned(); if !host.contains(":") { host = format!("{}:{}", host, 0); } + use std::net::ToSocketAddrs; match Config::get_network_type() { - NetworkType::Direct => match to_socket_addr(&host) { + NetworkType::Direct => match host.to_socket_addrs() { Err(err) => err.to_string(), Ok(_) => "".to_owned(), }, @@ -44,33 +28,126 @@ pub fn test_if_valid_server(host: &str) -> String { } } -pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>( +pub trait IsResolvedSocketAddr { + fn resolve(&self) -> Option<&SocketAddr>; +} + +impl IsResolvedSocketAddr for SocketAddr { + fn resolve(&self) -> Option<&SocketAddr> { + Some(&self) + } +} + +impl IsResolvedSocketAddr for String { + fn resolve(&self) -> Option<&SocketAddr> { + None + } +} + +impl IsResolvedSocketAddr for &str { + fn resolve(&self) -> Option<&SocketAddr> { + None + } +} + +#[inline] +pub async fn connect_tcp< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( target: T, - local: SocketAddr, ms_timeout: u64, ) -> ResultType { - let target_addr = target.into_target_addr()?; + connect_tcp_local(target, None, ms_timeout).await +} +pub async fn connect_tcp_local< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( + target: T, + local: Option, + ms_timeout: u64, +) -> ResultType { if let Some(conf) = Config::get_socks() { - FramedStream::connect( + return FramedStream::connect( conf.proxy.as_str(), - target_addr, + target, local, conf.username.as_str(), conf.password.as_str(), ms_timeout, ) - .await - } else { - let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)? - .filter(|x| x.is_ipv4()) - .next() - .context("Invalid target addr, no valid ipv4 address can be resolved.")?; - Ok(FramedStream::new(addr, local, ms_timeout).await?) + .await; + } + if let Some(target) = target.resolve() { + if let Some(local) = local { + if local.is_ipv6() && target.is_ipv4() { + let target = query_nip_io(&target).await?; + return Ok(FramedStream::new(target, Some(local), ms_timeout).await?); + } + } + } + Ok(FramedStream::new(target, local, ms_timeout).await?) +} + +#[inline] +pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { + match target { + TargetAddr::Ip(addr) => addr.is_ipv4(), + _ => true, } } -pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType { +#[inline] +pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { + tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) + .await? + .filter(|x| x.is_ipv6()) + .next() + .context("Failed to get ipv6 from nip.io") +} + +#[inline] +pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { + if !ipv4 && crate::is_ipv4_str(&addr) { + if let Some(ip) = addr.split(":").next() { + return addr.replace(ip, &format!("{}.nip.io", ip)); + } + } + addr +} + +async fn test_target(target: &str) -> ResultType { + if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { + if let Ok(addr) = s.peer_addr() { + return Ok(addr); + } + } + tokio::net::lookup_host(target) + .await? + .next() + .context(format!("Failed to look up host for {}", target)) +} + +#[inline] +pub async fn new_udp_for( + target: &str, + ms_timeout: u64, +) -> ResultType<(FramedSocket, TargetAddr<'static>)> { + let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() { + let addr = test_target(target).await?; + (addr.is_ipv4(), addr.into_target_addr()?) + } else { + (true, target.into_target_addr()?) + }; + Ok(( + new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?, + target.to_owned(), + )) +} + +async fn new_udp(local: T, ms_timeout: u64) -> ResultType { match Config::get_socks() { None => Ok(FramedSocket::new(local).await?), Some(conf) => { @@ -87,9 +164,56 @@ pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType< } } -pub async fn rebind_udp(local: T) -> ResultType> { - match Config::get_network_type() { - NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)), - _ => Ok(None), +pub async fn rebind_udp_for( + target: &str, +) -> ResultType)>> { + if Config::get_network_type() != NetworkType::Direct { + return Ok(None); + } + let addr = test_target(target).await?; + let v4 = addr.is_ipv4(); + Ok(Some(( + FramedSocket::new(Config::get_any_listen_addr(v4)).await?, + addr.into_target_addr()?.to_owned(), + ))) +} + +#[cfg(test)] +mod tests { + use std::net::ToSocketAddrs; + + use super::*; + + #[test] + fn test_nat64() { + test_nat64_async(); + } + + #[tokio::main(flavor = "current_thread")] + async fn test_nat64_async() { + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1"); + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io"); + assert_eq!( + ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false), + "1.1.1.1.nip.io:8080" + ); + assert_eq!( + ipv4_to_ipv6("rustdesk.com".to_owned(), false), + "rustdesk.com" + ); + if ("rustdesk.com:80") + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + .is_ipv6() + { + assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()) + .await + .unwrap() + .is_ipv6()); + return; + } + assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err()); } } diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index 7966920..a1322fc 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -5,7 +5,7 @@ use protobuf::Message; use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; use std::{ io::{self, Error, ErrorKind}, - net::SocketAddr, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::{Deref, DerefMut}, pin::Pin, task::{Context, Poll}, @@ -73,73 +73,79 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result( - remote_addr: T1, - local_addr: T2, + pub async fn new( + remote_addr: T, + local_addr: Option, ms_timeout: u64, ) -> ResultType { - for local_addr in lookup_host(&local_addr).await? { - for remote_addr in lookup_host(&remote_addr).await? { - let stream = super::timeout( - ms_timeout, - new_socket(local_addr, true)?.connect(remote_addr), - ) - .await??; - stream.set_nodelay(true).ok(); - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); + for remote_addr in lookup_host(&remote_addr).await? { + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4()) + }; + if let Ok(socket) = new_socket(local, true) { + if let Ok(Ok(stream)) = + super::timeout(ms_timeout, socket.connect(remote_addr)).await + { + stream.set_nodelay(true).ok(); + 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"); + bail!(format!("Failed to connect to {}", remote_addr)); } - pub async fn connect<'a, 't, P, T1, T2>( + pub async fn connect<'a, 't, P, T>( proxy: P, - target: T1, - local: T2, + target: T, + local_addr: Option, username: &'a str, password: &'a str, ms_timeout: u64, ) -> ResultType where P: ToProxyAddrs, - T1: IntoTargetAddr<'t>, - T2: ToSocketAddrs, + T: IntoTargetAddr<'t>, { - if let Some(local) = lookup_host(&local).await?.next() { - if let Some(proxy) = proxy.to_proxy_addrs().next().await { - 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, - )); + 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"); } @@ -252,6 +258,38 @@ pub async fn new_listener(addr: T, reuse: bool) -> ResultType< } } +pub async fn listen_any(port: u16) -> ResultType { + if let Ok(mut socket) = TcpSocket::new_v6() { + #[cfg(unix)] + { + use std::os::unix::io::{FromRawFd, IntoRawFd}; + let raw_fd = socket.into_raw_fd(); + let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) }; + } + #[cfg(windows)] + { + use std::os::windows::prelude::{FromRawSocket, IntoRawSocket}; + let raw_socket = socket.into_raw_socket(); + let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) }; + } + if socket + .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) + .is_ok() + { + if let Ok(l) = socket.listen(DEFAULT_BACKLOG) { + return Ok(l); + } + } + } + let s = TcpSocket::new_v4()?; + s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?; + Ok(s.listen(DEFAULT_BACKLOG)?) +} + impl Unpin for DynTcpStream {} impl AsyncRead for DynTcpStream { diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 3532dd1..38121a4 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -49,7 +49,7 @@ impl FramedSocket { #[allow(clippy::never_loop)] pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + for addr in addr.to_socket_addrs()? { let socket = new_socket(addr, true, 0)?.into_udp_socket(); return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(socket)?, @@ -63,7 +63,7 @@ impl FramedSocket { addr: T, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + for addr in addr.to_socket_addrs()? { return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, BytesCodec::new(), @@ -164,4 +164,13 @@ impl FramedSocket { None } } + + pub fn is_ipv4(&self) -> bool { + if let FramedSocket::Direct(x) = self { + if let Ok(v) = x.get_ref().local_addr() { + return v.is_ipv4(); + } + } + true + } } diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index 9cea15d..ed8796d 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -1202,7 +1202,7 @@ async fn check_relay_servers(rs0: Arc, tx: Sender) { let rs = rs.clone(); let x = x.clone(); futs.push(tokio::spawn(async move { - if FramedStream::new(&host, "0.0.0.0:0", CHECK_RELAY_TIMEOUT) + if FramedStream::new(&host, None, CHECK_RELAY_TIMEOUT) .await .is_ok() { diff --git a/src/version.rs b/src/version.rs index a68926b..cd0f4db 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1 +1,2 @@ -pub const VERSION: &str = "1.1.6"; \ No newline at end of file +pub const VERSION: &str = "1.1.6"; +pub const BUILD_DATE: &str = "2023-01-06 10:39"; \ No newline at end of file From 93a89b8ea30416f5190ef94a79a8a27f202d5f36 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 11:04:57 +0800 Subject: [PATCH 14/14] modify LOCAL_IP desc --- README.md | 2 +- db_v2.sqlite3 | Bin 24576 -> 24576 bytes src/version.rs | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8254884..9f23a1b 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,7 @@ You can specify the variables as usual or use an `.env` file. | DOWNGRADE_THRESHOLD | hbbr | threshold of downgrade check (bit/ms) | | KEY | hbbs/hbbr | if set force the use of a specific key, if set to **"_"** force the use of any key | | LIMIT_SPEED | hbbr | speed limit (in Mb/s) | -| LOCAL_IP | hbbs | hbbs IP address used in hole-punching | +| LOCAL_IP | hbbs | hbbs local IP address used together with MASK for solving relay failure between LAN and WAN | | MASK | hbbs | network+mask of LAN IPs | | PORT | hbbs/hbbr | listening port (21116 for hbbs - 21117 for hbbr) | | RELAY_SERVERS | hbbs | IP address/DNS name of the machines running hbbr (separated by comma) | diff --git a/db_v2.sqlite3 b/db_v2.sqlite3 index 3d9350d633b1cc07d8369dd281b13aa2f6fbe07b..c95a2f35642e8de05efa929a3e21c4b53f2acf40 100644 GIT binary patch delta 44 zcmZoTz}RqrQ6@OhC$l6~AuYcsH?c&)m_dMnk&(ecL4kpRfpemaGb885gaz>cAk_cAnOZq diff --git a/src/version.rs b/src/version.rs index cd0f4db..03a4771 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,2 +1,2 @@ pub const VERSION: &str = "1.1.6"; -pub const BUILD_DATE: &str = "2023-01-06 10:39"; \ No newline at end of file +pub const BUILD_DATE: &str = "2023-01-06 11:03"; \ No newline at end of file