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