diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 000000000..90a3d4fb3 --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,15 @@ +name: Publish to WinGet +on: + release: + types: [released] + workflow_dispatch: +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: vedantmgoyal9/winget-releaser@main + with: + identifier: RustDesk.RustDesk + version: "1.4.6" + release-tag: "1.4.6" + token: ${{ secrets.WINGET_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index e36c65fab..000000000 --- a/AGENTS.md +++ /dev/null @@ -1,62 +0,0 @@ -# RustDesk Guide - -## Project Layout - -### Directory Structure -* `src/` Rust app -* `src/server/` audio / clipboard / input / video / network -* `src/platform/` platform-specific code -* `src/ui/` legacy Sciter UI (deprecated) -* `flutter/` current UI -* `libs/hbb_common/` config / proto / shared utils -* `libs/scrap/` screen capture -* `libs/enigo/` input control -* `libs/clipboard/` clipboard -* `libs/hbb_common/src/config.rs` all options - -### Key Components -- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server -- **Screen Capture**: Platform-specific screen capture in `libs/scrap/` -- **Input Handling**: Cross-platform input simulation in `libs/enigo/` -- **Audio/Video Services**: Real-time audio/video streaming in `src/server/` -- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` - -### UI Architecture -- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` -- **Modern UI**: Flutter-based - files in `flutter/` - - Desktop: `flutter/lib/desktop/` - - Mobile: `flutter/lib/mobile/` - - Shared: `flutter/lib/common/` and `flutter/lib/models/` - -## Rust Rules - -* Avoid `unwrap()` / `expect()` in production code. -* Exceptions: - - * tests; - * lock acquisition where failure means poisoning, not normal control flow. -* Otherwise prefer `Result` + `?` or explicit handling. -* Do not ignore errors silently. -* Avoid unnecessary `.clone()`. -* Prefer borrowing when practical. -* Do not add dependencies unless needed. -* Keep code simple and idiomatic. - -## Tokio Rules - -* Assume a Tokio runtime already exists. -* Never create nested runtimes. -* Never call `Runtime::block_on()` inside Tokio / async code. -* Do not hide runtime creation inside helpers or libraries. -* Do not hold locks across `.await`. -* Prefer `.await`, `tokio::spawn`, channels. -* Use `spawn_blocking` or dedicated threads for blocking work. -* Do not use `std::thread::sleep()` in async code. - -## Editing Hygiene - -* Change only what is required. -* Prefer the smallest valid diff. -* Do not refactor unrelated code. -* Do not make formatting-only changes. -* Keep naming/style consistent with nearby code. diff --git a/CLAUDE.md b/CLAUDE.md index c31706425..8d46e1fa1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,91 @@ -AGENTS.md +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Build Commands +- `cargo run` - Build and run the desktop application (requires libsciter library) +- `python3 build.py --flutter` - Build Flutter version (desktop) +- `python3 build.py --flutter --release` - Build Flutter version in release mode +- `python3 build.py --hwcodec` - Build with hardware codec support +- `python3 build.py --vram` - Build with VRAM feature (Windows only) +- `cargo build --release` - Build Rust binary in release mode +- `cargo build --features hwcodec` - Build with specific features + +### Flutter Mobile Commands +- `cd flutter && flutter build android` - Build Android APK +- `cd flutter && flutter build ios` - Build iOS app +- `cd flutter && flutter run` - Run Flutter app in development mode +- `cd flutter && flutter test` - Run Flutter tests + +### Testing +- `cargo test` - Run Rust tests +- `cd flutter && flutter test` - Run Flutter tests + +### Platform-Specific Build Scripts +- `flutter/build_android.sh` - Android build script +- `flutter/build_ios.sh` - iOS build script +- `flutter/build_fdroid.sh` - F-Droid build script + +## Project Architecture + +### Directory Structure +- **`src/`** - Main Rust application code + - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead) + - `src/server/` - Audio/clipboard/input/video services and network connections + - `src/client.rs` - Peer connection handling + - `src/platform/` - Platform-specific code +- **`flutter/`** - Flutter UI code for desktop and mobile +- **`libs/`** - Core libraries + - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities + - `libs/scrap/` - Screen capture functionality + - `libs/enigo/` - Platform-specific keyboard/mouse control + - `libs/clipboard/` - Cross-platform clipboard implementation + +### Key Components +- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server +- **Screen Capture**: Platform-specific screen capture in `libs/scrap/` +- **Input Handling**: Cross-platform input simulation in `libs/enigo/` +- **Audio/Video Services**: Real-time audio/video streaming in `src/server/` +- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` + +### UI Architecture +- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` +- **Modern UI**: Flutter-based - files in `flutter/` + - Desktop: `flutter/lib/desktop/` + - Mobile: `flutter/lib/mobile/` + - Shared: `flutter/lib/common/` and `flutter/lib/models/` + +## Important Build Notes + +### Dependencies +- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom` +- Set `VCPKG_ROOT` environment variable +- Download appropriate Sciter library for legacy UI support + +### Ignore Patterns +When working with files, ignore these directories: +- `target/` - Rust build artifacts +- `flutter/build/` - Flutter build output +- `flutter/.dart_tool/` - Flutter tooling files + +### Cross-Platform Considerations +- Windows builds require additional DLLs and virtual display drivers +- macOS builds need proper signing and notarization for distribution +- Linux builds support multiple package formats (deb, rpm, AppImage) +- Mobile builds require platform-specific toolchains (Android SDK, Xcode) + +### Feature Flags +- `hwcodec` - Hardware video encoding/decoding +- `vram` - VRAM optimization (Windows only) +- `flutter` - Enable Flutter UI +- `unix-file-copy-paste` - Unix file clipboard support +- `screencapturekit` - macOS ScreenCaptureKit (macOS only) + +### Config +All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types: +- Settings +- Local +- Display +- Built-in diff --git a/Cargo.lock b/Cargo.lock index fe1f67cc0..06cfeeb96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aead" version = "0.5.2" @@ -299,8 +293,8 @@ dependencies = [ "image 0.25.1", "log", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "parking_lot", "percent-encoding", "serde 1.0.228", @@ -643,7 +637,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", ] @@ -866,15 +860,6 @@ dependencies = [ "objc2 0.5.2", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.4", -] - [[package]] name = "blocking" version = "1.6.1" @@ -1197,7 +1182,7 @@ dependencies = [ "js-sys", "num-traits 0.2.19", "wasm-bindgen", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -1305,8 +1290,8 @@ dependencies = [ "lazy_static", "libc", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "parking_lot", "percent-encoding", @@ -2231,15 +2216,6 @@ dependencies = [ "dirs-sys 0.4.1", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -2257,7 +2233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users 0.4.5", + "redox_users", "winapi 0.3.9", ] @@ -2269,22 +2245,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.5", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -2292,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users 0.4.5", + "redox_users", "winapi 0.3.9", ] @@ -2302,16 +2266,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.4", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2761,7 +2715,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide 0.7.4", + "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", @@ -2847,12 +2801,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.9" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -4087,7 +4041,7 @@ dependencies = [ "gif", "jpeg-decoder", "num-traits 0.2.19", - "png 0.17.13", + "png", "qoi", "tiff", ] @@ -4101,7 +4055,7 @@ dependencies = [ "bytemuck", "byteorder", "num-traits 0.2.19", - "png 0.17.13", + "png", "tiff", ] @@ -4812,16 +4766,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "0.8.11" @@ -4872,23 +4816,21 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.1" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145" dependencies = [ + "cocoa 0.25.0", "crossbeam-channel", "dpi", "gtk", "keyboard-types", "libxdo", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc", "once_cell", - "png 0.17.13", - "thiserror 2.0.17", - "windows-sys 0.60.2", + "png", + "thiserror 1.0.61", + "windows-sys 0.52.0", ] [[package]] @@ -5432,16 +5374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys 0.3.5", - "objc2-encode 4.1.0", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode 4.1.0", + "objc2-encode 4.0.3", ] [[package]] @@ -5456,22 +5389,10 @@ dependencies = [ "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-quartz-core", ] -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -5482,7 +5403,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5493,7 +5414,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5505,28 +5426,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.9.1", - "dispatch2", - "objc2 0.6.4", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.9.1", - "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -5537,7 +5437,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-metal", ] @@ -5550,7 +5450,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5564,9 +5464,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.1.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" [[package]] name = "objc2-foundation" @@ -5581,18 +5481,6 @@ dependencies = [ "objc2 0.5.2", ] -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.9.1", - "block2 0.6.2", - "objc2 0.6.4", - "objc2-core-foundation", -] - [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -5601,8 +5489,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -5614,7 +5502,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5626,7 +5514,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-metal", ] @@ -5637,7 +5525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5653,7 +5541,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -5669,7 +5557,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5682,7 +5570,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5996,8 +5884,8 @@ dependencies = [ [[package]] name = "parity-tokio-ipc" -version = "0.7.3-6" -source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01" +version = "0.7.3-5" +source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291" dependencies = [ "futures", "libc", @@ -6290,20 +6178,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.4", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.9.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -6988,17 +6863,6 @@ dependencies = [ "thiserror 1.0.61", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 2.0.17", -] - [[package]] name = "regex" version = "1.11.1" @@ -8117,8 +7981,8 @@ dependencies = [ "log", "memmap2", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "objc2-quartz-core", "raw-window-handle 0.6.2", "redox_syscall 0.5.2", @@ -8448,7 +8312,7 @@ dependencies = [ "objc", "once_cell", "parking_lot", - "png 0.17.13", + "png", "raw-window-handle 0.6.2", "scopeguard", "tao-macros", @@ -8702,7 +8566,7 @@ dependencies = [ "bytemuck", "cfg-if 1.0.0", "log", - "png 0.17.13", + "png", "tiny-skia-path", ] @@ -9075,22 +8939,21 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.3" -source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6" +version = "0.14.3" +source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f" dependencies = [ + "core-graphics 0.23.2", "crossbeam-channel", - "dirs 6.0.0", + "dirs 5.0.1", "libappindicator", "muda", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", - "png 0.18.1", - "thiserror 2.0.17", - "windows-sys 0.60.2", + "png", + "thiserror 1.0.61", + "windows-sys 0.52.0", ] [[package]] @@ -10195,7 +10058,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link 0.1.1", + "windows-link", "windows-numerics", ] @@ -10244,7 +10107,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link 0.1.1", + "windows-link", "windows-result 0.3.2", "windows-strings 0.4.0", ] @@ -10256,7 +10119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10309,12 +10172,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-numerics" version = "0.2.0" @@ -10322,7 +10179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10340,7 +10197,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10360,7 +10217,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10369,7 +10226,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10399,24 +10256,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" @@ -10456,30 +10295,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-version" version = "0.1.1" @@ -10516,12 +10338,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -10552,12 +10368,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -10588,24 +10398,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -10636,12 +10434,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -10672,12 +10464,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -10696,12 +10482,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -10732,12 +10512,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winit" version = "0.30.9" @@ -10762,8 +10536,8 @@ dependencies = [ "memmap2", "ndk 0.9.0", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "objc2-ui-kit", "orbclient", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index fa22dcd7b..d792d5cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,7 @@ piet-coregraphics = "0.6" foreign-types = "0.3" [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] -tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" } +tray-icon = { git = "https://github.com/tauri-apps/tray-icon" } tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" } image = "0.24" @@ -245,6 +245,3 @@ panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip rpath = true - -[profile.dev] -debug = 1 diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index c31706425..000000000 --- a/GEMINI.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md diff --git a/build.py b/build.py index 5c53e4fc8..ce9a09ef6 100755 --- a/build.py +++ b/build.py @@ -512,7 +512,7 @@ def main(): system2('pip3 install -r requirements.txt') system2( f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe') - system2(f'mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..') + system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) diff --git a/docs/CODE_OF_CONDUCT-FR.md b/docs/CODE_OF_CONDUCT-FR.md deleted file mode 100644 index dca61e0aa..000000000 --- a/docs/CODE_OF_CONDUCT-FR.md +++ /dev/null @@ -1,143 +0,0 @@ - -# Code de conduite des contributeurs - -## Notre engagement - -En tant que membres, contributeurs et responsables, nous nous engageons à faire -de la participation à notre communauté une expérience exempte de harcèlement pour -tous, indépendamment de l'âge, de la taille corporelle, du handicap visible ou -invisible, de l'origine ethnique, des caractéristiques sexuelles, de l'identité -et de l'expression de genre, du niveau d'expérience, de l'éducation, du statut -socio-économique, de la nationalité, de l'apparence personnelle, de la race, de -la religion ou de l'identité et de l'orientation sexuelle. - -Nous nous engageons à agir et à interagir de manière à contribuer à une -communauté ouverte, accueillante, diversifiée, inclusive et saine. - -## Nos standards - -Exemples de comportements qui contribuent à un environnement positif pour notre -communauté : - -* Faire preuve d'empathie et de bienveillance envers les autres -* Respecter les opinions, les points de vue et les expériences différents -* Donner et accepter gracieusement les retours constructifs -* Assumer ses responsabilités, s'excuser auprès des personnes affectées par nos - erreurs et apprendre de l'expérience -* Se concentrer sur ce qui est le mieux non seulement pour nous en tant - qu'individus, mais pour l'ensemble de la communauté - -Exemples de comportements inacceptables : - -* L'utilisation de langage ou d'images à caractère sexuel, et les attentions ou - avances sexuelles de quelque nature que ce soit -* Le trolling, les commentaires insultants ou désobligeants, et les attaques - personnelles ou politiques -* Le harcèlement public ou privé -* La publication d'informations privées d'autrui, telles qu'une adresse physique - ou électronique, sans autorisation explicite -* Tout autre comportement qui pourrait raisonnablement être considéré comme - inapproprié dans un cadre professionnel - -## Responsabilités en matière d'application - -Les responsables de la communauté sont chargés de clarifier et d'appliquer nos -standards de comportement acceptable et prendront des mesures correctives -appropriées et équitables en réponse à tout comportement qu'ils jugent -inapproprié, menaçant, offensant ou nuisible. - -Les responsables de la communauté ont le droit et la responsabilité de -supprimer, modifier ou rejeter les commentaires, commits, code, modifications -du wiki, issues et autres contributions qui ne sont pas conformes à ce Code de -conduite, et communiqueront les raisons de leurs décisions de modération le cas -échéant. - -## Portée - -Ce Code de conduite s'applique dans tous les espaces communautaires, et -s'applique également lorsqu'une personne représente officiellement la communauté -dans les espaces publics. Les exemples de représentation de notre communauté -incluent l'utilisation d'une adresse e-mail officielle, la publication via un -compte de réseau social officiel, ou le fait d'agir en tant que représentant -désigné lors d'un événement en ligne ou hors ligne. - -## Application - -Les cas de comportements abusifs, harcelants ou autrement inacceptables peuvent -être signalés aux responsables de la communauté chargés de l'application à -[info@rustdesk.com](mailto:info@rustdesk.com). -Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et -équitable. - -Tous les responsables de la communauté sont tenus de respecter la vie privée et -la sécurité de la personne ayant signalé un incident. - -## Directives d'application - -Les responsables de la communauté suivront ces Directives d'impact communautaire -pour déterminer les conséquences de toute action qu'ils jugent en violation de ce -Code de conduite : - -### 1. Correction - -**Impact communautaire** : Utilisation d'un langage inapproprié ou autre -comportement jugé non professionnel ou indésirable dans la communauté. - -**Conséquence** : Un avertissement écrit et privé de la part des responsables de -la communauté, expliquant la nature de la violation et pourquoi le comportement -était inapproprié. Des excuses publiques peuvent être demandées. - -### 2. Avertissement - -**Impact communautaire** : Une violation par un incident isolé ou une série -d'actions. - -**Conséquence** : Un avertissement avec des conséquences en cas de comportement -répété. Aucune interaction avec les personnes impliquées, y compris les -interactions non sollicitées avec les personnes chargées d'appliquer le Code de -conduite, pendant une période déterminée. Cela inclut d'éviter les interactions -dans les espaces communautaires ainsi que dans les canaux externes comme les -réseaux sociaux. Le non-respect de ces conditions peut entraîner une exclusion -temporaire ou permanente. - -### 3. Exclusion temporaire - -**Impact communautaire** : Une violation grave des standards communautaires, y -compris un comportement inapproprié persistant. - -**Conséquence** : Une exclusion temporaire de toute interaction ou communication -publique avec la communauté pendant une période déterminée. Aucune interaction -publique ou privée avec les personnes impliquées, y compris les interactions non -sollicitées avec les personnes chargées d'appliquer le Code de conduite, n'est -autorisée pendant cette période. Le non-respect de ces conditions peut entraîner -une exclusion permanente. - -### 4. Exclusion permanente - -**Impact communautaire** : Démontrer un schéma de violation des standards -communautaires, y compris un comportement inapproprié persistant, le harcèlement -d'une personne, ou une agression envers des catégories de personnes ou leur -dénigrement. - -**Conséquence** : Une exclusion permanente de toute interaction publique au sein -de la communauté. - -## Attribution - -Ce Code de conduite est adapté du [Contributor Covenant][homepage], version 2.0, -disponible à l'adresse -[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. - -Les Directives d'impact communautaire ont été inspirées par -[l'échelle d'application du code de conduite de Mozilla][Mozilla CoC]. - -Pour des réponses aux questions fréquentes sur ce code de conduite, consultez la -FAQ à l'adresse [https://www.contributor-covenant.org/faq][FAQ]. Des traductions -sont disponibles à l'adresse -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING-FR.md b/docs/CONTRIBUTING-FR.md deleted file mode 100644 index 6f800de7d..000000000 --- a/docs/CONTRIBUTING-FR.md +++ /dev/null @@ -1,55 +0,0 @@ - -# Contribuer à RustDesk - -RustDesk accueille les contributions de tous. Voici les directives si vous -envisagez de nous aider : - -## Contributions - -Les contributions à RustDesk ou à ses dépendances doivent être soumises sous -forme de pull requests GitHub. Chaque pull request sera examinée par un -contributeur principal (une personne ayant la permission d'intégrer des -correctifs) et sera soit intégrée dans la branche principale, soit accompagnée -de retours sur les modifications requises. Toutes les contributions doivent -suivre ce format, même celles des contributeurs principaux. - -Si vous souhaitez travailler sur une issue, veuillez d'abord la revendiquer en -commentant sur l'issue GitHub indiquant que vous souhaitez la traiter. Cela -permet d'éviter les efforts en double de la part des contributeurs sur la même -issue. - -## Liste de vérification pour les pull requests - -- Partez de la branche master et, si nécessaire, effectuez un rebase sur la - branche master actuelle avant de soumettre votre pull request. Si elle ne - fusionne pas proprement avec master, il vous sera peut-être demandé de - rebaser vos modifications. - -- Les commits doivent être aussi petits que possible, tout en s'assurant que - chaque commit est correct de manière indépendante (c.-à-d. que chaque commit - doit compiler et passer les tests). - -- Les commits doivent être accompagnés d'une signature Developer Certificate of - Origin (http://developercertificate.org), indiquant que vous (et votre - employeur le cas échéant) acceptez d'être liés par les termes de la - [licence du projet](../LICENCE). Dans git, il s'agit de l'option `-s` de - `git commit`. - -- Si votre correctif n'est pas examiné ou si vous avez besoin qu'une personne - spécifique l'examine, vous pouvez @-mentionner un relecteur pour demander une - revue dans la pull request ou un commentaire, ou vous pouvez demander une - revue par [e-mail](mailto:info@rustdesk.com). - -- Ajoutez des tests relatifs au bug corrigé ou à la nouvelle fonctionnalité. - -Pour des instructions git spécifiques, consultez le -[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). - -## Conduite - -https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md - -## Communication - -Les contributeurs de RustDesk se retrouvent fréquemment sur -[Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/README-FR.md b/docs/README-FR.md index 345e53b58..c2e25886d 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -34,9 +34,9 @@ Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface - Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`. - Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - - Linux/macOS : vcpkg install libvpx libyuv opus aom + - Linux/Osx : vcpkg install libvpx libyuv opus aom -- Exécutez `cargo run` +- Exécuter `cargo run` ## Comment compiler/build sous Linux @@ -93,7 +93,7 @@ cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so mv libsciter-gtk.so target/debug -cargo run +Exécution du cargo ``` ## Comment construire avec Docker diff --git a/docs/SECURITY-FR.md b/docs/SECURITY-FR.md deleted file mode 100644 index 1cf2c6167..000000000 --- a/docs/SECURITY-FR.md +++ /dev/null @@ -1,16 +0,0 @@ - -# Politique de sécurité - -## Signaler une vulnérabilité - -Nous accordons une très grande importance à la sécurité du projet. Nous -encourageons tous les utilisateurs à nous signaler toute vulnérabilité qu'ils -découvrent. - -Si vous trouvez une vulnérabilité de sécurité dans le projet RustDesk, veuillez -la signaler de manière responsable en envoyant un e-mail à info@rustdesk.com. - -À ce stade, nous n'avons pas de programme de bug bounty. Nous sommes une petite -équipe qui s'attaque à un grand défi. Nous vous encourageons vivement à signaler -toute vulnérabilité de manière responsable afin que nous puissions continuer à -développer une application sécurisée pour l'ensemble de la communauté. diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml index 90bdafcb5..0d3b33bb8 100644 --- a/flatpak/com.rustdesk.RustDesk.metainfo.xml +++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml @@ -18,7 +18,7 @@
  • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
  • Own your data, easily set up self-hosting solution on your infrastructure.
  • P2P connection with end-to-end encryption based on NaCl.
  • -
  • No administrative privileges or installation needed for Windows, elevate privilege locally or from remote on demand.
  • +
  • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
  • We like to keep things simple and will strive to make simpler where possible.
  • @@ -56,4 +56,4 @@ pointing - + \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt index 05742d7fd..db222dc84 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt @@ -62,13 +62,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart: return false } } - val recorder = try { - builder.build() - } catch (e: Exception) { - Log.e(logTag, "createAudioRecorder failed", e) - return false - } - audioRecorder = recorder + audioRecorder = builder.build() Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize") return true } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt index 6dd4a2f61..696d536c6 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -311,10 +311,7 @@ class FloatingWindowService : Service(), View.OnTouchListener { popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) } val idStopService = 2 - val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y" - if (!hideStopService) { - popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) - } + popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { idShowRustDesk -> { @@ -392,3 +389,4 @@ class FloatingWindowService : Service(), View.OnTouchListener { return false } } + diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index e3c9d9830..8e9b39968 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -24,7 +24,6 @@ object FFI { external fun setFrameRawEnable(name: String, value: Boolean) external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String - external fun getBuildinOption(key: String): String external fun onClipboardUpdate(clips: ByteBuffer) external fun isServiceClipboardEnabled(): Boolean } diff --git a/flutter/assets/auth-microsoft.svg b/flutter/assets/auth-microsoft.svg deleted file mode 100644 index c9ce5f9cf..000000000 --- a/flutter/assets/auth-microsoft.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index d50a6a6ce..ecfb444ef 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -7,7 +7,7 @@ # 2024, Vasyl Gello # -# The script is invoked by F-Droid builder system step-by-step. +# The script is invoked by F-Droid builder system ste-by-step. # # It accepts the following arguments: # @@ -16,6 +16,7 @@ # - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64 # - The build step to execute: # +# + sudo-deps: as root, install needed Debian packages into builder VM # + prebuild: patch sources and do other stuff before the build # + build: perform actual build of APK file # @@ -183,9 +184,13 @@ prebuild) fi # Map NDK version to revision - NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json | - jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" | - sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')" + + NDK_VERSION="$(wget \ + -qO- \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + 'https://api.github.com/repos/android/ndk/releases' | + jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")" if [ -z "${NDK_VERSION}" ]; then echo "ERROR: Can not map Android NDK codename to revision!" >&2 @@ -311,18 +316,6 @@ prebuild) # `FLUTTER_BRIDGE_VERSION` an restore the pubspec later if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then - # Find first libclang.so and set BRIDGE_LLVM_PATH - - BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)" - - if [ -z "${BRIDGE_LLVM_PATH}" ]; then - echo 'ERROR: Can not find libclang.so for bridge generator!' >&2 - exit 1 - fi - - BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")" - BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")" - # Install Flutter bridge version prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" @@ -351,8 +344,7 @@ prebuild) flutter_rust_bridge_codegen \ --rust-input ./src/flutter_ffi.rs \ - --dart-output ./flutter/lib/generated_bridge.dart \ - --llvm-path "${BRIDGE_LLVM_PATH}" + --dart-output ./flutter/lib/generated_bridge.dart # Add bridge files to save-list @@ -363,15 +355,13 @@ prebuild) git checkout '*' git clean -dffx git reset - - unset BRIDGE_LLVM_PATH fi # Install Flutter version for RustDesk library build prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter" - # gms is not in these files now, but we still keep the following line for future reference(maybe). + # gms is not in thoes files now, but we still keep the following line for future reference(maybe). sed \ -i \ @@ -424,9 +414,13 @@ build) .github/workflows/flutter-build.yml)" # Map NDK version to revision - NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json | - jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" | - sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')" + + NDK_VERSION="$(wget \ + -qO- \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + 'https://api.github.com/repos/android/ndk/releases' | + jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")" if [ -z "${NDK_VERSION}" ]; then echo "ERROR: Can not map Android NDK codename to revision!" >&2 diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 366a7b6ba..af87f980f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -716,17 +716,6 @@ closeConnection({String? id}) { stateGlobal.isInMainPage = true; } else { final controller = Get.find(); - if (controller.tabType == DesktopTabType.terminal && - controller.onCloseWindow != null) { - // Terminal windows are scoped to one peer. The optional id passed to - // closeConnection() is that peer id, not a terminal tab key - // (${peerId}_${terminalId}). Closing from terminal dialogs should close - // the peer's whole terminal window, including all terminal tabs. - unawaited(controller.onCloseWindow!().catchError((e, _) { - debugPrint('[closeConnection] Failed to close terminal window: $e'); - })); - return; - } controller.closeBy(id); } } @@ -2376,19 +2365,6 @@ List? urlLinkToCmdArgs(Uri uri) { id = uri.path.substring("/new/".length); } else if (uri.authority == "config") { if (isAndroid || isIOS) { - final allowDeepLinkServerSettings = - bind.mainGetBuildinOption(key: kOptionAllowDeepLinkServerSettings) == - 'Y'; - if (!allowDeepLinkServerSettings) { - debugPrint( - "Ignore rustdesk://config because $kOptionAllowDeepLinkServerSettings is not enabled."); - // Keep the user-facing error generic; detailed rejection reason is in debug logs. - // Delay toast to avoid missing overlay during cold-start deeplink handling. - Timer(Duration(seconds: 1), () { - showToast(translate('Failed')); - }); - return null; - } final config = uri.path.substring("/".length); // add a timer to make showToast work Timer(Duration(seconds: 1), () { @@ -2398,24 +2374,11 @@ List? urlLinkToCmdArgs(Uri uri) { return null; } else if (uri.authority == "password") { if (isAndroid || isIOS) { - final allowDeepLinkPassword = - bind.mainGetBuildinOption(key: kOptionAllowDeepLinkPassword) == 'Y'; - if (!allowDeepLinkPassword) { - debugPrint( - "Ignore rustdesk://password because $kOptionAllowDeepLinkPassword is not enabled."); - // Keep the user-facing error generic; detailed rejection reason is in debug logs. - // Delay toast to avoid missing overlay during cold-start deeplink handling. - Timer(Duration(seconds: 1), () { - showToast(translate('Failed')); - }); - return null; - } final password = uri.path.substring("/".length); if (password.isNotEmpty) { Timer(Duration(seconds: 1), () async { - final ok = - await bind.mainSetPermanentPasswordWithResult(password: password); - showToast(translate(ok ? 'Successful' : 'Failed')); + await bind.mainSetPermanentPassword(password: password); + showToast(translate('Successful')); }); } } @@ -4190,7 +4153,8 @@ Widget? buildAvatarWidget({ width: size, height: size, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(), + errorBuilder: (_, __, ___) => + fallback ?? SizedBox.shrink(), ), ); } diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 054a1666c..1a09d6f53 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -54,9 +54,9 @@ class _AddressBookState extends State { const LinearProgressIndicator(), buildErrorBanner(context, loading: gFFI.abModel.currentAbLoading, - err: gFFI.abModel.abPullError, + err: gFFI.abModel.currentAbPullError, retry: null, - close: gFFI.abModel.clearPullErrors), + close: () => gFFI.abModel.currentAbPullError.value = ''), buildErrorBanner(context, loading: gFFI.abModel.currentAbLoading, err: gFFI.abModel.currentAbPushError, diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index ee376de68..62ade8e51 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -20,8 +20,7 @@ const kOpSvgList = [ 'okta', 'facebook', 'azure', - 'auth0', - 'microsoft' + 'auth0' ]; class _IconOP extends StatelessWidget { @@ -225,59 +224,21 @@ class _WidgetOPState extends State { return Offstage( offstage: _failedMsg.isEmpty && widget.curOP.value != widget.config.op, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (_stateMsg.isNotEmpty && _failedMsg.isEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SelectableText( - translate(_stateMsg), - style: DefaultTextStyle.of(context) - .style - .copyWith(fontSize: 12), - ), - ), - if (_failedMsg.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Builder(builder: (context) { - final errorColor = - Theme.of(context).colorScheme.error; - final bgColor = Theme.of(context) - .colorScheme - .errorContainer - .withOpacity(0.3); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, vertical: 6.0), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(4.0), + child: RichText( + text: TextSpan( + text: '$_stateMsg ', + style: + DefaultTextStyle.of(context).style.copyWith(fontSize: 12), + children: [ + TextSpan( + text: _failedMsg, + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14, + color: Colors.red, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.error_outline, - color: errorColor, size: 16), - const SizedBox(width: 6), - Flexible( - child: SelectableText( - translate(_failedMsg), - style: DefaultTextStyle.of(context) - .style - .copyWith( - fontSize: 13, - color: errorColor, - ), - ), - ), - ], - ), - ); - }), ), - ], + ], + ), ), ); }), diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 9515ca759..e35da6424 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -31,7 +31,7 @@ class RawKeyFocusScope extends StatelessWidget { // https://github.com/flutter/flutter/issues/154053 final useRawKeyEvents = isLinux && !isWeb; // FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events, - // while `Alt` and `Control` are separated key events for en-US input method. + // while `Alt` and `Control` are seperated key events for en-US input method. return FocusScope( autofocus: true, child: Focus( @@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState // Official TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer( - supportedDevices: kTouchBasedDeviceKinds, - ), (instance) { + () => TapGestureRecognizer(), (instance) { instance ..onTapDown = onTapDown ..onTapUp = onTapUp @@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState }), DoubleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => DoubleTapGestureRecognizer( - supportedDevices: kTouchBasedDeviceKinds, - ), (instance) { + () => DoubleTapGestureRecognizer(), (instance) { instance ..onDoubleTapDown = onDoubleTapDown ..onDoubleTap = onDoubleTap; }), LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer( - supportedDevices: kTouchBasedDeviceKinds, - ), (instance) { + () => LongPressGestureRecognizer(), (instance) { instance ..onLongPressDown = onLongPressDown ..onLongPressUp = onLongPressUp @@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState // Customized HoldTapMoveGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => HoldTapMoveGestureRecognizer( - supportedDevices: kTouchBasedDeviceKinds, - ), + () => HoldTapMoveGestureRecognizer(), (instance) => instance ..onHoldDragStart = onHoldDragStart ..onHoldDragUpdate = onHoldDragUpdate @@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState ..onHoldDragEnd = onHoldDragEnd), DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => DoubleFinerTapGestureRecognizer( - supportedDevices: kTouchBasedDeviceKinds, - ), (instance) { + () => DoubleFinerTapGestureRecognizer(), (instance) { instance ..onDoubleFinerTap = onDoubleFinerTap ..onDoubleFinerTapDown = onDoubleFinerTapDown; }), CustomTouchGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomTouchGestureRecognizer( - supportedDevices: kTouchBasedDeviceKinds, - ), (instance) { + () => CustomTouchGestureRecognizer(), (instance) { instance.onOneFingerPanStart = (DragStartDetails d) => onOneFingerPanStart(context, d); instance diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 537014246..a46ce54fd 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -16,12 +16,6 @@ import 'package:get/get.dart'; bool isEditOsPassword = false; -// macOS privacy mode blacks out all online displays, so switching the remote -// display does not weaken the local privacy protection. -bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) { - return pi.platform == kPeerPlatformMacOS; -} - class TTextMenu { final Widget child; final VoidCallback? onPressed; @@ -281,6 +275,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { isDesktop && ffiModel.keyboard && pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && versionCmp(pi.version, '1.2.0') >= 0 && bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) { v.add(TTextMenu( @@ -690,9 +685,8 @@ Future> toolbarDisplayToggle( child: Text(translate('Lock after session end')))); } - final privacyModeState = PrivacyModeState.find(id); if (pi.isSupportMultiDisplay && - (privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) && + PrivacyModeState.find(id).isEmpty && pi.displaysCount.value > 1 && bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { final value = @@ -766,25 +760,15 @@ List toolbarPrivacyMode( final ffiModel = ffi.ffiModel; final pi = ffiModel.pi; final sessionId = ffi.sessionId; - final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false; - - // Backend revocation already attempts to turn privacy mode off. - // Still keep this menu when privacy mode is active, so users can turn it off - // if there is a sync delay, version mismatch, or off attempt failure. - if (!hasPrivacyModePermission && privacyModeState.isEmpty) { - return []; // No permission and not active, hide options. - } getDefaultMenu(Future Function(SessionID sid, String opt) toggleFunc) { - final enabled = - !ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty); + final enabled = !ffi.ffiModel.viewOnly; return TToggleMenu( value: privacyModeState.isNotEmpty, onChanged: enabled ? (value) { if (value == null) return; - if (!allowDisplaySwitchInPrivacyMode(pi) && - ffiModel.pi.currentDisplay != 0 && + if (ffiModel.pi.currentDisplay != 0 && ffiModel.pi.currentDisplay != kAllDisplayValue) { msgBox( sessionId, @@ -827,29 +811,18 @@ List toolbarPrivacyMode( }) ]; } else { - final visibleImpls = hasPrivacyModePermission - ? privacyModeImpls - : privacyModeImpls.where((e) { - final implKey = (e as List)[0] as String; - return privacyModeState.value == implKey; - }).toList(); - return visibleImpls.map((e) { + return privacyModeImpls.map((e) { final implKey = (e as List)[0] as String; final implName = (e)[1] as String; - final enabled = !ffiModel.viewOnly && - (hasPrivacyModePermission || privacyModeState.value == implKey); return TToggleMenu( child: Text(translate(implName)), value: privacyModeState.value == implKey, - onChanged: enabled - ? (value) { - if (value == null) return; - if (value && !hasPrivacyModePermission) return; - togglePrivacyModeTime = DateTime.now(); - bind.sessionTogglePrivacyMode( - sessionId: sessionId, implKey: implKey, on: value); - } - : null); + onChanged: (value) { + if (value == null) return; + togglePrivacyModeTime = DateTime.now(); + bind.sessionTogglePrivacyMode( + sessionId: sessionId, implKey: implKey, on: value); + }); }).toList(); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index adf7b1d45..3b9940c9c 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -114,9 +114,6 @@ const String kOptionTerminalPersistent = "terminal-persistent"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; -const String kOptionEnablePrivacyMode = "enable-privacy-mode"; -const String kOptionEnablePermChangeInAcceptWindow = - "enable-perm-change-in-accept-window"; const String kOptionAllowRemoteConfigModification = "allow-remote-config-modification"; const String kOptionVerificationMethod = "verification-method"; @@ -142,10 +139,6 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse"; const String kOptionCodecPreference = "codec-preference"; const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left"; const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right"; -const String kOptionRemoteMenubarEdge = "remote-menubar-edge"; -const String kOptionRemoteMenubarFraction = "remote-menubar-frac"; -const String kOptionAllowMultiEdgeToolbarDock = - "allow-multi-edge-toolbar-dock"; const String kOptionHideAbTagsPanel = "hideAbTagsPanel"; const String kOptionRemoteMenubarState = "remoteMenubarState"; const String kOptionPeerSorting = "peer-sorting"; @@ -182,7 +175,6 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust"; const String kOptionHideServerSetting = "hide-server-settings"; const String kOptionHideProxySetting = "hide-proxy-settings"; const String kOptionHideWebSocketSetting = "hide-websocket-settings"; -const String kOptionHideStopService = "hide-stop-service"; const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings"; const String kOptionHideSecuritySetting = "hide-security-settings"; const String kOptionHideNetworkSetting = "hide-network-settings"; @@ -194,9 +186,6 @@ const String kOptionDisableChangeId = "disable-change-id"; const String kOptionDisableUnlockPin = "disable-unlock-pin"; const kHideUsernameOnCard = "hide-username-on-card"; const String kOptionHideHelpCards = "hide-help-cards"; -const String kOptionAllowDeepLinkPassword = "allow-deep-link-password"; -const String kOptionAllowDeepLinkServerSettings = - "allow-deep-link-server-settings"; const String kOptionToggleViewOnly = "view-only"; const String kOptionToggleShowMyCursor = "show-my-cursor"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 42ec10032..339ecddb0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -908,17 +908,12 @@ class _DesktopHomePageState extends State } void setPasswordDialog({VoidCallback? notEmptyCallback}) async { - final p0 = TextEditingController(text: ""); - final p1 = TextEditingController(text: ""); + final pw = await bind.mainGetPermanentPassword(); + final p0 = TextEditingController(text: pw); + final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; - final localPasswordSet = - (await bind.mainGetCommon(key: "local-permanent-password-set")) == "true"; - final permanentPasswordSet = - (await bind.mainGetCommon(key: "permanent-password-set")) == "true"; - final presetPassword = permanentPasswordSet && !localPasswordSet; - var canSubmit = false; - final RxString rxPass = "".obs; + final RxString rxPass = pw.trim().obs; final rules = [ DigitValidationRule(), UppercaseValidationRule(), @@ -927,21 +922,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { MinCharactersValidationRule(8), ]; final maxLength = bind.mainMaxEncryptLen(); - final statusTip = localPasswordSet - ? translate('password-hidden-tip') - : (presetPassword ? translate('preset-password-in-use-tip') : ''); - final showStatusTipOnMobile = - statusTip.isNotEmpty && !isDesktop && !isWebDesktop; gFFI.dialogManager.show((setState, close, context) { - updateCanSubmit() { - canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty; - } - - submit() async { - if (!canSubmit) { - return; - } + submit() { setState(() { errMsg0 = ""; errMsg1 = ""; @@ -964,13 +947,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); return; } - final ok = await bind.mainSetPermanentPasswordWithResult(password: pass); - if (!ok) { - setState(() { - errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; - }); - return; - } + bind.mainSetPermanentPassword(password: pass); if (pass.isNotEmpty) { notEmptyCallback?.call(); } @@ -978,20 +955,14 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { } return CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.key, color: MyTheme.accent), - Text(translate("Set Password")).paddingOnly(left: 10), - ], - ), + title: Text(translate("Set Password")), content: ConstrainedBox( constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - height: showStatusTipOnMobile ? 0.0 : 6.0, + const SizedBox( + height: 8.0, ), Row( children: [ @@ -1007,7 +978,6 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { rxPass.value = value.trim(); setState(() { errMsg0 = ''; - updateCanSubmit(); }); }, maxLength: maxLength, @@ -1019,9 +989,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { children: [ Expanded(child: PasswordStrengthIndicator(password: rxPass)), ], - ).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8), - SizedBox( - height: showStatusTipOnMobile ? 0.0 : 8.0, + ).marginSymmetric(vertical: 8), + const SizedBox( + height: 8.0, ), Row( children: [ @@ -1035,7 +1005,6 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { onChanged: (value) { setState(() { errMsg1 = ''; - updateCanSubmit(); }); }, maxLength: maxLength, @@ -1043,23 +1012,11 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { ), ], ), - if (statusTip.isNotEmpty) - Row( - children: [ - Icon(Icons.info, color: Colors.amber, size: 18) - .marginOnly(right: 6), - Expanded( - child: Text( - statusTip, - style: const TextStyle(fontSize: 13, height: 1.1), - )) - ], - ).marginOnly(top: 6, bottom: 2), - SizedBox( - height: showStatusTipOnMobile ? 0.0 : 8.0, + const SizedBox( + height: 8.0, ), Obx(() => Wrap( - runSpacing: showStatusTipOnMobile ? 2.0 : 8.0, + runSpacing: 8, spacing: 4, children: rules.map((e) { var checked = e.validate(rxPass.value.trim()); @@ -1079,67 +1036,11 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { ], ), ), - actions: (() { - final cancelButton = dialogButton( - "Cancel", - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ); - final removeButton = dialogButton( - "Remove", - icon: Icon(Icons.delete_outline_rounded), - onPressed: () async { - setState(() { - errMsg0 = ""; - errMsg1 = ""; - }); - final ok = - await bind.mainSetPermanentPasswordWithResult(password: ""); - if (!ok) { - setState(() { - errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; - }); - return; - } - close(); - }, - buttonStyle: ButtonStyle( - backgroundColor: MaterialStatePropertyAll(Colors.red)), - ); - final okButton = dialogButton( - "OK", - icon: Icon(Icons.done_rounded), - onPressed: canSubmit ? submit : null, - ); - if (!isDesktop && !isWebDesktop && localPasswordSet) { - return [ - Align( - alignment: Alignment.centerRight, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - cancelButton, - const SizedBox(width: 4), - removeButton, - const SizedBox(width: 4), - okButton, - ], - ), - ), - ), - ]; - } - return [ - cancelButton, - if (localPasswordSet) removeButton, - okButton, - ]; - })(), - onSubmit: canSubmit ? submit : null, + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, onCancel: close, ); }); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d1d620014..82212d191 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -458,27 +458,18 @@ class _GeneralState extends State<_General> { return const Offstage(); } - final hideStopService = - bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; - - return Obx(() { - if (hideStopService && !serviceStop.value) { - return const Offstage(); - } - - return _Card(title: 'Service', children: [ - _Button(serviceStop.value ? 'Start' : 'Stop', () { - () async { - serviceBtnEnabled.value = false; - await start_service(serviceStop.value); - // enable the button after 1 second - Future.delayed(const Duration(seconds: 1), () { - serviceBtnEnabled.value = true; - }); - }(); - }, enabled: serviceBtnEnabled.value) - ]); - }); + return _Card(title: 'Service', children: [ + Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () { + () async { + serviceBtnEnabled.value = false; + await start_service(serviceStop.value); + // enable the button after 1 second + Future.delayed(const Duration(seconds: 1), () { + serviceBtnEnabled.value = true; + }); + }(); + }, enabled: serviceBtnEnabled.value)) + ]); } Widget other() { @@ -488,16 +479,6 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), - if (!bind.isIncomingOnly()) - _OptionCheckBox( - context, - 'allow-remote-toolbar-docking-any-edge', - kOptionAllowMultiEdgeToolbarDock, - isServer: false, - update: (_) { - reloadAllWindows(); - }, - ), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), if (!isWeb) wallpaper(), if (!isWeb && !bind.isIncomingOnly()) ...[ @@ -1072,10 +1053,6 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'Enable blocking user input', kOptionEnableBlockInput, enabled: enabled, fakeValue: fakeValue), - if (bind.mainSupportedPrivacyModeImpls() != '[]') - _OptionCheckBox( - context, 'Enable privacy mode', kOptionEnablePrivacyMode, - enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable remote configuration modification', kOptionAllowRemoteConfigModification, enabled: enabled, fakeValue: fakeValue), @@ -1123,9 +1100,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (value == passwordValues[passwordKeys .indexOf(kUsePermanentPassword)] && - (await bind.mainGetCommon( - key: "permanent-password-set")) != - "true") { + (await bind.mainGetPermanentPassword()) + .isEmpty) { if (isChangePermanentPasswordDisabled()) { await callback(); return; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 8bd7df08b..7d48452a8 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -610,24 +610,19 @@ class _PrivilegeBoard extends StatefulWidget { class _PrivilegeBoardState extends State<_PrivilegeBoard> { late final client = widget.client; Widget buildPermissionIcon(bool enabled, IconData iconData, - Function(bool)? onTap, String tooltipText, - {required bool canModify}) { + Function(bool)? onTap, String tooltipText) { return Tooltip( message: "$tooltipText: ${enabled ? "ON" : "OFF"}", waitDuration: Duration.zero, child: Container( decoration: BoxDecoration( - color: enabled - ? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6)) - : Colors.grey[700], + color: enabled ? MyTheme.accent : Colors.grey[700], borderRadius: BorderRadius.circular(10.0), ), padding: EdgeInsets.all(8.0), child: InkWell( - onTap: canModify - ? () => - checkClickTime(widget.client.id, () => onTap?.call(!enabled)) - : null, + onTap: () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -648,9 +643,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { Widget build(BuildContext context) { final crossAxisCount = 4; final spacing = 10.0; - final canModifyPermission = - bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) != - 'N'; return Container( width: double.infinity, height: 160.0, @@ -697,7 +689,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable audio'), - canModify: canModifyPermission, ), buildPermissionIcon( client.recording, @@ -712,7 +703,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable recording session'), - canModify: canModifyPermission, ), ] : [ @@ -729,7 +719,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable keyboard/mouse'), - canModify: canModifyPermission, ), buildPermissionIcon( client.clipboard, @@ -744,7 +733,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable clipboard'), - canModify: canModifyPermission, ), buildPermissionIcon( client.audio, @@ -759,7 +747,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable audio'), - canModify: canModifyPermission, ), buildPermissionIcon( client.file, @@ -774,7 +761,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable file copy and paste'), - canModify: canModifyPermission, ), buildPermissionIcon( client.restart, @@ -789,7 +775,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable remote restart'), - canModify: canModifyPermission, ), buildPermissionIcon( client.recording, @@ -804,7 +789,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable recording session'), - canModify: canModifyPermission, ), // only windows support block input if (isWindows) @@ -821,23 +805,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable blocking user input'), - canModify: canModifyPermission, - ), - if (bind.mainSupportedPrivacyModeImpls() != '[]') - buildPermissionIcon( - client.privacyMode, - Icons.visibility_off, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, - name: "privacy_mode", - enabled: enabled); - setState(() { - client.privacyMode = enabled; - }); - }, - translate('Enable privacy mode'), - canModify: canModifyPermission, ) ], ), diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index d38dc4a8b..0070cd73b 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -27,7 +27,6 @@ class TerminalPage extends StatefulWidget { final bool? isSharedPassword; final String? connToken; final int terminalId; - /// Tab key for focus management, passed from parent to avoid duplicate construction final String tabKey; final SimpleWrapper?> _lastState = SimpleWrapper(null); @@ -44,9 +43,6 @@ class TerminalPage extends StatefulWidget { class _TerminalPageState extends State with AutomaticKeepAliveClientMixin { - static const EdgeInsets _defaultTerminalPadding = - EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); - late FFI _ffi; late TerminalModel _terminalModel; double? _cellHeight; @@ -159,27 +155,13 @@ class _TerminalPageState extends State // extra space left after dividing the available height by the height of a single // terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding. EdgeInsets _calculatePadding(double heightPx) { - final cellHeight = _cellHeight; - if (!heightPx.isFinite || - heightPx <= 0 || - cellHeight == null || - !cellHeight.isFinite || - cellHeight <= 0) { - return _defaultTerminalPadding; - } - final rows = (heightPx / cellHeight).floor(); - if (rows <= 0) { - return _defaultTerminalPadding; - } - final extraSpace = heightPx - rows * cellHeight; - if (!extraSpace.isFinite || extraSpace < 0) { - return _defaultTerminalPadding; + if (_cellHeight == null) { + return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); } + final rows = (heightPx / _cellHeight!).floor(); + final extraSpace = heightPx - rows * _cellHeight!; final topBottom = extraSpace / 2.0; - return EdgeInsets.symmetric( - horizontal: _defaultTerminalPadding.horizontal / 2, - vertical: topBottom, - ); + return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom); } @override diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 63289e94d..28e59fb05 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -46,7 +46,6 @@ class _TerminalTabPageState extends State { .setTitle(getWindowNameWithId(id)); }; tabController.onRemoved = (_, id) => onRemoveId(id); - tabController.onCloseWindow = _closeWindowFromConnection; final terminalId = params['terminalId'] ?? _nextTerminalId++; tabController.add(_createTerminalTab( peerId: params['id'], @@ -145,8 +144,6 @@ class _TerminalTabPageState extends State { _windowClosing = true; final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList(); // Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) - // Keep the cleanup target lookup below synchronous before its first await: - // it relies on the current frame still retaining each TerminalPage's FFI/model. tabController.clear(); // Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout). // Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls. @@ -371,34 +368,8 @@ class _TerminalTabPageState extends State { final persistentSessions = args['persistent_sessions'] as List? ?? []; final sortedSessions = persistentSessions.whereType().toList()..sort(); - var peerId = args['peer_id'] as String? ?? ''; - if (peerId.isEmpty) { - if (tabController.state.value.tabs.isEmpty || - tabController.state.value.selected >= - tabController.state.value.tabs.length) { - debugPrint('[TerminalTabPage] Skip restore: no selected tab'); - return; - } - final currentTab = tabController.state.value.selectedTabInfo; - final parsed = _parseTabKey(currentTab.key); - if (parsed == null) return; - peerId = parsed.$1; - } - final existingTerminalIds = tabController.state.value.tabs - .map((tab) => _parseTabKey(tab.key)) - .where((parsed) => parsed != null && parsed.$1 == peerId) - .map((parsed) => parsed!.$2) - .toSet(); - if (existingTerminalIds.isEmpty) { - debugPrint( - '[TerminalTabPage] Skip restore: no seed tab for peer $peerId'); - return; - } for (final terminalId in sortedSessions) { - if (!existingTerminalIds.add(terminalId)) { - continue; - } - _addNewTerminal(peerId, terminalId: terminalId); + _addNewTerminalForCurrentPeer(terminalId: terminalId); // A delay is required to ensure the UI has sufficient time to update // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()` // may be called prematurely while the tab widget is still in the tab controller. @@ -575,11 +546,6 @@ class _TerminalTabPageState extends State { } } - Future _closeWindowFromConnection() async { - await _closeAllTabs(); - await WindowController.fromWindowId(windowId()).close(); - } - int windowId() { return widget.params["windowId"]; } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 44a2dc1c7..ec05c987f 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -28,220 +28,6 @@ import './kb_layout_type_chooser.dart'; import 'package:flutter_hbb/utils/scale.dart'; import 'package:flutter_hbb/common/widgets/custom_scale_base.dart'; -enum _ToolbarEdge { top, right, bottom, left } - -_ToolbarEdge _parseToolbarEdge(String? s) { - switch (s) { - case 'right': - return _ToolbarEdge.right; - case 'bottom': - return _ToolbarEdge.bottom; - case 'left': - return _ToolbarEdge.left; - default: - return _ToolbarEdge.top; - } -} - -String _toolbarEdgeToString(_ToolbarEdge e) { - switch (e) { - case _ToolbarEdge.top: - return 'top'; - case _ToolbarEdge.right: - return 'right'; - case _ToolbarEdge.bottom: - return 'bottom'; - case _ToolbarEdge.left: - return 'left'; - } -} - -bool _isHorizontalEdge(_ToolbarEdge e) => - e == _ToolbarEdge.top || e == _ToolbarEdge.bottom; - -const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x'; - -double _clampToolbarFraction(double fraction, double left, double right) { - if (fraction < left) fraction = left; - if (fraction > right) fraction = right; - return fraction; -} - -Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) { - final isHorizontal = _isHorizontalEdge(edge); - final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360); - final size = measured ?? fallback; - final long = size.longestSide; - final short = size.shortestSide; - return Size(isHorizontal ? long : short, isHorizontal ? short : long); -} - -Offset _toolbarOffsetForEdge({ - required _ToolbarEdge edge, - required double fraction, - required Size parentSize, - required Size toolbarSize, -}) { - final xTravel = parentSize.width - toolbarSize.width; - final yTravel = parentSize.height - toolbarSize.height; - switch (edge) { - case _ToolbarEdge.top: - return Offset(xTravel * fraction, 0); - case _ToolbarEdge.bottom: - return Offset(xTravel * fraction, yTravel); - case _ToolbarEdge.left: - return Offset(0, yTravel * fraction); - case _ToolbarEdge.right: - return Offset(xTravel, yTravel * fraction); - } -} - -double _fractionForAlignedDrag({ - required double cursor, - required double grabOffset, - required double parentExtent, - required double toolbarExtent, - required double left, - required double right, -}) { - final travelExtent = parentExtent - toolbarExtent; - if (travelExtent <= 0) { - return _clampToolbarFraction(0.5, left, right); - } - return _clampToolbarFraction( - (cursor - grabOffset) / travelExtent, left, right); -} - -({double left, double right}) _fractionBoundsForEdge( - _ToolbarEdge edge, - double left, - double right, -) { - return _isHorizontalEdge(edge) - ? (left: left, right: right) - : (left: 0, right: 1); -} - -String _toolbarRawFraction({ - required bool multiEdgeEnabled, - required _ToolbarEdge edge, - required String? savedFraction, - required String? legacyFraction, -}) { - if (!multiEdgeEnabled) { - return (legacyFraction != null && legacyFraction.isNotEmpty) - ? legacyFraction - : '0.5'; - } - if (savedFraction != null && savedFraction.isNotEmpty) { - return savedFraction; - } - if (edge == _ToolbarEdge.top && - legacyFraction != null && - legacyFraction.isNotEmpty) { - return legacyFraction; - } - return '0.5'; -} - -// Returns the alignment for the wrapper Align that positions the entire -// toolbar against the given edge at the given fraction along that edge. -// Alignment uses [-1, 1] coordinates (0 = center). -Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) { - final f = fraction * 2 - 1; - switch (edge) { - case _ToolbarEdge.top: - return Alignment(f, -1); - case _ToolbarEdge.bottom: - return Alignment(f, 1); - case _ToolbarEdge.left: - return Alignment(-1, f); - case _ToolbarEdge.right: - return Alignment(1, f); - } -} - -// The drag handle hangs off the side of the toolbar facing away from the -// docked edge, so the icons themselves sit flush against that edge. -BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) { - const r = Radius.circular(5); - switch (edge) { - case _ToolbarEdge.top: - return const BorderRadius.vertical(bottom: r); - case _ToolbarEdge.bottom: - return const BorderRadius.vertical(top: r); - case _ToolbarEdge.left: - return const BorderRadius.horizontal(right: r); - case _ToolbarEdge.right: - return const BorderRadius.horizontal(left: r); - } -} - -int _monitorMenuQuarterTurns(_ToolbarEdge edge) { - switch (edge) { - case _ToolbarEdge.left: - return 1; - case _ToolbarEdge.right: - return 3; - case _ToolbarEdge.top: - case _ToolbarEdge.bottom: - return 0; - } -} - -IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) { - switch (edge) { - case _ToolbarEdge.top: - return isCollapsed ? Icons.expand_more : Icons.expand_less; - case _ToolbarEdge.bottom: - return isCollapsed ? Icons.expand_less : Icons.expand_more; - case _ToolbarEdge.left: - return isCollapsed ? Icons.chevron_right : Icons.chevron_left; - case _ToolbarEdge.right: - return isCollapsed ? Icons.chevron_left : Icons.chevron_right; - } -} - -class _ToolbarDockingOptions { - _ToolbarDockingOptions({ - required this.edge, - required this.fraction, - required this.multiEdgeEnabled, - }); - - _ToolbarEdge edge; - double fraction; - bool multiEdgeEnabled; -} - -final _toolbarDockingOptionsBySession = {}; - -String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString(); - -_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) => - _toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)]; - -void _cacheToolbarDockingOptions({ - required SessionID sessionId, - required _ToolbarEdge edge, - required double fraction, - required bool multiEdgeEnabled, -}) { - final key = _toolbarDockingCacheKey(sessionId); - final cached = _toolbarDockingOptionsBySession[key]; - if (cached == null) { - _toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions( - edge: edge, - fraction: fraction, - multiEdgeEnabled: multiEdgeEnabled, - ); - return; - } - cached.edge = edge; - cached.fraction = fraction; - cached.multiEdgeEnabled = multiEdgeEnabled; -} - class ToolbarState { late RxBool _pin; @@ -464,26 +250,8 @@ class RemoteToolbar extends StatefulWidget { class _RemoteToolbarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - final _fraction = 0.5.obs; - final _edge = _ToolbarEdge.top.obs; + final _fractionX = 0.5.obs; final _dragging = false.obs; - // Live drag preview: where the toolbar would dock if the user dropped now. - final _previewEdge = Rxn<_ToolbarEdge>(); - final _previewFraction = Rxn(); - // Measured size of the live toolbar, so the preview ghost matches reality - // (collapsed handle vs expanded toolbar). Updated after every layout pass. - final _toolbarSize = Rxn(); - final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root'); - // When false (default), the toolbar stays on the top edge and the drag - // handle just slides it horizontally — preserving long-standing UX while - // still fixing the bug where dragging only moved the handle. When true, - // the user has opted into multi-edge docking with nearest-edge snap. - // Kept in sync after settings-triggered rebuilds. - final _multiEdgeEnabled = false.obs; - final _dockingOptionsInitialized = false.obs; - bool _pendingDockingOptionSync = false; - int _dockingOptionSyncSerial = 0; - int _dragEpoch = 0; int get windowId => stateGlobal.windowId; @@ -505,144 +273,16 @@ class _RemoteToolbarState extends State { void _minimize() async => await WindowController.fromWindowId(windowId).minimize(); - Future _syncDockingOptions({required bool force}) async { - final syncSerial = ++_dockingOptionSyncSerial; - if (_dragging.isTrue) { - _deferDockingOptionsSync(); - return; - } - final dragEpoch = _dragEpoch; - - // Use the canonical helper so the option's documented default semantics - // apply (allow-* prefix => default false). Keeping it raw-string would - // diverge from how _OptionCheckBox displays the same key. - final multiEdgeEnabled = - mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); - final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); - if (cached == null && pi.isSet.isFalse) { - return; - } - final hadDockingOptions = cached != null; - final wasMultiEdgeEnabled = - cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value; - if (!force && - hadDockingOptions && - wasMultiEdgeEnabled == multiEdgeEnabled) { - _pendingDockingOptionSync = false; - return; - } - - final savedFraction = await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction); - // Backward compat: legacy horizontal-only position. - final legacyFraction = await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX); - if (!mounted || syncSerial != _dockingOptionSyncSerial) return; - - var nextEdge = _edge.value; - var savedFractionForNextEdge = savedFraction; - var keepCurrentPosition = false; - if (!multiEdgeEnabled) { - nextEdge = _ToolbarEdge.top; - } else if (force || wasMultiEdgeEnabled || cached == null) { - final edgeStr = await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge); - if (!mounted || syncSerial != _dockingOptionSyncSerial) return; - nextEdge = _parseToolbarEdge(edgeStr); - } else { - // The setting changed from top-only to multi-edge while this toolbar is - // already visible. Keep its current position instead of jumping to the - // last saved multi-edge dock. - nextEdge = cached.edge; - savedFractionForNextEdge = cached.fraction.toString(); - keepCurrentPosition = true; - } - - final rawFraction = _toolbarRawFraction( - multiEdgeEnabled: multiEdgeEnabled, - edge: nextEdge, - savedFraction: savedFractionForNextEdge, - legacyFraction: legacyFraction, - ); - // Clamp to the saved drag-bound contract so a corrupted or out-of-range - // saved value can't bypass it until the user drags again. - final dragLeft = double.tryParse( - bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ?? - 0.0; - final dragRight = double.tryParse( - bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ?? - 1.0; - final fractionBounds = - _fractionBoundsForEdge(nextEdge, dragLeft, dragRight); - final nextFraction = (double.tryParse(rawFraction) ?? 0.5) - .clamp(fractionBounds.left, fractionBounds.right) - .toDouble(); - if (!mounted || syncSerial != _dockingOptionSyncSerial) return; - if (_dragging.isTrue || dragEpoch != _dragEpoch) { - _deferDockingOptionsSync(); - return; - } - _edge.value = nextEdge; - _fraction.value = nextFraction; - _multiEdgeEnabled.value = multiEdgeEnabled; - _dockingOptionsInitialized.value = true; - _cacheToolbarDockingOptions( - sessionId: widget.ffi.sessionId, - edge: nextEdge, - fraction: nextFraction, - multiEdgeEnabled: multiEdgeEnabled, - ); - _pendingDockingOptionSync = false; - if (!multiEdgeEnabled || keepCurrentPosition) { - bind.sessionPeerOption( - sessionId: widget.ffi.sessionId, - name: kOptionRemoteMenubarEdge, - value: _toolbarEdgeToString(nextEdge), - ); - bind.sessionPeerOption( - sessionId: widget.ffi.sessionId, - name: kOptionRemoteMenubarFraction, - value: nextFraction.toString(), - ); - } - } - - void _deferDockingOptionsSync() { - _pendingDockingOptionSync = true; - if (_dragging.isFalse) { - _syncDockingOptionsAfterDragIfNeeded(); - } - } - - void _markToolbarDragEpoch() { - ++_dragEpoch; - } - - void _syncDockingOptionsAfterDragIfNeeded() { - if (!_pendingDockingOptionSync) return; - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: false); - }); - } - @override initState() { super.initState(); - final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); - final multiEdgeEnabled = - mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); - final shouldResetToTop = - cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled; - if (cached != null && !shouldResetToTop) { - _edge.value = cached.edge; - _fraction.value = cached.fraction; - _multiEdgeEnabled.value = multiEdgeEnabled; - _dockingOptionsInitialized.value = true; - } - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: cached == null || shouldResetToTop); + _fractionX.value = double.tryParse(await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, + arg: 'remote-menubar-drag-x') ?? + '0.5') ?? + 0.5; // Initialize toolbar states (collapse, hide) from session options widget.state.init(widget.ffi.sessionId); }); @@ -663,14 +303,6 @@ class _RemoteToolbarState extends State { }); } - @override - void didUpdateWidget(covariant RemoteToolbar oldWidget) { - super.didUpdateWidget(oldWidget); - WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: false); - }); - } - _debouncerHideProc(int v) { if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) { collapse.value = true; @@ -679,130 +311,64 @@ class _RemoteToolbarState extends State { @override dispose() { - ++_dockingOptionSyncSerial; - widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); super.dispose(); + + widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); } @override Widget build(BuildContext context) { return Obx(() { // Wait for initialization to complete to prevent flickering - if (!widget.state.initialized.value || - !_dockingOptionsInitialized.value) { + if (!widget.state.initialized.value) { return const SizedBox.shrink(); } // If toolbar is hidden, return empty widget if (hide.value) { return const SizedBox.shrink(); } - final edge = _edge.value; - final isHorizontal = _isHorizontalEdge(edge); - - // Measure the live toolbar after every layout so the preview ghost can - // match its actual footprint (collapsed handle vs expanded toolbar). - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_dragging.isTrue) return; - final ro = _toolbarKey.currentContext?.findRenderObject(); - if (ro is RenderBox && ro.hasSize) { - final s = ro.size; - if (_toolbarSize.value != s) _toolbarSize.value = s; - } - }); - - final toolbar = Align( - alignment: _alignmentForEdge(edge, _fraction.value), - child: KeyedSubtree( - key: _toolbarKey, - child: collapse.isFalse - ? _buildToolbar(context, edge, isHorizontal) - : _buildDraggableCollapse(context, edge, isHorizontal), - ), - ); - - // Always return the Stack — even when not dragging — so the toolbar's - // position in the Element tree stays stable. Wrapping/unwrapping it - // mid-drag was killing the Draggable's gesture state. - return Stack( - fit: StackFit.expand, - children: [ - IgnorePointer( - child: Obx(() { - final pe = _previewEdge.value; - final pf = _previewFraction.value; - if (!_dragging.isTrue || pe == null || pf == null) { - return const SizedBox.shrink(); - } - return _buildDragPreview(context, pe, pf, _toolbarSize.value); - }), - ), - toolbar, - ], + return Align( + alignment: Alignment.topCenter, + child: collapse.isFalse + ? _buildToolbar(context) + : _buildDraggableCollapse(context), ); }); } - Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge, - double fraction, Size? measured) { - final color = Theme.of(context).colorScheme.primary; - // Use the measured live toolbar size so collapsed vs expanded looks - // right. The current orientation may differ from the preview orientation - // (e.g. dragging a top-docked toolbar toward the left edge), so swap the - // long/short axes when previewing a different orientation. - final previewSize = _toolbarSizeForEdge(edge, measured); - return Align( - alignment: _alignmentForEdge(edge, fraction), - child: Container( - width: previewSize.width, - height: previewSize.height, - decoration: BoxDecoration( - color: color.withOpacity(0.10), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: color.withOpacity(0.55), width: 1.5), - ), - ), - ); - } - - Widget _buildDraggableCollapse( - BuildContext context, _ToolbarEdge edge, bool isHorizontal) { + Widget _buildDraggableCollapse(BuildContext context) { return Obx(() { if (collapse.isFalse && _dragging.isFalse) { triggerAutoHide(); } - final borderRadius = _collapseHandleBorderRadius(edge); - return Offstage( - offstage: _dragging.isTrue, - child: Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, - borderRadius: borderRadius, - child: _DraggableShowHide( - id: widget.id, - sessionId: widget.ffi.sessionId, - dragging: _dragging, - fraction: _fraction, - edge: _edge, - previewEdge: _previewEdge, - previewFraction: _previewFraction, - toolbarSize: _toolbarSize, - markDragEpoch: _markToolbarDragEpoch, - syncDockingOptionsAfterDragIfNeeded: - _syncDockingOptionsAfterDragIfNeeded, - isHorizontal: isHorizontal, - multiEdgeEnabled: _multiEdgeEnabled.value, - toolbarState: widget.state, - setFullscreen: _setFullscreen, - setMinimize: _minimize, + final borderRadius = BorderRadius.vertical( + bottom: Radius.circular(5), + ); + return Align( + alignment: FractionalOffset(_fractionX.value, 0), + child: Offstage( + offstage: _dragging.isTrue, + child: Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, borderRadius: borderRadius, + child: _DraggableShowHide( + id: widget.id, + sessionId: widget.ffi.sessionId, + dragging: _dragging, + fractionX: _fractionX, + toolbarState: widget.state, + setFullscreen: _setFullscreen, + setMinimize: _minimize, + borderRadius: borderRadius, + ), ), ), ); }); } - Widget _buildToolbar( - BuildContext context, _ToolbarEdge edge, bool isHorizontal) { + Widget _buildToolbar(BuildContext context) { final List toolbarItems = []; toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { @@ -810,13 +376,11 @@ class _RemoteToolbarState extends State { } toolbarItems.add(Obx(() { - if ((PrivacyModeState.find(widget.id).isEmpty || - allowDisplaySwitchInPrivacyMode(pi)) && + if (PrivacyModeState.find(widget.id).isEmpty && pi.displaysCount.value > 1) { return _MonitorMenu( id: widget.id, ffi: widget.ffi, - edge: edge, setRemoteState: widget.setRemoteState); } else { return Offstage(); @@ -842,53 +406,37 @@ class _RemoteToolbarState extends State { if (!isWeb) toolbarItems.add(_RecordMenu()); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0)); - // innerAxis: how the toolbar icons themselves flow. - // outerAxis: how the toolbar block and the handle stack against each other - // (perpendicular to the dock edge, so the handle hangs off the interior face). - final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical; - final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal; - final spacer = isHorizontal - ? SizedBox(width: _ToolbarTheme.buttonHMargin * 2) - : SizedBox(height: _ToolbarTheme.buttonHMargin * 2); - final toolbarMaterial = Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, - borderRadius: toolbarBorderRadius, - color: Theme.of(context) - .menuBarTheme - .style - ?.backgroundColor - ?.resolve(MaterialState.values.toSet()), - child: SingleChildScrollView( - scrollDirection: innerAxis, - child: Theme( - data: themeData(), - child: _ToolbarTheme.borderWrapper( - context, - Flex( - direction: innerAxis, - mainAxisSize: MainAxisSize.min, - children: [ - spacer, - ...toolbarItems, - spacer, - ], - ), - toolbarBorderRadius), - ), - ), - ); - final handle = _buildDraggableCollapse(context, edge, isHorizontal); - // The handle hangs off the interior face of the toolbar (away from the - // docked edge), centered along that face by the Flex's default cross-axis - // alignment, so the icons themselves sit flush against the docked edge. - final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left) - ? [toolbarMaterial, handle] - : [handle, toolbarMaterial]; - return Flex( - direction: outerAxis, + return Column( mainAxisSize: MainAxisSize.min, - children: children, + children: [ + Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: toolbarBorderRadius, + color: Theme.of(context) + .menuBarTheme + .style + ?.backgroundColor + ?.resolve(MaterialState.values.toSet()), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Theme( + data: themeData(), + child: _ToolbarTheme.borderWrapper( + context, + Row( + children: [ + SizedBox(width: _ToolbarTheme.buttonHMargin * 2), + ...toolbarItems, + SizedBox(width: _ToolbarTheme.buttonHMargin * 2) + ], + ), + toolbarBorderRadius), + ), + ), + ), + _buildDraggableCollapse(context), + ], ); } @@ -967,13 +515,11 @@ class _MobileActionMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget { final String id; final FFI ffi; - final _ToolbarEdge edge; final Function(VoidCallback) setRemoteState; const _MonitorMenu({ Key? key, required this.id, required this.ffi, - required this.edge, required this.setRemoteState, }) : super(key: key); @@ -984,17 +530,9 @@ class _MonitorMenu extends StatelessWidget { !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay; @override - Widget build(BuildContext context) { - final child = showMonitorsToolbar - ? buildMultiMonitorMenu(context) - : Obx(() => buildMonitorMenu(context)); - final quarterTurns = _monitorMenuQuarterTurns(edge); - if (quarterTurns == 0) return child; - return RotatedBox( - quarterTurns: quarterTurns, - child: child, - ); - } + Widget build(BuildContext context) => showMonitorsToolbar + ? buildMultiMonitorMenu(context) + : Obx(() => buildMonitorMenu(context)); Widget buildMonitorMenu(BuildContext context) { final width = SimpleWrapper(0); @@ -1126,8 +664,7 @@ class _MonitorMenu extends StatelessWidget { } final scale = _ToolbarTheme.buttonSize / rect.height * 0.75; - final height = rect.height * scale; - final startY = (_ToolbarTheme.buttonSize - height) * 0.5; + final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5; final startX = startY; final children = []; @@ -1170,7 +707,7 @@ class _MonitorMenu extends StatelessWidget { width.value = rect.width * scale + startX * 2; return SizedBox( width: width.value, - height: height + startY * 2, + height: rect.height * scale + startY * 2, child: Stack( children: children, ), @@ -1459,10 +996,10 @@ class _DisplayMenuState extends State<_DisplayMenu> { toggles(), ]; // privacy mode - final privacyModeState = PrivacyModeState.find(id); if (ffi.connType == ConnType.defaultConn && - (pi.features.privacyMode || privacyModeState.isNotEmpty) && - (ffiModel.keyboard || privacyModeState.isNotEmpty)) { + ffiModel.keyboard && + pi.features.privacyMode) { + final privacyModeState = PrivacyModeState.find(id); final privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, ffi); if (privacyModeList.length == 1) { @@ -2981,18 +2518,7 @@ class RdoMenuButton extends StatelessWidget { class _DraggableShowHide extends StatefulWidget { final String id; final SessionID sessionId; - final RxDouble fraction; - final Rx<_ToolbarEdge> edge; - final Rxn<_ToolbarEdge> previewEdge; - final Rxn previewFraction; - final Rxn toolbarSize; - final VoidCallback markDragEpoch; - final VoidCallback syncDockingOptionsAfterDragIfNeeded; - final bool isHorizontal; - // Whether multi-edge docking is enabled for this session (toggled in - // Settings -> Other). When false, the drag handle slides the toolbar - // horizontally on the top edge and never switches edges. - final bool multiEdgeEnabled; + final RxDouble fractionX; final RxBool dragging; final ToolbarState toolbarState; final BorderRadius borderRadius; @@ -3004,15 +2530,7 @@ class _DraggableShowHide extends StatefulWidget { Key? key, required this.id, required this.sessionId, - required this.fraction, - required this.edge, - required this.previewEdge, - required this.previewFraction, - required this.toolbarSize, - required this.markDragEpoch, - required this.syncDockingOptionsAfterDragIfNeeded, - required this.isHorizontal, - required this.multiEdgeEnabled, + required this.fractionX, required this.dragging, required this.toolbarState, required this.setFullscreen, @@ -3025,12 +2543,10 @@ class _DraggableShowHide extends StatefulWidget { } class _DraggableShowHideState extends State<_DraggableShowHide> { + Offset position = Offset.zero; + Size size = Size.zero; double left = 0.0; double right = 1.0; - Offset? _lastPointerDown; - Offset? _dragGrabOffset; - double? _dragLongAxisGrabOffset; - Size? _dragToolbarSize; RxBool get collapse => widget.toolbarState.collapse; @@ -3056,174 +2572,41 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { } } - // Bias applied to the currently-previewed edge so a drag hovering between - // two edges doesn't flicker. Only relevant when multi-edge is enabled. - static const double _switchHysteresisPx = 50.0; - - _ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) { - if (!widget.multiEdgeEnabled) return widget.edge.value; - - double rawDist(_ToolbarEdge e) { - switch (e) { - case _ToolbarEdge.top: - return cursor.dy; - case _ToolbarEdge.bottom: - return mediaSize.height - cursor.dy; - case _ToolbarEdge.left: - return cursor.dx; - case _ToolbarEdge.right: - return mediaSize.width - cursor.dx; - } - } - - final previewed = widget.previewEdge.value; - var winner = widget.edge.value; - var best = double.infinity; - for (final e in _ToolbarEdge.values) { - final biased = - e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e); - if (biased < best) { - best = biased; - winner = e; - } - } - return winner; - } - - void _ensureDragGrabOffset(Offset cursor) { - if (_dragGrabOffset != null) return; - final mediaSize = MediaQueryData.fromView(View.of(context)).size; - final toolbarSize = - _toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value); - _dragToolbarSize = toolbarSize; - final toolbarOffset = _toolbarOffsetForEdge( - edge: widget.edge.value, - fraction: widget.fraction.value, - parentSize: mediaSize, - toolbarSize: toolbarSize, - ); - _dragGrabOffset = cursor - toolbarOffset; - _dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value) - ? _dragGrabOffset?.dx - : _dragGrabOffset?.dy; - } - - double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) { - final offset = _dragLongAxisGrabOffset ?? 0; - final extent = - _isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height; - return _clampToolbarFraction(offset, 0, extent); - } - - void _updatePreview(Offset cursor) { - _ensureDragGrabOffset(cursor); - final mediaSize = MediaQueryData.fromView(View.of(context)).size; - final winner = _nearestToolbarEdge(cursor, mediaSize); - widget.previewEdge.value = winner; - - final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize); - final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize); - final double frac; - if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) { - frac = _fractionForAlignedDrag( - cursor: cursor.dx, - grabOffset: grabOffset, - parentExtent: mediaSize.width, - toolbarExtent: toolbarSize.width, - left: left, - right: right, - ); - } else { - final fractionBounds = _fractionBoundsForEdge(winner, left, right); - frac = _fractionForAlignedDrag( - cursor: cursor.dy, - grabOffset: grabOffset, - parentExtent: mediaSize.height, - toolbarExtent: toolbarSize.height, - left: fractionBounds.left, - right: fractionBounds.right, - ); - } - widget.previewFraction.value = frac; - } - - void _resetDragTracking() { - _lastPointerDown = null; - _dragGrabOffset = null; - _dragLongAxisGrabOffset = null; - _dragToolbarSize = null; - } - - void _commitPreview() { - final newEdge = widget.previewEdge.value; - final frac = widget.previewFraction.value; - widget.previewEdge.value = null; - widget.previewFraction.value = null; - widget.dragging.value = false; - widget.markDragEpoch(); - _resetDragTracking(); - widget.syncDockingOptionsAfterDragIfNeeded(); - if (newEdge == null || frac == null) return; - widget.edge.value = newEdge; - widget.fraction.value = frac; - _cacheToolbarDockingOptions( - sessionId: widget.sessionId, - edge: newEdge, - fraction: frac, - multiEdgeEnabled: widget.multiEdgeEnabled, - ); - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: kOptionRemoteMenubarEdge, - value: _toolbarEdgeToString(newEdge), - ); - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: kOptionRemoteMenubarFraction, - value: frac.toString(), - ); - if (widget.multiEdgeEnabled) { - return; - } - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: _legacyRemoteMenubarDragX, - value: frac.toString(), - ); - } - Widget _buildDraggable(BuildContext context) { - return Listener( - onPointerDown: (event) => _lastPointerDown = event.position, - child: Draggable( - // When multi-edge docking is off the toolbar stays on the top edge, - // so lock the feedback to horizontal motion — otherwise the handle - // floats away from the top while dragging and the toolbar looks - // unmoored. When multi-edge is on we need 2D drag for snap-to-edge. - axis: widget.multiEdgeEnabled ? null : Axis.horizontal, - child: Icon( - widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle, - size: 20, - color: MyTheme.color(context).drag_indicator, - ), - feedback: widget, - onDragStarted: () { - widget.markDragEpoch(); - final pointerDown = _lastPointerDown; - if (pointerDown != null) { - _ensureDragGrabOffset(pointerDown); - } - widget.dragging.value = true; - // Seed the preview at the current docked edge/fraction so something - // shows the instant the drag begins, before the first onDragUpdate. - widget.previewEdge.value = widget.edge.value; - widget.previewFraction.value = widget.fraction.value; - }, - onDragUpdate: (details) { - _updatePreview(details.globalPosition); - }, - onDragEnd: (_) => _commitPreview(), + return Draggable( + axis: Axis.horizontal, + child: Icon( + Icons.drag_indicator, + size: 20, + color: MyTheme.color(context).drag_indicator, ), + feedback: widget, + onDragStarted: (() { + final RenderObject? renderObj = context.findRenderObject(); + if (renderObj != null) { + final RenderBox renderBox = renderObj as RenderBox; + size = renderBox.size; + position = renderBox.localToGlobal(Offset.zero); + } + widget.dragging.value = true; + }), + onDragEnd: (details) { + final mediaSize = MediaQueryData.fromView(View.of(context)).size; + widget.fractionX.value += + (details.offset.dx - position.dx) / (mediaSize.width - size.width); + if (widget.fractionX.value < left) { + widget.fractionX.value = left; + } + if (widget.fractionX.value > right) { + widget.fractionX.value = right; + } + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: 'remote-menubar-drag-x', + value: widget.fractionX.value.toString(), + ); + widget.dragging.value = false; + }, ); } @@ -3253,9 +2636,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } - final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical; - final child = Flex( - direction: axis, + final child = Row( mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), @@ -3296,7 +2677,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { message: translate( collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( - _toolbarCollapseIcon(widget.edge.value, collapse.isTrue), + collapse.isFalse ? Icons.expand_less : Icons.expand_more, size: iconSize, ), ))), @@ -3338,8 +2719,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { borderRadius: widget.borderRadius, ), child: SizedBox( - height: widget.isHorizontal ? 20 : null, - width: widget.isHorizontal ? null : 20, + height: 20, child: child, ), ), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9ef7d38d9..ac7d80017 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -99,7 +99,6 @@ class DesktopTabController { /// index, key Function(int, String)? onRemoved; Function(String)? onSelected; - Future Function()? onCloseWindow; DesktopTabController( {required this.tabType, this.onRemoved, this.onSelected}); @@ -593,13 +592,13 @@ class _DesktopTabState extends State } Widget _buildBar() { - final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage(); return Row( children: [ Expanded( child: GestureDetector( // custom double tap handler - onTap: !isIncomingHomePage && showMaximize + onTap: !(bind.isIncomingOnly() && isInHomePage()) && + showMaximize ? () { final current = DateTime.now().millisecondsSinceEpoch; final elapsed = current - _lastClickTime; @@ -610,7 +609,7 @@ class _DesktopTabState extends State .then((value) => stateGlobal.setMaximized(value)); } } - : (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch. + : null, onPanStart: (_) => startDragging(isMainWindow), onPanCancel: () { // We want to disable dragging of the tab area in the tab bar. diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 74a5af45c..9102d163c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -426,10 +426,12 @@ class _RemotePageState extends State with WidgetsBindingObserver { } return Container( color: MyTheme.canvasColor, - child: RawTouchGestureDetectorRegion( - child: getBodyForMobile(), - ffi: gFFI, - ), + child: inputModel.isPhysicalMouse.value + ? getBodyForMobile() + : RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + ), ); }), ), @@ -1183,8 +1185,7 @@ void showOptions( List privacyModeList = []; // privacy mode final privacyModeState = PrivacyModeState.find(id); - if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) || - privacyModeState.isNotEmpty) { + if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) { privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI); if (privacyModeList.length == 1) { displayToggles.add(privacyModeList[0]); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index cd3f97a53..54406ff2e 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -150,8 +150,7 @@ class _DropDownAction extends StatelessWidget { } if (value == kUsePermanentPassword && - (await bind.mainGetCommon(key: "permanent-password-set")) != - "true") { + (await bind.mainGetPermanentPassword()).isEmpty) { if (isChangePermanentPasswordDisabled()) { callback(); return; @@ -583,20 +582,10 @@ class _PermissionCheckerState extends State { Widget build(BuildContext context) { final serverModel = Provider.of(context); final hasAudioPermission = androidVersion >= 30; - final hideStopService = isAndroid && - bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; - final allowPermChangeInAcceptWindow = option2bool( - kOptionEnablePermChangeInAcceptWindow, - bind.mainGetBuildinOption( - key: kOptionEnablePermChangeInAcceptWindow, - )); - final permissionChangeLocked = isAndroid && - serverModel.clients.any((c) => !c.disconnected) && - !allowPermChangeInAcceptWindow; return PaddingCard( title: translate("Permissions"), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - serverModel.mediaOk && !hideStopService + serverModel.mediaOk ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: @@ -606,30 +595,21 @@ class _PermissionCheckerState extends State { label: Text(translate("Stop service"))) .marginOnly(bottom: 8) : SizedBox.shrink(), - if (!hideStopService || !serverModel.mediaOk) - PermissionRow( - translate("Screen Capture"), - serverModel.mediaOk, - !serverModel.mediaOk && - gFFI.userModel.userName.value.isEmpty && - bind.mainGetLocalOption(key: "show-scam-warning") != "N" - ? () => showScamWarning(context, serverModel) - : serverModel.toggleService), PermissionRow( - translate("Input Control"), - serverModel.inputOk, - serverModel.toggleInput, - ), - PermissionRow( - translate("Transfer file"), - serverModel.fileOk, - serverModel.toggleFile, - enabled: !permissionChangeLocked, - ), + translate("Screen Capture"), + serverModel.mediaOk, + !serverModel.mediaOk && + gFFI.userModel.userName.value.isEmpty && + bind.mainGetLocalOption(key: "show-scam-warning") != "N" + ? () => showScamWarning(context, serverModel) + : serverModel.toggleService), + PermissionRow(translate("Input Control"), serverModel.inputOk, + serverModel.toggleInput), + PermissionRow(translate("Transfer file"), serverModel.fileOk, + serverModel.toggleFile), hasAudioPermission ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, - serverModel.toggleAudio, - enabled: !permissionChangeLocked) + serverModel.toggleAudio) : Row(children: [ Icon(Icons.info_outline).marginOnly(right: 15), Expanded( @@ -638,25 +618,19 @@ class _PermissionCheckerState extends State { style: const TextStyle(color: MyTheme.darkGray), )) ]), - PermissionRow( - translate("Enable clipboard"), - serverModel.clipboardOk, - serverModel.toggleClipboard, - enabled: !permissionChangeLocked, - ), + PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, + serverModel.toggleClipboard), ])); } } class PermissionRow extends StatelessWidget { - const PermissionRow(this.name, this.isOk, this.onPressed, - {Key? key, this.enabled = true}) + const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key}) : super(key: key); final String name; final bool isOk; final VoidCallback onPressed; - final bool enabled; @override Widget build(BuildContext context) { @@ -665,11 +639,9 @@ class PermissionRow extends StatelessWidget { contentPadding: EdgeInsets.all(0), title: Text(name), value: isOk, - onChanged: enabled - ? (bool value) { - onPressed(); - } - : null); + onChanged: (bool value) { + onPressed(); + }); } } diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 08c8cda1a..0898125c4 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -259,11 +259,13 @@ class _ViewCameraPageState extends State } return Container( color: MyTheme.canvasColor, - child: RawTouchGestureDetectorRegion( - child: getBodyForMobile(), - ffi: gFFI, - isCamera: true, - ), + child: inputModel.isPhysicalMouse.value + ? getBodyForMobile() + : RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + isCamera: true, + ), ); }), ), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 8b645bb88..f6900e5dd 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -12,6 +12,100 @@ void _showSuccess() { showToast(translate("Successful")); } +void _showError() { + showToast(translate("Error")); +} + +void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { + final pw = await bind.mainGetPermanentPassword(); + final p0 = TextEditingController(text: pw); + final p1 = TextEditingController(text: pw); + var validateLength = false; + var validateSame = false; + dialogManager.show((setState, close, context) { + submit() async { + close(); + dialogManager.showLoading(translate("Waiting")); + if (await gFFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.dismissAll(); + _showSuccess(); + } else { + dialogManager.dismissAll(); + _showError(); + } + } + + return CustomAlertDialog( + title: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('Set your own password')).paddingOnly(left: 10), + ], + ), + content: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + TextFormField( + autofocus: true, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: translate('Password'), + ), + controller: p0, + validator: (v) { + if (v == null) return null; + final val = v.trim().length > 5; + if (validateLength != val) { + // use delay to make setState success + Future.delayed(Duration(microseconds: 1), + () => setState(() => validateLength = val)); + } + return val + ? null + : translate('Too short, at least 6 characters.'); + }, + ).workaroundFreezeLinuxMint(), + TextFormField( + obscureText: true, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + labelText: translate('Confirmation'), + ), + controller: p1, + validator: (v) { + if (v == null) return null; + final val = p0.text == v; + if (validateSame != val) { + Future.delayed(Duration(microseconds: 1), + () => setState(() => validateSame = val)); + } + return val + ? null + : translate('The confirmation is not identical.'); + }, + ).workaroundFreezeLinuxMint(), + ])), + onCancel: close, + onSubmit: (validateLength && validateSame) ? submit : null, + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: (validateLength && validateSame) ? submit : null, + ), + ], + ); + }); +} + void setTemporaryPasswordLengthDialog( OverlayDialogManager dialogManager) async { List lengths = ['6', '8', '10']; diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 001887c0c..81c4dc851 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; @@ -52,9 +53,7 @@ class AbModel { RxBool get currentAbLoading => current.abLoading; bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty; - final _listPullError = ''.obs; - RxString get abPullError => - _listPullError.value.isNotEmpty ? _listPullError : current.pullError; + RxString get currentAbPullError => current.pullError; RxString get currentAbPushError => current.pushError; String? _personalAbGuid; RxBool legacyMode = false.obs; @@ -69,7 +68,6 @@ class AbModel { var _syncFromRecentLock = false; var _timerCounter = 0; var _cacheLoadOnceFlag = false; - var _pulledOnce = false; var listInitialized = false; var _maxPeerOneAb = 0; @@ -99,17 +97,10 @@ class AbModel { print("reset ab model"); addressbooks.clear(); _currentName.value = ''; - _listPullError.value = ''; - _pulledOnce = false; await bind.mainClearAb(); listInitialized = false; } - void clearPullErrors() { - _listPullError.value = ''; - current.pullError.value = ''; - } - // #region ab /// Pulls the address book data from the server. /// @@ -119,41 +110,31 @@ class AbModel { var _pulling = false; Future pullAb( {required ForcePullAb? force, required bool quiet}) async { - if (bind.isDisableAb()) return; - if (!gFFI.userModel.isLogin) return; - if (gFFI.userModel.networkError.isNotEmpty) return; if (_pulling) return; - if (force == null && _pulledOnce) { - return; - } _pulling = true; - if (!quiet) { - _listPullError.value = ''; - current.pullError.value = ''; - } try { await _pullAb(force: force, quiet: quiet); _refreshTab(); } catch (_) {} _pulling = false; - _pulledOnce = true; } Future _pullAb( {required ForcePullAb? force, required bool quiet}) async { + if (bind.isDisableAb()) return; + if (!gFFI.userModel.isLogin) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (force == null && listInitialized && current.initialized) return; debugPrint("pullAb, force: $force, quiet: $quiet"); if (!listInitialized || force == ForcePullAb.listAndCurrent) { try { // Read personal guid every time to avoid upgrading the server without closing the main window _personalAbGuid = null; - // `true`: continue init. `false`: stop, error already recorded. - if (!await _getPersonalAbGuid(quiet: quiet)) { - return; - } + await _getPersonalAbGuid(); + // Determine legacy mode based on whether _personalAbGuid is null legacyMode.value = _personalAbGuid == null; if (!legacyMode.value && _maxPeerOneAb == 0) { - await _getAbSettings(quiet: quiet); + await _getAbSettings(); } if (_personalAbGuid != null) { debugPrint("pull ab list"); @@ -161,7 +142,7 @@ class AbModel { abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName, gFFI.userModel.userName.value, null, ShareRule.read.value, null)); // get all address book name - await _getSharedAbProfiles(abProfiles, quiet: quiet); + await _getSharedAbProfiles(abProfiles); addressbooks.removeWhere((key, value) => abProfiles.firstWhereOrNull((e) => e.name == key) == null); for (int i = 0; i < abProfiles.length; i++) { @@ -201,7 +182,6 @@ class AbModel { } } catch (e) { debugPrint("pull ab list error: $e"); - _setListPullError(e, quiet: quiet); } } else if (listInitialized && (!current.initialized || force == ForcePullAb.current)) { @@ -217,26 +197,14 @@ class AbModel { } } - void _setListPullError(Object err, {required bool quiet, int? statusCode}) { - if (!quiet) { - _listPullError.value = - '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; - } - if (statusCode == 401) { - gFFI.userModel.reset(resetOther: true); - } - } - - Future _getAbSettings({required bool quiet}) async { - int? statusCode; + Future _getAbSettings() async { try { final api = "${await bind.mainGetApiServer()}/api/ab/settings"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); - statusCode = resp.statusCode; - if (statusCode == 404) { + if (resp.statusCode == 404) { debugPrint("HTTP 404, api server doesn't support shared address book"); return false; } @@ -245,57 +213,46 @@ class AbModel { if (json.containsKey('error')) { throw json['error']; } - if (statusCode != 200) { - throw 'HTTP $statusCode'; + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; } _maxPeerOneAb = json['max_peer_one_ab'] ?? 0; return true; } catch (err) { debugPrint('get ab settings err: ${err.toString()}'); - _setListPullError(err, quiet: quiet, statusCode: statusCode); } return false; } - /// Loads `/api/ab/personal`. - /// Returns `true` to continue init, `false` to stop after a real error. - Future _getPersonalAbGuid({required bool quiet}) async { - int? statusCode; + Future _getPersonalAbGuid() async { try { final api = "${await bind.mainGetApiServer()}/api/ab/personal"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); - statusCode = resp.statusCode; - if (statusCode == 404) { + if (resp.statusCode == 404) { debugPrint("HTTP 404, current api server is legacy mode"); - // Old server: keep `_personalAbGuid` null and continue in legacy mode. - return true; + return false; } Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } - if (statusCode != 200) { - throw 'HTTP $statusCode'; + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; } _personalAbGuid = json['guid']; - // New server: guid is available, continue in non-legacy mode. return true; } catch (err) { debugPrint('get personal ab err: ${err.toString()}'); - _setListPullError(err, quiet: quiet, statusCode: statusCode); } - // Real error: stop the current pull. return false; } - Future _getSharedAbProfiles(List profiles, - {required bool quiet}) async { + Future _getSharedAbProfiles(List profiles) async { final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles"; - int? statusCode; try { var uri0 = Uri.parse(api); final pageSize = 100; @@ -316,19 +273,13 @@ class AbModel { headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(uri, headers: headers); - statusCode = resp.statusCode; - if (statusCode == 404) { - debugPrint( - "HTTP 404, api server doesn't support shared address book"); - return false; - } Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } - if (statusCode != 200) { - throw 'HTTP $statusCode'; + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; } if (json.containsKey('total')) { if (total == 0) total = json['total']; @@ -351,7 +302,6 @@ class AbModel { return true; } catch (err) { debugPrint('_getSharedAbProfiles err: ${err.toString()}'); - _setListPullError(err, quiet: quiet, statusCode: statusCode); } return false; } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 7d91b03b3..35001cbf2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -391,30 +391,14 @@ class FileController { await Future.delayed(Duration(milliseconds: 100)); - final savedDir = (await bind.sessionGetPeerOption( + final dir = (await bind.sessionGetPeerOption( sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir")); - Future tryOpenReadyDirs() async { - final dirs = { - if (directory.value.path.isNotEmpty) directory.value.path, - if (savedDir.isNotEmpty) savedDir, - options.value.home, - }; - for (final dir in dirs) { - if (await _openDirectoryPath(dir, isBack: true)) { - return true; - } - } - return false; - } - - var opened = await tryOpenReadyDirs(); + openDirectory(dir.isEmpty ? options.value.home : dir); await Future.delayed(Duration(seconds: 1)); - if (!opened) { - // The peer may become ready during the reconnect delay, so retry the - // same candidates instead of only retrying the default home directory. - await tryOpenReadyDirs(); + if (directory.value.path.isEmpty) { + openDirectory(options.value.home); } } @@ -445,23 +429,19 @@ class FileController { }); } - Future refresh() async { - // "." can be both a refresh command and a real remote directory path. - // Refresh must bypass openDirectory's command dispatch to avoid recursion. - return await _openDirectoryPath(directory.value.path, isBack: true); + Future refresh() async { + await openDirectory(directory.value.path); } - Future openDirectory(String path, {bool isBack = false}) async { - if (!isBack && path == ".") { - return await refresh(); + Future openDirectory(String path, {bool isBack = false}) async { + if (path == ".") { + refresh(); + return; } - if (!isBack && path == "..") { - return await _goToParentDirectory(isBack: isBack); + if (path == "..") { + goToParentDirectory(); + return; } - return await _openDirectoryPath(path, isBack: isBack); - } - - Future _openDirectoryPath(String path, {bool isBack = false}) async { if (!isBack) { pushHistory(); } @@ -478,10 +458,8 @@ class FileController { final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden); fd.format(isWindows, sort: sortBy.value); directory.value = fd; - return true; } catch (e) { debugPrint("Failed to openDirectory $path: $e"); - return false; } } @@ -509,22 +487,19 @@ class FileController { goBack(); return; } - unawaited(_openDirectoryPath(path, isBack: true).then((_) {})); + openDirectory(path, isBack: true); } void goToParentDirectory() { - unawaited(_goToParentDirectory().then((_) {})); - } - - Future _goToParentDirectory({bool isBack = false}) async { final isWindows = options.value.isWindows; final dirPath = directory.value.path; var parent = PathUtil.dirname(dirPath, isWindows); // specially for C:\, D:\, goto '/' if (parent == dirPath && isWindows) { - return await _openDirectoryPath('/', isBack: isBack); + openDirectory('/'); + return; } - return await _openDirectoryPath(parent, isBack: isBack); + openDirectory(parent); } // TODO deprecated this diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index d55cff453..c6ba992d2 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -343,7 +343,6 @@ class GroupModel { } reset() async { - initialized = false; groupLoadError.value = ''; deviceGroups.clear(); users.clear(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 984d6a25c..675a95e42 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -16,13 +15,12 @@ import 'package:get/get.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../models/state_model.dart'; -import 'input_modifier_utils.dart'; import 'relative_mouse_model.dart'; import '../common.dart'; import '../consts.dart'; /// Mouse button enum. -enum MouseButtons { left, right, wheel, back, forward } +enum MouseButtons { left, right, wheel, back } const _kMouseEventDown = 'mousedown'; const _kMouseEventUp = 'mouseup'; @@ -159,8 +157,6 @@ extension ToString on MouseButtons { return 'wheel'; case MouseButtons.back: return 'back'; - case MouseButtons.forward: - return 'forward'; } } } @@ -331,80 +327,6 @@ class ToReleaseKeys { } class InputModel { - // Side mouse button support for Linux. - // Flutter's Linux embedder drops X11 button 8/9 events, so we capture them - // natively via GDK and forward through the platform channel. - static InputModel? _activeSideButtonModel; - // Tracks per-button which model received a side button down event, so the - // matching up event is routed there even if the pointer has left the view - // or a different button was pressed in between. - static final Map _sideButtonDownModels = {}; - static bool _sideButtonChannelInitialized = false; - - /// Each Flutter engine (main window + sub-windows from desktop_multi_window) - /// runs its own Dart isolate with its own statics. Called from initEnv() - /// which runs per-engine, so each isolate registers its own handler tied - /// to its own set of InputModels. - static void initSideButtonChannel() { - if (!isLinux) return; - if (_sideButtonChannelInitialized) return; - _sideButtonChannelInitialized = true; - - const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons'); - channel.setMethodCallHandler((call) async { - if (call.method == 'onSideMouseButton') { - final args = call.arguments as Map; - final button = args['button'] as String; - final type = args['type'] as String; - final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward; - - if (type == 'down') { - final model = _activeSideButtonModel; - if (model != null && - !(model.isViewOnly && !model.showMyCursor) && - model.keyboardPerm && - !model.isViewCamera) { - _sideButtonDownModels[mb] = model; - // Fire-and-forget to avoid blocking the platform channel handler. - unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) { - debugPrint('[InputModel] failed to send side button $type for $mb: $e'); - })); - } - } else { - // Only route 'up' when we recorded the matching 'down'; - // dropping avoids sending unpaired 'up' to an unrelated session. - // Use _sendMouseUnchecked to bypass permission checks so the - // release always goes through even if permissions changed. - final model = _sideButtonDownModels.remove(mb); - if (model != null) { - unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) { - debugPrint('[InputModel] failed to send side button $type for $mb: $e'); - })); - } - } - } - return null; - }); - } - - /// Clear any static references to this model (prevents stale routing). - /// Releases any held side buttons on the peer so closing a session - /// mid-press does not leave a stuck button. - void disposeSideButtonTracking() { - if (_activeSideButtonModel == this) _activeSideButtonModel = null; - final held = _sideButtonDownModels.entries - .where((e) => e.value == this) - .map((e) => e.key) - .toList(); - for (final mb in held) { - _sideButtonDownModels.remove(mb); - // Best-effort release; session may already be tearing down. - unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) { - debugPrint('[InputModel] failed to release side button $mb: $e'); - })); - } - } - final WeakReference parent; String keyboardMode = ''; @@ -490,7 +412,6 @@ class InputModel { bool get isRelativeMouseModeSupported => _relativeMouse.isSupported; InputModel(this.parent) { - initSideButtonChannel(); sessionId = parent.target!.sessionId; _relativeMouse = RelativeMouseModel( sessionId: sessionId, @@ -699,38 +620,6 @@ class InputModel { } } - // Safe: this only re-dispatches synthesized Shift key-up events. - // The key-up path clears the tracked Shift state so this does not loop. - void _releaseTrackedShiftKeyEventIfNeeded() { - final leftShift = toReleaseKeys.lastLShiftKeyEvent; - final rightShift = toReleaseKeys.lastRShiftKeyEvent; - if (leftShift != null) { - handleKeyEvent(leftShift); - } - if (rightShift != null) { - handleKeyEvent(rightShift); - } - } - - // Safe: this only re-dispatches synthesized Shift key-up events. - // The raw key-up path clears the tracked Shift state so this does not loop. - void _releaseTrackedRawShiftKeyEventIfNeeded() { - final leftShift = toReleaseRawKeys.lastLShiftKeyEvent; - final rightShift = toReleaseRawKeys.lastRShiftKeyEvent; - if (leftShift != null) { - handleRawKeyEvent(RawKeyUpEvent( - data: leftShift.data, - character: leftShift.character, - )); - } - if (rightShift != null) { - handleRawKeyEvent(RawKeyUpEvent( - data: rightShift.data, - character: rightShift.character, - )); - } - } - KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; if (isViewCamera) return KeyEventResult.handled; @@ -785,27 +674,6 @@ class InputModel { toReleaseRawKeys.updateKeyUp(key, e); } - // On some mobile soft-keyboard paths, Flutter may leave cached Shift state - // set even though the current raw key event is not shifted anymore. - if (e is RawKeyDownEvent && - shouldReleaseStaleMobileShift( - isMobile: isMobile, - cachedShiftPressed: shift, - actualShiftPressed: e.isShiftPressed, - logicalKey: e.logicalKey, - hasTrackedShiftKeyDown: toReleaseRawKeys.lastLShiftKeyEvent != null || - toReleaseRawKeys.lastRShiftKeyEvent != null, - )) { - if (kDebugMode) { - debugPrint( - 'input: releasing stale mobile Shift before replaying tracked raw ' - 'key-up (logicalKey=${e.logicalKey.keyLabel}, ' - 'actualShiftPressed=${e.isShiftPressed}, cachedShiftPressed=$shift)', - ); - } - _releaseTrackedRawShiftKeyEventIfNeeded(); - } - // * Currently mobile does not enable map mode if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { mapKeyboardModeRaw(e, iosCapsLock); @@ -849,8 +717,6 @@ class InputModel { iosCapsLock = _getIosCapsFromCharacter(e); } - // Update cached modifier state before sending the event. The stale mobile - // Shift release check below relies on this cached state. if (e is KeyUpEvent) { handleKeyUpEventModifiers(e); } else if (e is KeyDownEvent) { @@ -888,21 +754,6 @@ class InputModel { } } } - - // On some mobile soft-keyboard paths, Flutter may leave cached Shift state - // set even though the current key event is not shifted anymore. - if (e is KeyDownEvent && - shouldReleaseStaleMobileShift( - isMobile: isMobile, - cachedShiftPressed: shift, - actualShiftPressed: HardwareKeyboard.instance.isShiftPressed, - logicalKey: e.logicalKey, - hasTrackedShiftKeyDown: toReleaseKeys.lastLShiftKeyEvent != null || - toReleaseKeys.lastRShiftKeyEvent != null, - )) { - _releaseTrackedShiftKeyEventIfNeeded(); - } - final isDesktopAndMapMode = isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode); if (isMobileAndMapMode || isDesktopAndMapMode) { @@ -1115,20 +966,13 @@ class InputModel { return evt; } - /// Send mouse event unconditionally (no permission checks). - /// Used for side button releases that must go through even if permissions - /// changed after the matching down was sent. - Future _sendMouseUnchecked(String type, MouseButtons button) async { - await bind.sessionSendMouse( - sessionId: sessionId, - msg: json.encode(modify({'type': type, 'buttons': button.value}))); - } - /// Send mouse press event. Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; if (isViewCamera) return; - await _sendMouseUnchecked(type, button); + await bind.sessionSendMouse( + sessionId: sessionId, + msg: json.encode(modify({'type': type, 'buttons': button.value}))); } void enterOrLeave(bool enter) { @@ -1138,13 +982,6 @@ class InputModel { _pointerInsideImage = enter; _lastWheelTsUs = 0; - // Track active model for side button events (Linux). - if (enter) { - _activeSideButtonModel = this; - } else if (_activeSideButtonModel == this) { - _activeSideButtonModel = null; - } - // Fix status if (!enter) { resetModifiers(); @@ -1495,16 +1332,6 @@ class InputModel { return false; } - /// iOS may emit a synthesized touch event after a real mouse click. - /// This helper ignores touch-down events that arrive shortly after a mouse down, - /// even when the position is far (e.g., near the top edge). - bool _shouldIgnoreTouchAfterMouse(int nowMs) { - if (!isIOS) return false; - const int kTouchAfterMouseWindowMs = 700; - final dt = nowMs - _lastMouseDownTimeMs; - return dt >= 0 && dt < kTouchAfterMouseWindowMs; - } - void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage ${e.kind}"); _stopFling = true; @@ -1517,9 +1344,6 @@ class InputModel { // Track mouse down events for duplicate detection on iOS. final nowMs = DateTime.now().millisecondsSinceEpoch; if (e.kind == ui.PointerDeviceKind.mouse) { - if (!isPhysicalMouse.value) { - isPhysicalMouse.value = true; - } _lastMouseDownTimeMs = nowMs; _lastMouseDownPos = e.position; } @@ -1529,10 +1353,6 @@ class InputModel { } if (e.kind != ui.PointerDeviceKind.mouse) { - // Ignore duplicate touch events that follow a recent mouse click (iOS Magic Mouse issue). - if (isPhysicalMouse.value && _shouldIgnoreTouchAfterMouse(nowMs)) { - return; - } if (isPhysicalMouse.value) { isPhysicalMouse.value = false; } diff --git a/flutter/lib/models/input_modifier_utils.dart b/flutter/lib/models/input_modifier_utils.dart deleted file mode 100644 index e65c32790..000000000 --- a/flutter/lib/models/input_modifier_utils.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/services.dart'; - -/// Returns true when a stale mobile one-shot Shift state should be released -/// by replaying a tracked Shift key-down as a synthesized key-up. -/// -/// This is only valid on mobile when Flutter's cached Shift state is still on -/// (`cachedShiftPressed == true`) but the current hardware/raw event reports -/// Shift as off (`actualShiftPressed == false`). -/// -/// A tracked Shift key-down is required so the caller can safely synthesize the -/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the -/// Shift key event itself must be processed first; otherwise we could release -/// the tracked key while still handling the original Shift press/release. -/// Callers should evaluate this only after their cached modifier state has been -/// updated for the current event. -/// -/// When this returns true, the caller logs a line like: -/// `input: releasing stale mobile Shift before replaying tracked raw key-up` -/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`. -bool shouldReleaseStaleMobileShift({ - required bool isMobile, - required bool cachedShiftPressed, - required bool actualShiftPressed, - required LogicalKeyboardKey logicalKey, - required bool hasTrackedShiftKeyDown, -}) { - if (!isMobile || !cachedShiftPressed || actualShiftPressed) { - return false; - } - if (!hasTrackedShiftKeyDown) { - return false; - } - if (logicalKey == LogicalKeyboardKey.shiftLeft || - logicalKey == LogicalKeyboardKey.shiftRight) { - return false; - } - return true; -} diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e94834a2b..4533f11fa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -3932,7 +3932,6 @@ class FFI { inputModel.resetModifiers(); // Dispose relative mouse mode resources to ensure cursor is restored inputModel.disposeRelativeMouseMode(); - inputModel.disposeSideButtonTracking(); if (closeSession) { await bind.sessionClose(sessionId: sessionId); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 40c94fcf5..5892ed0fe 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier { } toggleAudio() async { - if (clients.any((c) => !c.disconnected)) { + if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); } if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { @@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier { } toggleFile() async { - if (clients.any((c) => !c.disconnected)) { + if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); } if (!_fileOk && @@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier { } toggleInput() async { - if (clients.any((c) => !c.disconnected)) { + if (clients.isNotEmpty) { await showClientsMayNotBeChangedAlert(parent.target); } if (_inputOk) { @@ -471,6 +471,17 @@ class ServerModel with ChangeNotifier { WakelockManager.disable(_wakelockKey); } + Future setPermanentPassword(String newPW) async { + await bind.mainSetPermanentPassword(password: newPW); + await Future.delayed(Duration(milliseconds: 500)); + final pw = await bind.mainGetPermanentPassword(); + if (newPW == pw) { + return true; + } else { + return false; + } + } + fetchID() async { final id = await bind.mainGetMyId(); if (id != _serverId.id) { @@ -549,19 +560,10 @@ class ServerModel with ChangeNotifier { if (index < 0) { _clients.add(client); } else { - if (_clients[index].authorized) { - _clients[index].privacyMode = client.privacyMode; - notifyListeners(); - return; - } _clients[index].authorized = true; - _clients[index].privacyMode = client.privacyMode; } } else { - final index = _clients.indexWhere((c) => c.id == client.id); - if (index >= 0) { - _clients[index].privacyMode = client.privacyMode; - notifyListeners(); + if (_clients.any((c) => c.id == client.id)) { return; } _clients.add(client); @@ -827,7 +829,6 @@ class Client { bool restart = false; bool recording = false; bool blockInput = false; - bool privacyMode = false; bool disconnected = false; bool fromSwitch = false; bool inVoiceCall = false; @@ -856,7 +857,6 @@ class Client { restart = json['restart']; recording = json['recording']; blockInput = json['block_input']; - privacyMode = json['privacy_mode'] ?? privacyMode; disconnected = json['disconnected']; fromSwitch = json['from_switch']; inVoiceCall = json['in_voice_call']; @@ -881,7 +881,6 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['block_input'] = blockInput; - data['privacy_mode'] = privacyMode; data['disconnected'] = disconnected; data['from_switch'] = fromSwitch; data['in_voice_call'] = inVoiceCall; diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 8961d2dd8..a74241ccb 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -27,30 +27,25 @@ class TerminalModel with ChangeNotifier { // Buffer for output data received before terminal view has valid dimensions. // This prevents NaN errors when writing to terminal before layout is complete. final _pendingOutputChunks = []; - final _pendingOutputSuppressFlags = []; int _pendingOutputSize = 0; static const int _kMaxOutputBufferChars = 8 * 1024; // View ready state: true when terminal has valid dimensions, safe to write bool _terminalViewReady = false; - bool _markViewReadyScheduled = false; - bool _suppressTerminalOutput = false; - bool _suppressNextTerminalDataOutput = false; + + bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows; void Function(int w, int h, int pw, int ph)? onResizeExternal; Future _handleInput(String data) async { - // Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a - // real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'. - // - Peer Windows: '\r' works, '\n' is just a newline. - // - Peer Linux: canonical-mode shells accept both, but raw-mode apps - // (readline, prompt_toolkit, vim, TUI frameworks) expect '\r'. - // - Peer macOS: same as Linux, raw-mode apps expect '\r' - // (https://github.com/rustdesk/rustdesk/issues/14907). - // So on mobile / web-mobile, always normalize a lone '\n' to '\r'. - // We deliberately do not touch multi-character payloads (e.g. pasted text) - // so embedded newlines in pasted content are preserved. + // If we press the `Enter` button on Android, + // `data` can be '\r' or '\n' when using different keyboards. + // Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline. + // Android -> Linux. Both '\r' and '\n' work as expected (execute a command). + // So when we receive '\n', we may need to convert it to '\r' to ensure compatibility. + // Desktop -> Desktop works fine. + // Check if we are on mobile or web(mobile), and convert '\n' to '\r'. final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop)); - if (isMobileOrWebMobile && data == '\n') { + if (isMobileOrWebMobile && isPeerWindows && data == '\n') { data = '\r'; } if (_terminalOpened) { @@ -75,10 +70,7 @@ class TerminalModel with ChangeNotifier { terminalController = TerminalController(); // Setup terminal callbacks - terminal.onOutput = (data) { - if (_suppressTerminalOutput) return; - _handleInput(data); - }; + terminal.onOutput = _handleInput; terminal.onResize = (w, h, pw, ph) async { // Validate all dimensions before using them @@ -92,7 +84,7 @@ class TerminalModel with ChangeNotifier { // Mark terminal view as ready and flush any buffered output on first valid resize. // Must be after onResizeExternal so the view layer has valid dimensions before flushing. if (!_terminalViewReady) { - _scheduleMarkViewReady(); + _markViewReady(); } if (_terminalOpened) { @@ -118,16 +110,14 @@ class TerminalModel with ChangeNotifier { void onReady() { parent.dialogManager.dismissAll(); - // Fire and forget - don't block onReady. If the transport reconnects while - // this model is still open, re-send OpenTerminal so the remote service marks - // the persistent session active again and resumes output streaming. - openTerminal(force: _terminalOpened).catchError((e) { + // Fire and forget - don't block onReady + openTerminal().catchError((e) { debugPrint('[TerminalModel] Error opening terminal: $e'); }); } - Future openTerminal({bool force = false}) async { - if (_terminalOpened && !force) return; + Future openTerminal() async { + if (_terminalOpened) return; // Request the remote side to open a terminal with default shell // The remote side will decide which shell to use based on its OS @@ -285,12 +275,9 @@ class TerminalModel with ChangeNotifier { if (success) { _terminalOpened = true; - // On reconnect, the server may replay recent output. That replay can include - // terminal queries like DSR/DA; xterm answers them through onOutput as - // "^[[1;1R^[[2;2R^[[>0;0;0c", which must not be sent back to the peer. - final replayTerminalOutput = evt['replay_terminal_output']; - _suppressNextTerminalDataOutput = replayTerminalOutput == true || - message == 'Reconnected to existing terminal with pending output'; + // On reconnect ("Reconnected to existing terminal"), server may replay recent output. + // If this TerminalView instance is reused (not rebuilt), duplicate lines can appear. + // We intentionally accept this tradeoff for now to keep logic simple. // Fallback: if terminal view is not yet ready but already has valid // dimensions (e.g. layout completed before open response arrived), @@ -298,7 +285,7 @@ class TerminalModel with ChangeNotifier { if (!_terminalViewReady && terminal.viewWidth > 0 && terminal.viewHeight > 0) { - _scheduleMarkViewReady(); + _markViewReady(); } // Process any buffered input @@ -310,16 +297,12 @@ class TerminalModel with ChangeNotifier { }); final persistentSessions = - (evt['persistent_sessions'] as List? ?? []) - .whereType() - .where((id) => !parent.terminalModels.containsKey(id)) - .toList(); + evt['persistent_sessions'] as List? ?? []; if (kWindowId != null && persistentSessions.isNotEmpty) { DesktopMultiWindow.invokeMethod( kWindowId!, kWindowEventRestoreTerminalSessions, jsonEncode({ - 'peer_id': id, 'persistent_sessions': persistentSessions, })); } @@ -349,8 +332,6 @@ class TerminalModel with ChangeNotifier { final data = evt['data']; if (data != null) { - final suppressTerminalOutput = _suppressNextTerminalDataOutput; - _suppressNextTerminalDataOutput = false; try { String text = ''; if (data is String) { @@ -370,7 +351,7 @@ class TerminalModel with ChangeNotifier { return; } - _writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput); + _writeToTerminal(text); } catch (e) { debugPrint('[TerminalModel] Failed to process terminal data: $e'); } @@ -380,10 +361,7 @@ class TerminalModel with ChangeNotifier { /// Write text to terminal, buffering if the view is not yet ready. /// All terminal output should go through this method to avoid NaN errors /// from writing before the terminal view has valid layout dimensions. - void _writeToTerminal( - String text, { - bool suppressTerminalOutput = false, - }) { + void _writeToTerminal(String text) { if (!_terminalViewReady) { // If a single chunk exceeds the cap, keep only its tail. // Note: truncation may split a multi-byte ANSI escape sequence, @@ -395,73 +373,34 @@ class TerminalModel with ChangeNotifier { _pendingOutputChunks ..clear() ..add(truncated); - _pendingOutputSuppressFlags - ..clear() - ..add(suppressTerminalOutput); _pendingOutputSize = truncated.length; } else { _pendingOutputChunks.add(text); - _pendingOutputSuppressFlags.add(suppressTerminalOutput); _pendingOutputSize += text.length; // Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences) while (_pendingOutputSize > _kMaxOutputBufferChars && _pendingOutputChunks.length > 1) { final removed = _pendingOutputChunks.removeAt(0); - _pendingOutputSuppressFlags.removeAt(0); _pendingOutputSize -= removed.length; } } return; } - _writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput); + terminal.write(text); } void _flushOutputBuffer() { if (_pendingOutputChunks.isEmpty) return; debugPrint( '[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)'); - for (var i = 0; i < _pendingOutputChunks.length; i++) { - _writeTerminalChunk( - _pendingOutputChunks[i], - suppressTerminalOutput: _pendingOutputSuppressFlags[i], - ); + for (final chunk in _pendingOutputChunks) { + terminal.write(chunk); } _pendingOutputChunks.clear(); - _pendingOutputSuppressFlags.clear(); _pendingOutputSize = 0; } - void _writeTerminalChunk( - String text, { - required bool suppressTerminalOutput, - }) { - if (!suppressTerminalOutput) { - terminal.write(text); - return; - } - final previous = _suppressTerminalOutput; - _suppressTerminalOutput = true; - try { - terminal.write(text); - } finally { - _suppressTerminalOutput = previous; - } - } - /// Mark terminal view as ready and flush buffered output. - void _scheduleMarkViewReady() { - if (_disposed || _terminalViewReady || _markViewReadyScheduled) return; - _markViewReadyScheduled = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - _markViewReadyScheduled = false; - if (_disposed || _terminalViewReady) return; - if (terminal.viewWidth > 0 && terminal.viewHeight > 0) { - _markViewReady(); - } - }); - WidgetsBinding.instance.ensureVisualUpdate(); - } - void _markViewReady() { if (_terminalViewReady) return; _terminalViewReady = true; @@ -487,10 +426,7 @@ class TerminalModel with ChangeNotifier { // Clear buffers to free memory _inputBuffer.clear(); _pendingOutputChunks.clear(); - _pendingOutputSuppressFlags.clear(); _pendingOutputSize = 0; - _markViewReadyScheduled = false; - _suppressNextTerminalDataOutput = false; // Terminal cleanup is handled server-side when service closes super.dispose(); } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 54e6a9a9b..66191d004 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1159,6 +1159,10 @@ class RustdeskImpl { return Future.value(''); } + Future mainGetPermanentPassword({dynamic hint}) { + return Future.value(''); + } + Future mainGetFingerprint({dynamic hint}) { return Future.value(''); } @@ -1342,9 +1346,9 @@ class RustdeskImpl { throw UnimplementedError("mainUpdateTemporaryPassword"); } - Future mainSetPermanentPasswordWithResult( + Future mainSetPermanentPassword( {required String password, dynamic hint}) { - throw UnimplementedError("mainSetPermanentPasswordWithResult"); + throw UnimplementedError("mainSetPermanentPassword"); } Future mainCheckSuperUserPermission({dynamic hint}) { @@ -1538,10 +1542,7 @@ class RustdeskImpl { Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { - // Safari only allows auth popups while handling the original user gesture. - // Use Future.sync so the JS call runs synchronously (pre-opening the OIDC - // window) while any interop error still surfaces as a Future error. - return Future.sync(() => js.context.callMethod('setByName', [ + return Future(() => js.context.callMethod('setByName', [ 'account_auth', jsonEncode({'op': op, 'remember': rememberMe}) ])); @@ -1729,7 +1730,7 @@ class RustdeskImpl { } String mainSupportedPrivacyModeImpls({dynamic hint}) { - return '[]'; + throw UnimplementedError("mainSupportedPrivacyModeImpls"); } String mainSupportedInputSource({dynamic hint}) { diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 210adba96..a05bb7856 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -29,80 +29,6 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); extern bool gIsConnectionManager; -// --- Side mouse button support (back/forward) --- -// Flutter's Linux embedder doesn't deliver X11 button 8/9 events to Dart. -// We intercept them via GDK and forward through a dedicated platform channel. - -static const char* kSideButtonChannelName = "org.rustdesk.rustdesk/side_buttons"; - -static gboolean on_side_button_event(GtkWidget* widget, GdkEventButton* event, gpointer user_data) { - if (event->button != 8 && event->button != 9) { - return FALSE; - } - // Ignore GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS (double/triple-click synthetic - // events) - only handle real press and release. - if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) { - return FALSE; - } - - FlMethodChannel* channel = FL_METHOD_CHANNEL(user_data); - if (channel == NULL) return FALSE; - - g_autoptr(FlValue) args = fl_value_new_map(); - fl_value_set_string_take(args, "button", - fl_value_new_string(event->button == 8 ? "back" : "forward")); - fl_value_set_string_take(args, "type", - fl_value_new_string(event->type == GDK_BUTTON_PRESS ? "down" : "up")); - - fl_method_channel_invoke_method(channel, "onSideMouseButton", args, - NULL, NULL, NULL); - - return TRUE; -} - -static FlMethodChannel* side_buttons_create_channel(FlEngine* engine) { - g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); - return fl_method_channel_new( - fl_engine_get_binary_messenger(engine), - kSideButtonChannelName, - FL_METHOD_CODEC(codec)); -} - -static void side_buttons_channel_destroy(gpointer data) { - g_object_unref(data); -} - -static void side_buttons_init_for_window(GtkWindow* window, FlMethodChannel* channel) { - // Guard against double-initialization (would leave dangling signal user_data). - if (g_object_get_data(G_OBJECT(window), "side-buttons-channel") != NULL) return; - - gtk_widget_add_events(GTK_WIDGET(window), - GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); - // Store channel on the window so it stays alive and is freed with the window. - g_object_set_data_full(G_OBJECT(window), "side-buttons-channel", - g_object_ref(channel), side_buttons_channel_destroy); - g_signal_connect(window, "button-press-event", - G_CALLBACK(on_side_button_event), channel); - g_signal_connect(window, "button-release-event", - G_CALLBACK(on_side_button_event), channel); -} - -static void on_subwindow_created(FlPluginRegistry* registry) { -#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) - wayland_shortcuts_inhibit_init_for_subwindow(registry); -#endif - // Set up side button forwarding for sub-windows. - if (registry == NULL || !FL_IS_VIEW(registry)) return; - FlView* view = FL_VIEW(registry); - GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view)); - if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) { - FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view)); - if (channel == NULL) return; - side_buttons_init_for_window(GTK_WINDOW(toplevel), channel); - g_object_unref(channel); // window now owns a ref via g_object_set_data_full - } -} - GtkWidget *find_gl_area(GtkWidget *widget); // Implements GApplication::activate. @@ -170,12 +96,12 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(view)); - // Register callback for sub-windows created by desktop_multi_window plugin. - // Handles both Wayland shortcuts inhibition (guarded inside) and side button - // forwarding. Safe to call on X11-only builds - the plugin just stores the - // callback pointer regardless of windowing system. +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + // Register callback for sub-windows created by desktop_multi_window plugin + // Only sub-windows (remote windows) need keyboard shortcuts inhibition desktop_multi_window_plugin_set_window_created_callback( - (WindowCreatedCallback)on_subwindow_created); + (WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow); +#endif fl_register_plugins(FL_PLUGIN_REGISTRY(view)); @@ -190,11 +116,6 @@ static void my_application_activate(GApplication* application) { self, nullptr); - // Forward side mouse button events (back/forward) to Dart on the main window. - FlMethodChannel* side_channel = side_buttons_create_channel(fl_view_get_engine(view)); - side_buttons_init_for_window(window, side_channel); - g_object_unref(side_channel); - gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index eddf5a19d..eb6d76161 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -113,8 +113,8 @@ dependencies: dev_dependencies: icons_launcher: ^2.0.4 - flutter_test: - sdk: flutter + #flutter_test: + #sdk: flutter build_runner: ^2.4.6 freezed: ^2.4.2 flutter_lints: ^2.0.2 diff --git a/flutter/test/input_modifier_utils_test.dart b/flutter/test/input_modifier_utils_test.dart deleted file mode 100644 index 2e1971753..000000000 --- a/flutter/test/input_modifier_utils_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_hbb/models/input_modifier_utils.dart'; - -void main() { - group('shouldReleaseStaleMobileShift', () { - test('does not release when cached shift is already false', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: false, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.keyD, - hasTrackedShiftKeyDown: true, - ), - isFalse, - ); - }); - - test('releases one-shot mobile shift after a text key', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.keyD, - hasTrackedShiftKeyDown: true, - ), - isTrue, - ); - }); - - test('does not release manually toggled shift without tracked key down', - () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.keyD, - hasTrackedShiftKeyDown: false, - ), - isFalse, - ); - }); - - test('does not release when shift is still physically pressed', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: true, - logicalKey: LogicalKeyboardKey.keyD, - hasTrackedShiftKeyDown: true, - ), - isFalse, - ); - }); - - test('does not release on non-mobile platforms', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: false, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.keyD, - hasTrackedShiftKeyDown: true, - ), - isFalse, - ); - }); - - test('releases on enter key', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.enter, - hasTrackedShiftKeyDown: true, - ), - isTrue, - ); - }); - - test('releases on arrow key', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.arrowLeft, - hasTrackedShiftKeyDown: true, - ), - isTrue, - ); - }); - - test('does not release on modifier events', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.shiftLeft, - hasTrackedShiftKeyDown: true, - ), - isFalse, - ); - }); - - test('does not release on shiftRight modifier events', () { - expect( - shouldReleaseStaleMobileShift( - isMobile: true, - cachedShiftPressed: true, - actualShiftPressed: false, - logicalKey: LogicalKeyboardKey.shiftRight, - hasTrackedShiftKeyDown: true, - ), - isFalse, - ); - }); - }); -} diff --git a/libs/clipboard/src/platform/unix/fuse/cs.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs index fa1dea71d..0f1cf8739 100644 --- a/libs/clipboard/src/platform/unix/fuse/cs.rs +++ b/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -12,7 +12,7 @@ //! //! For now, we transfer all file names with windows separators, UTF-16 encoded. //! *Need a way to transfer file names with '\' safely*. -//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes. +//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes. //! //! # Note //! - all files on FS should be read only, and mark the owner to be the current user diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 95d1d1a5c..e1856863e 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -624,7 +624,6 @@ void CliprdrStream_Delete(CliprdrStream *instance) if (instance) { free(instance->iStream.lpVtbl); - instance->iStream.lpVtbl = NULL; free(instance); } } @@ -2161,7 +2160,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi return FALSE; /* add to name array */ - clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR)); + clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2); if (!clipboard->file_names[clipboard->nFiles]) return FALSE; diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 7796904f9..26d090855 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -8,7 +8,6 @@ use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::libc::c_int; -use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay}; use libxdo_sys::{self, xdo_t, CURRENTWINDOW}; use std::{borrow::Cow, ffi::CString}; @@ -33,51 +32,6 @@ fn mousebutton(button: MouseButton) -> c_int { } } -/// Minimum number of buttons the X11 core pointer must support. -/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons. -const MIN_POINTER_BUTTONS: usize = 9; - -/// Check that the X11 core pointer's button map includes at least 9 buttons -/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9). -/// -/// RustDesk's uinput "Mouse passthrough" device normally provides enough -/// buttons, but we log a warning if the map is too small so the issue is -/// diagnosable. `XSetPointerMapping` cannot extend the button count (its -/// length must match `XGetPointerMapping`), so we only diagnose here. -fn check_x11_button_map() { - // Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings - // on pure Wayland or headless environments without $DISPLAY. - if std::env::var_os("DISPLAY").is_none() { - return; - } - - let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) }; - if display.is_null() { - log::warn!("XOpenDisplay failed, cannot check button map"); - return; - } - - let mut current_map = [0u8; 32]; - let nbuttons = - unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) }; - unsafe { XCloseDisplay(display) }; - - if nbuttons < 0 { - log::warn!("XGetPointerMapping failed (returned {nbuttons})"); - return; - } - - let nbuttons = nbuttons as usize; - if nbuttons >= MIN_POINTER_BUTTONS { - log::info!("X11 pointer has {nbuttons} buttons, side buttons supported"); - } else { - log::warn!( - "X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \ - back/forward side buttons may not work until a device with more buttons is added" - ); - } -} - /// The main struct for handling the event emitting pub(super) struct EnigoXdo { xdo: *mut xdo_t, @@ -98,7 +52,6 @@ impl Default for EnigoXdo { log::warn!("Failed to create xdo context, xdo functions will be disabled"); } else { log::info!("xdo context created successfully"); - check_x11_button_map(); } Self { xdo, diff --git a/libs/hbb_common b/libs/hbb_common index 9043c15ac..48c37de3e 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0 +Subproject commit 48c37de3e6c4e399af6f51ca20e8e3e1fd037976 diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 8ec5e6b8f..bd3eace7b 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -151,7 +151,7 @@ fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option PixelProvider<'a> { } pub trait Recorder { - fn capture(&mut self, timeout_ms: u64) -> Result, Box>; + fn capture(&mut self, timeout_ms: u64) -> Result>; } pub trait BoxCloneCapturable { diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 8859d0d3b..d29677c7a 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -276,21 +276,12 @@ impl PipeWireRecorder { // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 src.set_property("always-copy", &true)?; - // COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink. - // xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream - // format set is too narrow (appsink only accepts BGRx/RGBx), producing - // "no more output formats" / not-negotiated (-4). videoconvert accepts any - // system-memory video/x-raw format, widening negotiation so the portal can - // settle on a format it can deliver via its SHM path. - let convert = gst::ElementFactory::make("videoconvert", None)?; - let sink = gst::ElementFactory::make("appsink", None)?; sink.set_property("drop", &true)?; sink.set_property("max-buffers", &1u32)?; - pipeline.add_many(&[&src, &convert, &sink])?; - src.link(&convert)?; - convert.link(&sink)?; + pipeline.add_many(&[&src, &sink])?; + src.link(&sink)?; let appsink = sink .dynamic_cast::() @@ -355,7 +346,7 @@ impl PipeWireRecorder { } impl Recorder for PipeWireRecorder { - fn capture(&mut self, timeout_ms: u64) -> Result, Box> { + fn capture(&mut self, timeout_ms: u64) -> Result> { if let Some(sample) = self .appsink .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) diff --git a/libs/virtual_display/dylib/README.md b/libs/virtual_display/dylib/README.md index fb71c3c56..30fa588f1 100644 --- a/libs/virtual_display/dylib/README.md +++ b/libs/virtual_display/dylib/README.md @@ -29,4 +29,4 @@ TODO ## X11 -## macOS +## OSX diff --git a/res/audits.py b/res/audits.py old mode 100755 new mode 100644 index 77b0fd120..d843233da --- a/res/audits.py +++ b/res/audits.py @@ -43,7 +43,7 @@ def get_connection_type_name(conn_type): """Convert connection type number to readable name""" type_map = { 0: "Remote Desktop", - 1: "File Transfer", + 1: "File Transfer", 2: "Port Transfer", 3: "View Camera", 4: "Terminal" @@ -55,7 +55,7 @@ def get_console_type_name(console_type): """Convert console audit type number to readable name""" type_map = { 0: "Group Management", - 1: "User Management", + 1: "User Management", 2: "Device Management", 3: "Address Book Management" } @@ -67,7 +67,7 @@ def get_console_operation_name(operation_code): operation_map = { 0: "User Login", 1: "Add Group", - 2: "Add User", + 2: "Add User", 3: "Add Device", 4: "Delete Groups", 5: "Disconnect Device", @@ -95,7 +95,7 @@ def get_console_operation_name(operation_code): def get_alarm_type_name(alarm_type): """Convert alarm type number to readable name""" type_map = { - 0: "Access attempt outside the IP whitelist", + 0: "Access attempt outside the IP whiltelist", 1: "Over 30 consecutive access attempts", 2: "Multiple access attempts within one minute", 3: "Over 30 consecutive login attempts", @@ -109,24 +109,24 @@ def enhance_audit_data(data, audit_type): """Enhance audit data with readable formats""" if not data: return data - + enhanced_data = [] for item in data: enhanced_item = item.copy() - + # Convert timestamps - replace original values if 'created_at' in enhanced_item: enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at']) if 'end_time' in enhanced_item: enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time']) - + # Add type-specific enhancements - replace original values if audit_type == 'conn': if 'conn_type' in enhanced_item: enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type']) else: enhanced_item['conn_type'] = "Not Logged In" - + elif audit_type == 'console': if 'typ' in enhanced_item: # Replace typ field with type and convert to readable name @@ -136,14 +136,14 @@ def enhance_audit_data(data, audit_type): # Replace iop field with operation and convert to readable name enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop']) del enhanced_item['iop'] - + elif audit_type == 'alarm' and 'typ' in enhanced_item: # Replace typ field with type and convert to readable name enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ']) del enhanced_item['typ'] - + enhanced_data.append(enhanced_item) - + return enhanced_data @@ -152,7 +152,7 @@ def check_response(response): if response.status_code != 200: print(f"Error: HTTP {response.status_code} - {response.text}") exit(1) - + try: response_json = response.json() if "error" in response_json: @@ -163,28 +163,28 @@ def check_response(response): return response.text or "Success" -def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None, +def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None, created_at=None, days_ago=None, non_wildcard_fields=None): """Common function for viewing audits""" headers = {"Authorization": f"Bearer {token}"} - + # Set default page size and current page if page_size is None: page_size = 10 if current is None: current = 1 - + params = { "pageSize": page_size, "current": current } - + # Add filter parameters if provided if filters: for key, value in filters.items(): if value is not None: params[key] = value - + # Handle time filters if days_ago is not None: # Calculate datetime from days ago @@ -205,10 +205,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre # Apply wildcard patterns for string fields (excluding specific fields) if non_wildcard_fields is None: non_wildcard_fields = set() - + # Always exclude these fields from wildcard treatment non_wildcard_fields.update(["created_at", "pageSize", "current"]) - + string_params = {} for k, v in params.items(): if isinstance(v, str) and k not in non_wildcard_fields: @@ -221,10 +221,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params) response_json = check_response(response) - + # Enhance the data with readable formats data = enhance_audit_data(response_json.get("data", []), endpoint) - + return { "data": data, "total": response_json.get("total", 0), @@ -233,7 +233,7 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre } -def view_conn_audits(url, token, remote=None, conn_type=None, +def view_conn_audits(url, token, remote=None, conn_type=None, page_size=None, current=None, created_at=None, days_ago=None): """View connection audits""" filters = { @@ -241,7 +241,7 @@ def view_conn_audits(url, token, remote=None, conn_type=None, "conn_type": conn_type } non_wildcard_fields = {"conn_type"} - + return view_audits_common( url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields ) @@ -254,7 +254,7 @@ def view_file_audits(url, token, remote=None, "remote": remote } non_wildcard_fields = set() - + return view_audits_common( url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields ) @@ -267,7 +267,7 @@ def view_alarm_audits(url, token, device=None, "device": device } non_wildcard_fields = set() - + return view_audits_common( url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields ) @@ -280,7 +280,7 @@ def view_console_audits(url, token, operator=None, "operator": operator } non_wildcard_fields = set() - + return view_audits_common( url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields ) @@ -295,15 +295,15 @@ def main(): ) parser.add_argument("--url", required=True, help="URL of the API") parser.add_argument("--token", required=True, help="Bearer token for authentication") - + # Pagination parameters parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)") parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)") - + # Time filtering parameters parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)") parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)") - + # Audit filters (simplified) parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)") parser.add_argument("--device", help="Device ID filter (for alarm audits)") @@ -319,9 +319,9 @@ def main(): if args.command == "view-conn": # View connection audits result = view_conn_audits( - args.url, - args.token, - args.remote, + args.url, + args.token, + args.remote, args.conn_type, args.page_size, args.current, @@ -329,12 +329,12 @@ def main(): args.days_ago ) print(json.dumps(result, indent=2)) - + elif args.command == "view-file": # View file audits result = view_file_audits( - args.url, - args.token, + args.url, + args.token, args.remote, args.page_size, args.current, @@ -342,12 +342,12 @@ def main(): args.days_ago ) print(json.dumps(result, indent=2)) - + elif args.command == "view-alarm": # View alarm audits result = view_alarm_audits( - args.url, - args.token, + args.url, + args.token, args.device, args.page_size, args.current, @@ -355,12 +355,12 @@ def main(): args.days_ago ) print(json.dumps(result, indent=2)) - + elif args.command == "view-console": # View console audits result = view_console_audits( - args.url, - args.token, + args.url, + args.token, args.operator, args.page_size, args.current, diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index f4780dd87..f21cc7ee1 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -31,17 +31,17 @@ LExit: return WcaFinalize(er); } -// Helper function to safely delete a file using handle-based deletion. -// Directories are refused after opening the handle. +// Helper function to safely delete a file or directory using handle-based deletion. +// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions. BOOL SafeDeleteItem(LPCWSTR fullPath) { - // Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT + // Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT // to prevent following symlinks. // Use shared access to allow deletion even when other processes have the file open. DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT; HANDLE hFile = CreateFileW( fullPath, - DELETE | FILE_READ_ATTRIBUTES, + DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access NULL, OPEN_EXISTING, @@ -55,21 +55,6 @@ BOOL SafeDeleteItem(LPCWSTR fullPath) return FALSE; } - BY_HANDLE_FILE_INFORMATION fileInfo; - if (FALSE == GetFileInformationByHandle(hFile, &fileInfo)) - { - WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError()); - CloseHandle(hFile); - return FALSE; - } - - if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath); - CloseHandle(hFile); - return FALSE; - } - // Use SetFileInformationByHandle to mark for deletion. // The file will be deleted when the handle is closed. FILE_DISPOSITION_INFO dispInfo; @@ -92,74 +77,98 @@ BOOL SafeDeleteItem(LPCWSTR fullPath) return result; } -BOOL PathEndsWithSlash(LPCWSTR path) +// Helper function to recursively delete a directory's contents with detailed logging. +void RecursiveDelete(LPCWSTR path) { - size_t length = 0; - HRESULT hr = StringCchLengthW(path, MAX_PATH, &length); - if (FAILED(hr) || length == 0) - { - return FALSE; - } - - WCHAR last = path[length - 1]; - return last == L'\\' || last == L'/'; -} - -void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes) -{ - if (!(attributes & FILE_ATTRIBUTE_READONLY)) + // Ensure the path is not empty or null. + if (path == NULL || path[0] == L'\0') { return; } - DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY; - if (writableAttributes == 0) + // Extra safety: never operate directly on a root path. + if (PathIsRootW(path)) { - writableAttributes = FILE_ATTRIBUTE_NORMAL; - } - - if (SetFileAttributesW(fullPath, writableAttributes)) - { - WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath); + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path); return; } - WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError()); -} - -BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName) -{ - WCHAR fullPath[MAX_PATH]; - LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\"; - HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName); - if (FAILED(hr)) - { - WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName); - return FALSE; + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR searchPath[MAX_PATH]; + HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path); + return; } - DWORD attributes = GetFileAttributesW(fullPath); - if (attributes == INVALID_FILE_ATTRIBUTES) + WIN32_FIND_DATAW findData; + HANDLE hFind = FindFirstFileW(searchPath, &findData); + + if (hFind == INVALID_HANDLE_VALUE) { - DWORD error = GetLastError(); - if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) + // This can happen if the directory is empty or doesn't exist, which is not an error in our case. + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError()); + return; + } + + do + { + // Skip '.' and '..' directories. + if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0) { - return TRUE; + continue; } - WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error); - return FALSE; - } + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR fullPath[MAX_PATH]; + hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path); + continue; + } - if (attributes & FILE_ATTRIBUTE_DIRECTORY) + // Before acting, ensure the read-only attribute is not set. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) + { + if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError()); + } + } + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + // Check for reparse points (symlinks/junctions) to prevent directory traversal attacks. + // Do not follow reparse points, only remove the link itself. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath); + SafeDeleteItem(fullPath); + } + else + { + // Recursively delete directory contents first + RecursiveDelete(fullPath); + // Then delete the directory itself + SafeDeleteItem(fullPath); + } + } + else + { + // Delete file using safe handle-based deletion + SafeDeleteItem(fullPath); + } + } while (FindNextFileW(hFind, &findData) != 0); + + DWORD lastError = GetLastError(); + if (lastError != ERROR_NO_MORE_FILES) { - WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath); - return FALSE; + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError); } - ClearReadOnlyAttribute(fullPath, attributes); - WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath); - return SafeDeleteItem(fullPath); + FindClose(hFind); } // See `Package.wxs` for the sequence of this custom action. @@ -169,13 +178,13 @@ BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName) // 2. RemoveExistingProducts // ├─ TerminateProcesses // ├─ TryStopDeleteService -// ├─ RemoveRuntimeGeneratedFiles - <-- Here +// ├─ RemoveInstallFolder - <-- Here // └─ RemoveFiles // 3. InstallValidate // 4. InstallFiles // 5. InstallExecute // 6. InstallFinalize -UINT __stdcall RemoveRuntimeGeneratedFiles( +UINT __stdcall RemoveInstallFolder( __in MSIHANDLE hInstall) { HRESULT hr = S_OK; @@ -185,7 +194,7 @@ UINT __stdcall RemoveRuntimeGeneratedFiles( LPWSTR pwz = NULL; LPWSTR pwzData = NULL; - hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles"); + hr = WcaInitialize(hInstall, "RemoveInstallFolder"); ExitOnFailure(hr, "Failed to initialize"); hr = WcaGetProperty(L"CustomActionData", &pwzData); @@ -193,20 +202,24 @@ UINT __stdcall RemoveRuntimeGeneratedFiles( pwz = pwzData; hr = WcaReadStringFromCaData(&pwz, &installFolder); - ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); if (installFolder == NULL || installFolder[0] == L'\0') { - WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup."); + WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete."); goto LExit; } if (PathIsRootW(installFolder)) { - WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder); + WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder); goto LExit; } - WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder); - DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe"); + WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder); + + RecursiveDelete(installFolder); + + // The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories. + // We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer. LExit: ReleaseStr(pwzData); @@ -603,10 +616,10 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall) } if (IsServiceRunningW(svcName)) { - WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stopped after 1000 ms.", svcName); + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stoped after 1000 ms.", svcName); } else { - WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped.", svcName); + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stoped.", svcName); } if (MyDeleteServiceW(svcName)) { @@ -632,7 +645,7 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall) } // It's really strange that we need sleep here. - // But the upgrading may be stuck at "copying new files" because the file is in using. + // But the upgrading may be stucked at "copying new files" because the file is in using. // Steps to reproduce: Install -> stop service in tray --> start service -> upgrade // Sleep(300); @@ -745,7 +758,7 @@ UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall) } // Why RegSetValueExW always return 998? - // + // result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL); if (result != ERROR_SUCCESS) { WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result); @@ -861,7 +874,7 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc i = 0; j = 0; // svcBinary is a string with double quotes, we need to escape it for shell arguments. - // It is original used for `CreateServiceW`. + // It is orignal used for `CreateServiceW`. // eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service while (true) { if (svcBinary[j] == L'"') { diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def index d50fbf59b..01b03490c 100644 --- a/res/msi/CustomActions/CustomActions.def +++ b/res/msi/CustomActions/CustomActions.def @@ -2,7 +2,7 @@ LIBRARY "CustomActions" EXPORTS CustomActionHello - RemoveRuntimeGeneratedFiles + RemoveInstallFolder TerminateProcesses AddFirewallRules SetPropertyIsServiceRunning diff --git a/res/msi/Package/Components/Folders.wxs b/res/msi/Package/Components/Folders.wxs index 6911600e9..de9edb7f3 100644 --- a/res/msi/Package/Components/Folders.wxs +++ b/res/msi/Package/Components/Folders.wxs @@ -16,15 +16,8 @@ - - - - - - - - - + + diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index 952172bdc..337e84ec3 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -12,7 +12,7 @@ - + @@ -77,21 +77,21 @@ - - - + + + - + - + - + - + diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs index 3a9811eb8..3727c0dd3 100644 --- a/res/msi/Package/Fragments/CustomActions.wxs +++ b/res/msi/Package/Fragments/CustomActions.wxs @@ -5,7 +5,7 @@ - + diff --git a/res/msi/Package/UI/MyInstallDlg.wxs b/res/msi/Package/UI/MyInstallDlg.wxs index 06c37097c..bf59d569c 100644 --- a/res/msi/Package/UI/MyInstallDlg.wxs +++ b/res/msi/Package/UI/MyInstallDlg.wxs @@ -23,13 +23,12 @@ Patch dialog sequence: --> - - + @@ -65,16 +64,9 @@ Patch dialog sequence: - - - - - - - - - - + + + diff --git a/src/cli.rs b/src/cli.rs index 2f3b3550f..f61bfe92f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,13 +25,7 @@ impl Session { pub fn new(id: &str, sender: mpsc::UnboundedSender) -> Self { let mut password = "".to_owned(); if PeerConfig::load(id).password.is_empty() { - match rpassword::prompt_password("Enter password: ") { - Ok(p) => password = p, - Err(e) => { - log::error!("Failed to read password: {:?}", e); - password = "".to_owned(); - } - } + password = rpassword::prompt_password("Enter password: ").unwrap(); } let session = Self { id: id.to_owned(), diff --git a/src/client.rs b/src/client.rs index 321a49ee6..8ea70898f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -119,13 +119,10 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; pub const LOGIN_MSG_OFFLINE: &str = "Offline"; pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported"; #[cfg(target_os = "linux")] -pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required"; +pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; #[cfg(target_os = "linux")] pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = - "wayland-requires-higher-linux-version"; -#[cfg(target_os = "linux")] -pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str = - "xdp-portal-unavailable"; + "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; @@ -1745,9 +1742,6 @@ pub struct LoginConfigHandler { pub direct: Option, pub received: bool, switch_uuid: Option, - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - switch_back_allowed: bool, pub save_ab_password_to_recent: bool, // true: connected with ab password pub other_server: Option<(String, String, String)>, pub custom_fps: Arc>>, @@ -1864,11 +1858,6 @@ impl LoginConfigHandler { self.direct = None; self.received = false; - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - self.switch_back_allowed = false; - } self.switch_uuid = switch_uuid; self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; @@ -1882,23 +1871,6 @@ impl LoginConfigHandler { self.is_terminal_admin = is_terminal_admin; } - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn allow_switch_back_once(&mut self) { - self.switch_back_allowed = true; - } - - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn consume_switch_back_permission(&mut self) -> bool { - if self.switch_back_allowed { - self.switch_back_allowed = false; - true - } else { - false - } - } - /// Check if the client should auto login. /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { @@ -3402,36 +3374,6 @@ pub fn handle_login_error( } } -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool { - let Ok(mut conn) = crate::ipc::connect(1000, "").await else { - return false; - }; - let uuid = uuid.to_string(); - if conn - .send(&crate::ipc::Data::SwitchSidesUuid( - uuid.clone(), - id.to_owned(), - None, - )) - .await - .is_err() - { - return false; - } - match conn.next_timeout(1000).await { - Ok(Some(crate::ipc::Data::SwitchSidesUuid( - returned_uuid, - returned_id, - Some(true), - ))) => { - returned_uuid == uuid && returned_id == id - } - _ => false, - } -} - /// Handle hash message sent by peer. /// Hash will be used for login. /// @@ -3452,22 +3394,12 @@ pub async fn handle_hash( // Take care of password application order // switch_uuid - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let uuid = lc.write().unwrap().switch_uuid.take(); - if let Some(uuid) = uuid { - if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { - let id = lc.read().unwrap().id.clone(); - if !consume_local_switch_sides_uuid(&id, &uuid).await { - log::warn!("Ignored untrusted switch_uuid"); - } else { - lc.write().unwrap().allow_switch_back_once(); - send_switch_login_request(lc.clone(), peer, uuid).await; - lc.write().unwrap().password_source = Default::default(); - return; - } - } + let uuid = lc.write().unwrap().switch_uuid.take(); + if let Some(uuid) = uuid { + if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { + send_switch_login_request(lc.clone(), peer, uuid).await; + lc.write().unwrap().password_source = Default::default(); + return; } } // last password @@ -3935,7 +3867,6 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("resolve") && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("restricted") && !text.to_lowercase().contains("not allowed"))) } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5eb7a273a..e0b3fcd6d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -586,6 +586,7 @@ impl Remote { file_num, include_hidden, is_remote, + Vec::new(), od, )); allow_err!( @@ -658,6 +659,7 @@ impl Remote { file_num, include_hidden, is_remote, + Vec::new(), od, ); job.is_last_job = true; @@ -843,7 +845,19 @@ impl Remote { } } Data::CancelJob(id) => { - self.cancel_transfer_job(id, peer).await; + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id: id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { + job.remove_download_file(); + } + let _ = fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); } Data::RemoveDir((id, path)) => { let mut msg_out = Message::new(); @@ -1039,22 +1053,6 @@ impl Remote { } } - async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { - job.remove_download_file(); - } - let _ = fs::remove_job(id, &mut self.read_jobs); - self.remove_jobs.remove(&id); - } - pub async fn sync_jobs_status_to_local(&mut self) -> bool { if !self.is_connected { return false; @@ -1448,23 +1446,6 @@ impl Remote { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(_mcb.clipboards, ClipboardSide::Client); - #[cfg(target_os = "ios")] - { - if let Some(cb) = _mcb - .clipboards - .iter() - .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) - { - let content = if cb.compress { - hbb_common::compress::decompress(&cb.content) - } else { - cb.content.to_vec() - }; - if let Ok(content) = String::from_utf8(content) { - self.handler.clipboard(content); - } - } - } #[cfg(target_os = "android")] crate::clipboard::handle_msg_multi_clipboards(_mcb); } @@ -1489,43 +1470,14 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - // We cannot call cancel_transfer_job/handle_job_status while holding - // a mutable borrow from fs::get_job(&mut self.write_jobs), so defer - // the error handling until after the borrow scope ends. - let mut set_files_err = None; + self.handler + .update_folder_files(fd.id, &entries, fd.path, false, false); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); - if let Err(err) = job.set_files(entries) { - set_files_err = Some(err.to_string()); - } else { - job.set_finished_size_on_resume(); - self.handler.update_folder_files( - fd.id, - job.files(), - fd.path, - false, - false, - ); - } + job.set_files(entries); + job.set_finished_size_on_resume(); } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { - // Intentionally keep raw entries here: - // - remote remove flow executes deletions on peer side; - // - local remove flow is populated from local get_recursive_files(). job.files = entries; - self.handler - .update_folder_files(fd.id, &job.files, fd.path, false, false); - } else { - self.handler - .update_folder_files(fd.id, &entries, fd.path, false, false); - } - if let Some(err) = set_files_err { - log::warn!( - "Rejected unsafe file list from remote peer for job {}: {}", - fd.id, - err - ); - self.cancel_transfer_job(fd.id, peer).await; - self.handle_job_status(fd.id, -1, Some(err)); } } Some(file_response::Union::Digest(digest)) => { @@ -1797,9 +1749,6 @@ impl Remote { Ok(Permission::BlockInput) => { self.handler.set_permission("block_input", p.enabled); } - Ok(Permission::PrivacyMode) => { - self.handler.set_permission("privacy_mode", p.enabled); - } _ => {} } } @@ -1923,23 +1872,9 @@ impl Remote { ); } } - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::SwitchBack(_)) => { - let allow_switch_back = self - .handler - .lc - .write() - .unwrap() - .consume_switch_back_permission(); - if allow_switch_back { - self.handler.switch_back(&self.handler.get_id()); - } else { - log::warn!( - "Ignored unsolicited SwitchBack from {}", - self.handler.get_id() - ); - } + #[cfg(feature = "flutter")] + self.handler.switch_back(&self.handler.get_id()); } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/common.rs b/src/common.rs index 69e3ec304..3e23770c6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -39,7 +39,7 @@ use hbb_common::{ use crate::{ hbbs_http::{create_http_client_async, get_url_for_tls}, - ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option}, + ui_interface::{get_option, is_installed, set_option}, }; #[derive(Debug, Eq, PartialEq)] @@ -1086,7 +1086,6 @@ fn get_api_server_(api: String, custom: String) -> String { #[inline] pub fn is_public(url: &str) -> bool { - let url = url.to_ascii_lowercase(); url.contains("rustdesk.com/") || url.ends_with("rustdesk.com") } @@ -1124,286 +1123,22 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String { format!("{}/api/audit/{}", url, typ) } -/// Check if we should use raw TCP proxy for API calls. -/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off, -/// and the target URL belongs to the configured non-public API host. -#[inline] -fn should_use_raw_tcp_for_api(url: &str) -> bool { - get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y" - && !use_ws() - && is_tcp_proxy_api_target(url) -} - -/// Check if we can attempt raw TCP proxy fallback for this target URL. -#[inline] -fn can_fallback_to_raw_tcp(url: &str) -> bool { - !use_ws() && is_tcp_proxy_api_target(url) -} - -#[inline] -fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool { - if api_url.is_empty() || is_public(api_url) { - return false; - } - - let target_host = url::Url::parse(url) - .ok() - .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase())); - let api_host = url::Url::parse(api_url) - .ok() - .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase())); - - matches!((target_host, api_host), (Some(target), Some(api)) if target == api) -} - -#[inline] -fn is_tcp_proxy_api_target(url: &str) -> bool { - should_use_tcp_proxy_for_api_url(url, &ui_get_api_server()) -} - -fn tcp_proxy_log_target(url: &str) -> String { - url::Url::parse(url) - .ok() - .map(|parsed| { - let mut redacted = format!("{}://", parsed.scheme()); - let Some(host) = parsed.host() else { - return "".to_owned(); - }; - redacted.push_str(&host.to_string()); - if let Some(port) = parsed.port() { - redacted.push(':'); - redacted.push_str(&port.to_string()); - } - redacted.push_str(parsed.path()); - redacted - }) - .unwrap_or_else(|| "".to_owned()) -} - -#[inline] -fn get_tcp_proxy_addr() -> String { - check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT) -} - -/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf. -/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`, -/// receives `HttpProxyResponse`. -/// -/// The entire operation (connect + handshake + send + receive) is wrapped in -/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at -/// any stage cannot block the caller indefinitely. -async fn tcp_proxy_request( - method: &str, - url: &str, - body: &[u8], - headers: Vec, -) -> ResultType { - let tcp_addr = get_tcp_proxy_addr(); - if tcp_addr.is_empty() { - bail!("No rendezvous server configured for TCP proxy"); - } - - let parsed = url::Url::parse(url)?; - let path = if let Some(query) = parsed.query() { - format!("{}?{}", parsed.path(), query) - } else { - parsed.path().to_string() - }; - - log::debug!( - "Sending {} {} via TCP proxy to {}", - method, - parsed.path(), - tcp_addr - ); - - let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT; - timeout(overall_timeout, async { - let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?; - let key = crate::get_key(true).await; - secure_tcp_silent(&mut conn, &key).await?; - - let mut req = HttpProxyRequest::new(); - req.method = method.to_uppercase(); - req.path = path; - req.headers = headers.into(); - req.body = Bytes::from(body.to_vec()); - - let mut msg_out = RendezvousMessage::new(); - msg_out.set_http_proxy_request(req); - conn.send(&msg_out).await?; - - match conn.next().await { - Some(Ok(bytes)) => { - let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?; - match msg_in.union { - Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp), - _ => bail!("Unexpected response from TCP proxy"), - } - } - Some(Err(e)) => bail!("TCP proxy read error: {}", e), - None => bail!("TCP proxy connection closed without response"), - } - }) - .await? -} - -/// Build HeaderEntry list from "Key: Value" style header string (used by post_request). -/// If the caller supplies a Content-Type header it overrides the default `application/json`. -fn parse_simple_header(header: &str) -> Vec { - let mut entries = Vec::new(); - let mut has_content_type = false; - if !header.is_empty() { - let tmp: Vec<&str> = header.splitn(2, ": ").collect(); - if tmp.len() == 2 { - if tmp[0].eq_ignore_ascii_case("Content-Type") { - has_content_type = true; - } - entries.push(HeaderEntry { - name: tmp[0].into(), - value: tmp[1].into(), - ..Default::default() - }); - } - } - if !has_content_type { - entries.insert( - 0, - HeaderEntry { - name: "Content-Type".into(), - value: "application/json".into(), - ..Default::default() - }, - ); - } - entries -} - -/// POST request via TCP proxy. -async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType { - let headers = parse_simple_header(header); - let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?; - if !resp.error.is_empty() { - bail!("TCP proxy error: {}", resp.error); - } - Ok(String::from_utf8_lossy(&resp.body).to_string()) -} - -fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType { - if !resp.error.is_empty() { - bail!("TCP proxy error: {}", resp.error); - } - - let mut response_headers = Map::new(); - for entry in resp.headers.iter() { - response_headers.insert(entry.name.to_lowercase(), json!(entry.value)); - } - - let mut result = Map::new(); - result.insert("status_code".to_string(), json!(resp.status)); - result.insert("headers".to_string(), Value::Object(response_headers)); - result.insert( - "body".to_string(), - json!(String::from_utf8_lossy(&resp.body)), - ); - - serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e)) -} - -fn parse_json_header_entries(header: &str) -> ResultType> { - let v: Value = serde_json::from_str(header)?; - if let Value::Object(obj) = v { - Ok(obj - .iter() - .map(|(key, value)| HeaderEntry { - name: key.clone(), - value: value.as_str().unwrap_or_default().into(), - ..Default::default() - }) - .collect()) - } else { - Err(anyhow!("HTTP header information parsing failed!")) - } -} - -/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback. -async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> { +pub async fn post_request(url: String, body: String, header: &str) -> ResultType { let proxy_conf = Config::get_socks(); - let tls_url = get_url_for_tls(url, &proxy_conf); + let tls_url = get_url_for_tls(&url, &proxy_conf); let tls_type = get_cached_tls_type(tls_url); let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); let response = post_request_( - url, + &url, tls_url, - body.to_owned(), + body.clone(), header, tls_type, danger_accept_invalid_cert, danger_accept_invalid_cert, ) .await?; - let status = response.status().as_u16(); - let text = response.text().await?; - Ok((status, text)) -} - -/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn` -/// if the URL is eligible. 4xx responses are returned as-is. -async fn with_tcp_proxy_fallback( - url: &str, - method: &str, - http_fn: HttpFut, - tcp_fn: TcpFut, -) -> ResultType -where - HttpFut: Future>, - TcpFut: Future>, -{ - if should_use_raw_tcp_for_api(url) { - return tcp_fn.await; - } - - let http_result = http_fn.await; - let should_fallback = match &http_result { - Err(_) => true, - Ok((status, _)) => *status >= 500, - }; - - if should_fallback && can_fallback_to_raw_tcp(url) { - log::warn!( - "HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback", - method, - tcp_proxy_log_target(url), - http_result - .as_ref() - .map(|(s, _)| *s) - .map_err(|e| e.to_string()), - ); - match tcp_fn.await { - Ok(resp) => return Ok(resp), - Err(tcp_err) => { - log::warn!("TCP proxy fallback also failed: {:?}", tcp_err); - } - } - } - - http_result.map(|(_status, text)| text) -} - -/// POST request with raw TCP proxy support. -/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy. -/// - Otherwise tries HTTP first; on connection failure or 5xx status, -/// falls back to TCP proxy if WS is off. -/// - 4xx responses are returned as-is (server is reachable, business logic error). -/// - If fallback also fails, returns the original HTTP result (text or error). -pub async fn post_request(url: String, body: String, header: &str) -> ResultType { - with_tcp_proxy_fallback( - &url, - "POST", - post_request_http(&url, &body, header), - post_request_via_tcp_proxy(&url, &body, header), - ) - .await + Ok(response.text().await?) } #[async_recursion] @@ -1511,16 +1246,21 @@ async fn get_http_response_async( tls_type.unwrap_or(TlsType::Rustls), danger_accept_invalid_cert.unwrap_or(false), ); - let normalized_method = method.to_ascii_lowercase(); - let mut http_client = match normalized_method.as_str() { + let mut http_client = match method { "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!")), }; - for entry in parse_json_header_entries(header)? { - http_client = http_client.header(entry.name, entry.value); + let v = serde_json::from_str(header)?; + + 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 tls_type.is_some() && danger_accept_invalid_cert.is_some() { @@ -1600,51 +1340,6 @@ async fn get_http_response_async( } } -/// Returns (status_code, json_string) so the caller can inspect the status -/// without re-parsing the serialized JSON. -async fn http_request_http( - url: &str, - method: &str, - body: Option, - header: &str, -) -> ResultType<(u16, String)> { - let proxy_conf = Config::get_socks(); - let tls_url = get_url_for_tls(url, &proxy_conf); - let tls_type = get_cached_tls_type(tls_url); - let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); - let response = get_http_response_async( - url, - tls_url, - method, - body, - header, - tls_type, - danger_accept_invalid_cert, - danger_accept_invalid_cert, - ) - .await?; - // Serialize response headers - let mut response_headers = Map::new(); - for (key, value) in response.headers() { - response_headers.insert(key.to_string(), 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 = Map::new(); - result.insert("status_code".to_string(), json!(status_code)); - result.insert("headers".to_string(), Value::Object(response_headers)); - result.insert("body".to_string(), json!(response_body)); - - // Convert map to JSON string - let json_str = serde_json::to_string(&result) - .map_err(|e| anyhow!("Failed to serialize response: {}", e))?; - Ok((status_code, json_str)) -} - -/// HTTP request with raw TCP proxy support. #[tokio::main(flavor = "current_thread")] pub async fn http_request_sync( url: String, @@ -1652,28 +1347,44 @@ pub async fn http_request_sync( body: Option, header: String, ) -> ResultType { - with_tcp_proxy_fallback( + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(&url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + let response = get_http_response_async( &url, + tls_url, &method, - http_request_http(&url, &method, body.clone(), &header), - http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header), + body.clone(), + &header, + tls_type, + danger_accept_invalid_cert, + danger_accept_invalid_cert, ) - .await -} + .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("")), + ); + } -/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync). -/// Returns a JSON string with status_code, headers, body (same format as http_request_sync). -async fn http_request_via_tcp_proxy( - url: &str, - method: &str, - body: Option<&str>, - header: &str, -) -> ResultType { - let headers = parse_json_header_entries(header)?; - let body_bytes = body.unwrap_or("").as_bytes(); + let status_code = response.status().as_u16(); + let response_body = response.text().await?; - let resp = tcp_proxy_request(method, url, body_bytes, headers).await?; - http_proxy_response_to_json(resp) + // 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] @@ -1936,7 +1647,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool { false } -async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> { +pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { // Skip additional encryption when using WebSocket connections (wss://) // as WebSocket Secure (wss://) already provides transport layer encryption. // This doesn't affect the end-to-end encryption between clients, @@ -1969,9 +1680,7 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> }); timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; conn.set_key(key); - if log_on_success { - log::info!("Connection secured"); - } + log::info!("Connection secured"); } _ => {} } @@ -1982,14 +1691,6 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> Ok(()) } -pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { - secure_tcp_impl(conn, key, true).await -} - -async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> { - secure_tcp_impl(conn, key, false).await -} - #[inline] fn get_pk(pk: &[u8]) -> Option<[u8; 32]> { if pk.len() == 32 { @@ -2767,13 +2468,11 @@ mod tests { assert!(is_public("https://rustdesk.com/")); assert!(is_public("https://www.rustdesk.com/")); assert!(is_public("https://api.rustdesk.com/v1")); - assert!(is_public("https://API.RUSTDESK.COM/v1")); assert!(is_public("https://rustdesk.com/path")); // Test URLs ending with "rustdesk.com" assert!(is_public("rustdesk.com")); assert!(is_public("https://rustdesk.com")); - assert!(is_public("https://RustDesk.com")); assert!(is_public("http://www.rustdesk.com")); assert!(is_public("https://api.rustdesk.com")); @@ -2786,193 +2485,6 @@ mod tests { assert!(!is_public("rustdesk.comhello.com")); } - #[test] - fn test_should_use_tcp_proxy_for_api_url() { - assert!(should_use_tcp_proxy_for_api_url( - "https://admin.example.com/api/login", - "https://admin.example.com" - )); - assert!(should_use_tcp_proxy_for_api_url( - "https://admin.example.com:21114/api/login", - "https://admin.example.com" - )); - assert!(!should_use_tcp_proxy_for_api_url( - "https://api.telegram.org/bot123/sendMessage", - "https://admin.example.com" - )); - assert!(!should_use_tcp_proxy_for_api_url( - "https://admin.rustdesk.com/api/login", - "https://admin.rustdesk.com" - )); - assert!(!should_use_tcp_proxy_for_api_url( - "https://admin.example.com/api/login", - "not a url" - )); - assert!(!should_use_tcp_proxy_for_api_url( - "not a url", - "https://admin.example.com" - )); - } - - #[test] - fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() { - struct RestoreCustomRendezvousServer(String); - - impl Drop for RestoreCustomRendezvousServer { - fn drop(&mut self) { - Config::set_option( - keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), - self.0.clone(), - ); - } - } - - let _restore = RestoreCustomRendezvousServer(Config::get_option( - keys::OPTION_CUSTOM_RENDEZVOUS_SERVER, - )); - Config::set_option( - keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), - "1:2".to_string(), - ); - - assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}")); - } - - #[tokio::test] - async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() { - let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() { - let err = http_request_via_tcp_proxy("not a url", "get", None, "[]") - .await - .unwrap_err() - .to_string(); - assert!(err.contains("HTTP header information parsing failed!")); - } - - #[test] - fn test_parse_json_header_entries_preserves_single_content_type() { - let headers = parse_json_header_entries( - r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#, - ) - .unwrap(); - - assert_eq!( - headers - .iter() - .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) - .count(), - 1 - ); - assert_eq!( - headers - .iter() - .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) - .map(|entry| entry.value.as_str()), - Some("text/plain") - ); - } - - #[test] - fn test_parse_json_header_entries_does_not_add_default_content_type() { - let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap(); - - assert!(!headers - .iter() - .any(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))); - } - - #[test] - fn test_parse_simple_header_respects_custom_content_type() { - let headers = parse_simple_header("Content-Type: text/plain"); - - assert_eq!( - headers - .iter() - .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) - .count(), - 1 - ); - assert_eq!( - headers - .iter() - .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) - .map(|entry| entry.value.as_str()), - Some("text/plain") - ); - } - - #[test] - fn test_parse_simple_header_preserves_non_content_type_header() { - let headers = parse_simple_header("Authorization: Bearer token"); - - assert!(headers.iter().any(|entry| { - entry.name.eq_ignore_ascii_case("Authorization") - && entry.value.as_str() == "Bearer token" - })); - assert_eq!( - headers - .iter() - .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) - .count(), - 1 - ); - assert_eq!( - headers - .iter() - .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) - .map(|entry| entry.value.as_str()), - Some("application/json") - ); - } - - #[test] - fn test_tcp_proxy_log_target_redacts_query_only() { - assert_eq!( - tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"), - "https://example.com/api/heartbeat" - ); - } - - #[test] - fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() { - assert_eq!( - tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"), - "https://[2001:db8::1]:21114/api/heartbeat" - ); - } - - #[test] - fn test_http_proxy_response_to_json() { - let mut resp = HttpProxyResponse { - status: 200, - body: br#"{"ok":true}"#.to_vec().into(), - ..Default::default() - }; - resp.headers.push(HeaderEntry { - name: "Content-Type".into(), - value: "application/json".into(), - ..Default::default() - }); - - let json = http_proxy_response_to_json(resp).unwrap(); - let value: Value = serde_json::from_str(&json).unwrap(); - assert_eq!(value["status_code"], 200); - assert_eq!(value["headers"]["content-type"], "application/json"); - assert_eq!(value["body"], r#"{"ok":true}"#); - - let err = http_proxy_response_to_json(HttpProxyResponse { - error: "dial failed".into(), - ..Default::default() - }) - .unwrap_err() - .to_string(); - assert!(err.contains("TCP proxy error: dial failed")); - } - #[test] fn test_mouse_event_constants_and_mask_layout() { use super::input::*; diff --git a/src/core_main.rs b/src/core_main.rs index 4515faa6b..3119529c6 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -146,13 +146,7 @@ pub fn core_main() -> Option> { crate::portable_service::client::set_quick_support(_is_quick_support); } let mut log_name = "".to_owned(); - // Keep portable-service logs under a stable directory name. - let has_portable_service_shmem_arg = args - .iter() - .any(|arg| arg.starts_with("--portable-service-shmem-name=")); - if has_portable_service_shmem_arg { - log_name = "portable-service".to_owned(); - } else if args.len() > 0 && args[0].starts_with("--") { + if args.len() > 0 && args[0].starts_with("--") { let name = args[0].replace("--", ""); if !name.is_empty() { log_name = name; @@ -199,20 +193,6 @@ pub fn core_main() -> Option> { } std::thread::spawn(move || crate::start_server(false, no_server)); } else { - #[cfg(any(target_os = "linux", target_os = "macos"))] - // Root CLI management commands must talk to the user `--server` main IPC. - // Example: `sudo rustdesk --option custom-rendezvous-server` should query the - // user's IPC instead of root's `/tmp/-0/ipc`; `connect()` still limits this - // routing to empty-postfix main IPC only. - let _user_main_ipc_scope = if crate::platform::is_installed() - && is_root() - && is_user_main_ipc_scope_cli_command(&args) - { - Some(crate::ipc::UserMainIpcScope::new()) - } else { - None - }; - #[cfg(windows)] { use crate::platform; @@ -233,7 +213,7 @@ pub fn core_main() -> Option> { } Ok(false) => "Update failed!".to_string(), Ok(true) => match platform::update_me(false) { - Ok(_) => "Updated successfully!".to_string(), + Ok(_) => "Update successfully!".to_string(), Err(err) => { log::error!("Failed with error: {err}"); "Update failed!".to_string() @@ -355,8 +335,8 @@ pub fn core_main() -> Option> { log::info!("Starting update process..."); let _text = match platform::update_me() { Ok(_) => { - println!("{}", translate("Updated successfully!".to_string())); - log::info!("Updated successfully!"); + println!("{}", translate("Update successfully!".to_string())); + log::info!("Update successfully!"); } Err(err) => { eprintln!("Update failed with error: {}", err); @@ -641,98 +621,6 @@ pub fn core_main() -> Option> { println!("Installation and administrative privileges required!"); } return None; - } else if args[0] == "--deploy" { - if config::Config::no_register_device() { - println!("Cannot deploy an unregistrable device!"); - } else if crate::platform::is_installed() && is_root() { - let max = args.len() - 1; - let pos = args.iter().position(|x| x == "--token").unwrap_or(max); - if pos >= max { - println!("--token is required!"); - return None; - } - let token = args[pos + 1].to_owned(); - let get_value = |c: &str| { - let pos = args.iter().position(|x| x == c).unwrap_or(max); - if pos < max { - Some(args[pos + 1].to_owned()) - } else { - None - } - }; - let new_id = get_value("--id"); - let local_id = crate::ipc::get_id(); - let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone()); - let uuid = crate::encode64(hbb_common::get_uuid()); - let pk = crate::encode64( - hbb_common::config::Config::get_key_pair().1, - ); - let body = serde_json::json!({ - "id": id_to_deploy, - "uuid": uuid, - "pk": pk, - }); - let header = "Authorization: Bearer ".to_owned() + &token; - let url = crate::ui_interface::get_api_server() + "/api/devices/deploy"; - match crate::post_request_sync(url, body.to_string(), &header) { - Err(err) => { - println!("Request failed: {}", err); - std::process::exit(1); - } - Ok(text) => { - let parsed: serde_json::Value = - serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); - let result = parsed["result"].as_str().unwrap_or(""); - match result { - "OK" => { - if let Some(ref new_id) = new_id { - if *new_id != local_id { - if let Err(err) = - crate::ipc::set_config("id", new_id.clone()) - { - println!( - "Failed to persist deployed id locally: {}", - err - ); - std::process::exit(1); - } - } - } - if let Err(err) = crate::ipc::notify_deployed() { - log::warn!("Failed to notify deployed state: {}", err); - } - println!("Device deployed."); - } - "NOT_ENABLED" => { - println!("Server does not require deployment."); - std::process::exit(3); - } - "INVALID_INPUT" => { - println!("Invalid input."); - std::process::exit(5); - } - "ID_TAKEN" => { - println!( - "Id `{}` is already used by another machine on the server.", - id_to_deploy - ); - std::process::exit(6); - } - _ => { - if text.is_empty() { - println!("Unknown response."); - } else { - println!("{}", text); - } - std::process::exit(1); - } - } - } - } - } else { - println!("Installation and administrative privileges required!"); - } - return None; } else if args[0] == "--check-hwcodec-config" { #[cfg(feature = "hwcodec")] crate::ipc::hwcodec_process(); @@ -952,57 +840,6 @@ fn is_root() -> bool { crate::platform::is_root() } -#[cfg(any(target_os = "linux", target_os = "macos", test))] -fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool { - matches!( - args.first().map(String::as_str), - Some("--password") - | Some("--set-unlock-pin") - | Some("--get-id") - | Some("--set-id") - | Some("--config") - | Some("--option") - | Some("--assign") - | Some("--deploy") - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn args(values: &[&str]) -> Vec { - values.iter().map(|value| value.to_string()).collect() - } - - #[test] - fn user_main_ipc_scope_cli_command_matches_management_commands_only() { - for command in [ - "--password", - "--set-unlock-pin", - "--get-id", - "--set-id", - "--config", - "--option", - "--assign", - "--deploy", - ] { - assert!(is_user_main_ipc_scope_cli_command(&args(&[command]))); - } - - for command in [ - "--service", - "--server", - "--tray", - "--cm", - "--check-hwcodec-config", - "--connect", - ] { - assert!(!is_user_main_ipc_scope_cli_command(&args(&[command]))); - } - } -} - /// Check if the executable is a Quick Support version. /// Note: This function must be kept in sync with `libs/portable/src/main.rs`. #[cfg(windows)] diff --git a/src/flutter.rs b/src/flutter.rs index f8b04bf6c..c7e07f892 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1135,10 +1135,6 @@ impl InvokeUiSession for FlutterHandler { ("message", json!(&opened.message)), ("pid", json!(opened.pid)), ("service_id", json!(&opened.service_id)), - ( - "replay_terminal_output", - json!(opened.replay_terminal_output), - ), ]; if !opened.persistent_sessions.is_empty() { event_data.push(("persistent_sessions", json!(opened.persistent_sessions))); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4b62b4fca..551ad799f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -605,30 +605,21 @@ pub fn session_handle_flutter_raw_key_event( } } +// SyncReturn<()> is used to make sure enter() and leave() are executed in the sequence this function is called. +// // If the cursor jumps between remote page of two connections, leave view and enter view will be called. // session_enter_or_leave() will be called then. -// As Rust is multi-threaded, enter() can be called before leave(). -// The Rust-side grab ownership state filters stale transitions. +// As rust is multi-thread, it is possible that enter() is called before leave(). +// This will cause the keyboard input to take no effect. pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = sessions::get_session_by_session_id(&_session_id) { let keyboard_mode = session.get_keyboard_mode(); - // Use the full per-window UUID (not lc.session_id which is per-connection) - // so that two windows viewing the same peer get distinct grab owners. - let window_id = _session_id.as_u128(); if _enter { set_cur_session_id_(_session_id, &keyboard_mode); - crate::keyboard::client::change_grab_status( - crate::common::GrabState::Run, - &keyboard_mode, - window_id, - ); + session.enter(keyboard_mode); } else { - crate::keyboard::client::change_grab_status( - crate::common::GrabState::Wait, - &keyboard_mode, - window_id, - ); + session.leave(keyboard_mode); } } SyncReturn(()) @@ -972,27 +963,6 @@ pub fn main_show_option(_key: String) -> SyncReturn { } pub fn main_set_option(key: String, value: String) { - #[cfg(target_os = "android")] - { - let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) - || key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER) - || key.eq(config::keys::OPTION_ENABLE_AUDIO); - let allow_perm_change_in_accept_window = config::option2bool( - config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), - ); - if is_permission_option - && !allow_perm_change_in_accept_window - && crate::ui_cm_interface::has_active_clients() - { - log::info!( - "blocked main_set_option by policy, key={}, value={}", - key, - value - ); - return; - } - } #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { crate::ui_cm_interface::switch_permission_all( @@ -1040,29 +1010,7 @@ pub fn main_get_options_sync() -> SyncReturn { } pub fn main_set_options(json: String) { - let mut map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); - #[cfg(target_os = "android")] - { - let allow_perm_change_in_accept_window = config::option2bool( - config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), - ); - if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() { - for key in [ - config::keys::OPTION_ENABLE_CLIPBOARD, - config::keys::OPTION_ENABLE_FILE_TRANSFER, - config::keys::OPTION_ENABLE_AUDIO, - ] { - if let Some(value) = map.remove(key) { - log::info!( - "blocked main_set_options item by policy, key={}, value={}", - key, - value - ); - } - } - } - } + let map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); if !map.is_empty() { set_options(map) } @@ -1745,8 +1693,8 @@ pub fn main_get_temporary_password() -> String { ui_interface::temporary_password() } -pub fn main_set_permanent_password_with_result(password: String) -> bool { - ui_interface::set_permanent_password_with_result(password) +pub fn main_get_permanent_password() -> String { + ui_interface::permanent_password() } pub fn main_get_fingerprint() -> String { @@ -2124,6 +2072,10 @@ pub fn main_update_temporary_password() { update_temporary_password(); } +pub fn main_set_permanent_password(password: String) { + set_permanent_password(password); +} + pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } @@ -2213,7 +2165,7 @@ pub fn cm_elevate_portable(conn_id: i32) { } pub fn cm_switch_back(conn_id: i32) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::switch_back(conn_id); } @@ -2471,23 +2423,16 @@ pub fn is_disable_installation() -> SyncReturn { } pub fn is_preset_password() -> bool { - let hard = config::HARD_SETTINGS + config::HARD_SETTINGS .read() .unwrap() .get("password") - .cloned() - .unwrap_or_default(); - if hard.is_empty() { - return false; - } - - // On desktop, service owns the authoritative config; query it via IPC and return only a boolean. - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return crate::ipc::is_permanent_password_preset(); - - // On mobile, we have no service IPC; verify against local storage. - #[cfg(any(target_os = "android", target_os = "ios"))] - return config::Config::matches_permanent_password_plain(&hard); + .map_or(false, |p| { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return p == &crate::ipc::get_permanent_password(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return p == &config::Config::get_permanent_password(); + }) } // Don't call this function for desktop version. @@ -2823,10 +2768,6 @@ pub fn main_get_common(key: String) -> String { return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string(); #[cfg(not(target_os = "linux"))] return false.to_string(); - } else if key == "permanent-password-set" { - return ui_interface::is_permanent_password_set().to_string(); - } else if key == "local-permanent-password-set" { - return ui_interface::is_local_permanent_password_set().to_string(); } else { if key.starts_with("download-data-") { let id = key.replace("download-data-", ""); @@ -2936,7 +2877,7 @@ pub fn main_set_common(_key: String, _value: String) { } else if _key == "update-me" { if let Some(new_version_file) = get_download_file_from_url(&_value) { log::debug!( - "New version file is downloaded, update begin, {:?}", + "New version file is downloaed, update begin, {:?}", new_version_file.to_str() ); if let Some(f) = new_version_file.to_str() { @@ -3108,22 +3049,6 @@ pub mod server_side { return env.new_string(res).unwrap_or_default().into_raw(); } - #[no_mangle] - pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption( - env: JNIEnv, - _class: JClass, - key: JString, - ) -> jstring { - let mut env = env; - let res = if let Ok(key) = env.get_string(&key) { - let key: String = key.into(); - super::get_builtin_option(&key) - } else { - "".into() - }; - return env.new_string(res).unwrap_or_default().into_raw(); - } - #[no_mangle] pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled( env: JNIEnv, diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 9e4538697..20316b6f5 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -1,4 +1,4 @@ -use hbb_common::ResultType; +use reqwest::blocking::Response; use serde::de::DeserializeOwned; use serde_json::{Map, Value}; @@ -21,9 +21,11 @@ pub enum HbbHttpResponse { Data(T), } -impl HbbHttpResponse { - pub fn parse(body: &str) -> ResultType { - let map = serde_json::from_str::>(body)?; +impl TryFrom for HbbHttpResponse { + type Error = reqwest::Error; + + fn try_from(resp: Response) -> Result>::Error> { + let map = resp.json::>()?; if let Some(error) = map.get("error") { if let Some(err) = error.as_str() { Ok(Self::Error(err.to_owned())) diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 3f824113b..8e6141200 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,6 +1,7 @@ use super::HbbHttpResponse; use crate::hbbs_http::create_http_client_with_url; use hbb_common::{config::LocalConfig, log, ResultType}; +use reqwest::blocking::Client; use serde_derive::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ @@ -108,7 +109,7 @@ pub struct AuthBody { } pub struct OidcSession { - warmed_api_server: Option, + client: Option, state_msg: &'static str, failed_msg: String, code_url: Option, @@ -135,7 +136,7 @@ impl Default for UserStatus { impl OidcSession { fn new() -> Self { Self { - warmed_api_server: None, + client: None, state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, @@ -148,13 +149,12 @@ impl OidcSession { fn ensure_client(api_server: &str) { let mut write_guard = OIDC_SESSION.write().unwrap(); - if write_guard.warmed_api_server.as_deref() == Some(api_server) { - return; + if write_guard.client.is_none() { + // This URL is used to detect the appropriate TLS implementation for the server. + let login_option_url = format!("{}/api/login-options", &api_server); + let client = create_http_client_with_url(&login_option_url); + write_guard.client = Some(client); } - // This URL is used to detect the appropriate TLS implementation for the server. - let login_option_url = format!("{}/api/login-options", api_server); - let _ = create_http_client_with_url(&login_option_url); - write_guard.warmed_api_server = Some(api_server.to_owned()); } fn auth( @@ -164,15 +164,26 @@ impl OidcSession { uuid: &str, ) -> ResultType> { Self::ensure_client(api_server); - let body = serde_json::json!({ - "op": op, - "id": id, - "uuid": uuid, - "deviceInfo": crate::ui_interface::get_login_device_info(), - }) - .to_string(); - let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?; - HbbHttpResponse::parse(&resp) + let resp = if let Some(client) = &OIDC_SESSION.read().unwrap().client { + client + .post(format!("{}/api/oidc/auth", api_server)) + .json(&serde_json::json!({ + "op": op, + "id": id, + "uuid": uuid, + "deviceInfo": crate::ui_interface::get_login_device_info(), + })) + .send()? + } else { + hbb_common::bail!("http client not initialized"); + }; + let status = resp.status(); + match resp.try_into() { + Ok(v) => Ok(v), + Err(err) => { + hbb_common::bail!("Http status: {}, err: {}", status, err); + } + } } fn query( @@ -186,19 +197,11 @@ impl OidcSession { &[("code", code), ("id", id), ("uuid", uuid)], )?; Self::ensure_client(api_server); - #[derive(Deserialize)] - struct HttpResponseBody { - body: String, + if let Some(client) = &OIDC_SESSION.read().unwrap().client { + Ok(client.get(url).send()?.try_into()?) + } else { + hbb_common::bail!("http client not initialized") } - - let resp = crate::http_request_sync( - url.to_string(), - "GET".to_owned(), - None, - "{}".to_owned(), - )?; - let resp = serde_json::from_str::(&resp)?; - HbbHttpResponse::parse(&resp.body) } fn reset(&mut self) { diff --git a/src/ipc.rs b/src/ipc.rs index ffe1b08a5..891ec81dd 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,28 +1,33 @@ -#[path = "ipc/auth.rs"] -mod ipc_auth; -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[path = "ipc/fs.rs"] -mod ipc_fs; +use crate::{ + common::CheckTestNatType, + privacy_mode::PrivacyModeState, + ui_interface::{get_local_option, set_local_option}, +}; +use bytes::Bytes; +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::plugin::ipc::Plugin; -use crate::{ - common::{is_server, CheckTestNatType}, - privacy_mode, - privacy_mode::PrivacyModeState, - rendezvous_mediator::RendezvousMediator, - ui_interface::{get_local_option, set_local_option}, -}; -use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipboardFile; -#[cfg(target_os = "linux")] -use hbb_common::anyhow; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, - config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, + config::{ + self, + keys::{self, OPTION_ALLOW_WEBSOCKET}, + Config, Config2, + }, futures::StreamExt as _, futures_util::sink::SinkExt, log, password_security as password, timeout, @@ -33,92 +38,13 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; -#[cfg(windows)] -pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection; -#[cfg(windows)] -pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt; -#[cfg(windows)] -pub(crate) use ipc_auth::log_rejected_windows_ipc_connection; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection}; -#[cfg(windows)] -use ipc_auth::{ - authorize_windows_main_ipc_connection, portable_service_listener_security_attributes, - should_allow_everyone_create_on_windows, -}; -#[cfg(target_os = "linux")] -pub(crate) use ipc_auth::{ - ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid, - log_rejected_uinput_connection, peer_uid_from_fd, -}; -#[cfg(target_os = "linux")] -use ipc_fs::terminal_count_candidate_uids; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use ipc_fs::{ - check_pid, ensure_secure_ipc_parent_dir, scrub_secure_ipc_parent_dir, - should_scrub_parent_entries_after_check_pid, write_pid, -}; -use parity_tokio_ipc::{ - Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, -}; -use serde_derive::{Deserialize, Serialize}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::cell::Cell; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::os::unix::fs::PermissionsExt; -use std::{ - collections::HashMap, - sync::atomic::{AtomicBool, Ordering}, -}; + +use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator}; // IPC actions here. pub const IPC_ACTION_CLOSE: &str = "close"; -#[cfg(target_os = "windows")] -const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000; -#[cfg(target_os = "windows")] -pub(crate) const IPC_TOKEN_LEN: usize = 64; -#[cfg(target_os = "windows")] -const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2; -#[cfg(target_os = "windows")] -const _: () = assert!(IPC_TOKEN_LEN % 2 == 0); pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); -#[cfg(any(target_os = "linux", target_os = "macos"))] -thread_local! { - static USE_USER_MAIN_IPC: Cell = Cell::new(false); -} - -#[must_use = "bind this guard to a local variable to keep the IPC scope active"] -/// Thread-local guard for routing root main IPC to the active user on Linux/macOS. -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub(crate) struct UserMainIpcScope { - previous: bool, -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl UserMainIpcScope { - pub(crate) fn new() -> Self { - let previous = USE_USER_MAIN_IPC.with(|use_user_main| { - let previous = use_user_main.get(); - use_user_main.set(true); - previous - }); - Self { previous } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl Drop for UserMainIpcScope { - fn drop(&mut self) { - USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous)); - } -} - -#[inline] -pub async fn connect_service(ms_timeout: u64) -> ResultType> { - connect(ms_timeout, crate::POSTFIX_SERVICE).await -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { @@ -281,8 +207,6 @@ pub enum DataControl { pub enum DataPortableService { Ping, Pong, - AuthToken(String), - AuthResult(bool), ConnCount(Option), Mouse((Vec, i32, String, u32, bool, bool)), Pointer((Vec, i32)), @@ -313,7 +237,6 @@ pub enum Data { restart: bool, recording: bool, block_input: bool, - privacy_mode: bool, from_switch: bool, }, ChatMessage { @@ -349,7 +272,6 @@ pub enum Data { ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, - Deployed, #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -362,14 +284,7 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] SwitchSidesRequest(String), - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - SwitchSidesUuid(String, String, Option), - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] SwitchSidesBack, UrlLink(String), VoiceCallIncoming, @@ -488,22 +403,6 @@ pub async fn start(postfix: &str) -> ResultType<()> { Ok(stream) => { let mut stream = Connection::new(stream); let postfix = postfix.to_owned(); - #[cfg(any(target_os = "linux", target_os = "macos"))] - if config::is_service_ipc_postfix(&postfix) { - if !authorize_service_scoped_ipc_connection(&stream, &postfix) { - continue; - } - } - #[cfg(windows)] - if postfix.is_empty() { - // Windows main IPC (`postfix == ""`) is authorized here. - // Other security-sensitive channels use dedicated authorization paths: - // - `_portable_service`: portable-service listener + handshake policy - // - service-scoped postfixes: service-specific listener/authorization - if !authorize_windows_main_ipc_connection(&stream, &postfix) { - continue; - } - } tokio::spawn(async move { loop { match stream.next().await { @@ -512,48 +411,9 @@ pub async fn start(postfix: &str) -> ResultType<()> { break; } Ok(Some(data)) => { - // On Linux/macOS, the protected `_service` channel is used only for - // syncing config between root service and the active user process. - // - // NOTE: `is_service_ipc_postfix()` also includes `_uinput_*`, but those - // channels are handled by the dedicated uinput listener/protocol in - // `src/server/uinput.rs` and therefore do not share this Data enum - // allowlist. The SyncConfig allowlist here is intentionally scoped to the - // `_service` channel only. - // - // Keep this explicit branch to avoid policy drift between `_service` and - // uinput IPC paths while still minimizing exposed message surface here. - #[cfg(any(target_os = "linux", target_os = "macos"))] - if postfix == crate::POSTFIX_SERVICE { - if matches!(&data, Data::SyncConfig(_)) { - handle(data, &mut stream).await; - } else { - log::warn!( - "Rejected non-sync data on protected _service IPC channel: postfix={}, data_kind={:?}, peer_uid={:?}", - postfix, - std::mem::discriminant(&data), - stream.peer_uid() - ); - // Close the connection to avoid keeping a protected channel - // alive while repeatedly receiving invalid traffic. - break; - } - continue; - } handle(data, &mut stream).await; } - Ok(None) => { - // `Ok(None)` means a complete frame arrived but did not - // deserialize into `Data`. Peer close/reset is returned as - // `Err` by `ConnectionTmpl::next()`. Keep the historical - // ignore behavior except on the protected `_service` channel. - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - if postfix == crate::POSTFIX_SERVICE { - break; - } - } - } + _ => {} } } }); @@ -568,77 +428,20 @@ pub async fn start(postfix: &str) -> ResultType<()> { pub async fn new_listener(postfix: &str) -> ResultType { let path = Config::ipc_path(postfix); - #[cfg(any(target_os = "linux", target_os = "macos"))] - let should_scrub_parent_entries = ensure_secure_ipc_parent_dir(&path, postfix)?; - #[cfg(any(target_os = "linux", target_os = "macos"))] - let existing_listener_alive = check_pid(postfix).await; - #[cfg(any(target_os = "linux", target_os = "macos"))] - if should_scrub_parent_entries_after_check_pid( - should_scrub_parent_entries, - existing_listener_alive, - ) { - scrub_secure_ipc_parent_dir(&path, postfix)?; - } + #[cfg(not(any(windows, target_os = "android", target_os = "ios")))] + check_pid(postfix).await; let mut endpoint = Endpoint::new(path.clone()); - let security_attrs = { - #[cfg(windows)] - { - if postfix == "_portable_service" { - portable_service_listener_security_attributes() - } else if should_allow_everyone_create_on_windows(postfix) { - SecurityAttributes::allow_everyone_create() - } else { - Ok(SecurityAttributes::empty()) - } - } - #[cfg(not(windows))] - { - SecurityAttributes::allow_everyone_create() - } - }; - match security_attrs { + match SecurityAttributes::allow_everyone_create() { Ok(attr) => endpoint.set_security_attributes(attr), - Err(err) => { - log::error!("Failed to set ipc{} security: {}", postfix, err); - #[cfg(windows)] - if postfix == "_portable_service" { - // Fail closed for `_portable_service` when SDDL construction fails. - // This endpoint is security-critical and must not start with default ACLs. - return Err(err.into()); - } - } + Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err), }; match endpoint.incoming() { Ok(incoming) => { - if postfix == crate::POSTFIX_SERVICE { - log::info!("Started protected ipc service server: postfix={}", postfix); - } else { - log::info!("Started ipc{} server at path: {}", postfix, &path); - } - #[cfg(any(target_os = "linux", target_os = "macos"))] + log::info!("Started ipc{} server at path: {}", postfix, &path); + #[cfg(not(windows))] { - // NOTE: On Linux/macOS, some IPC sockets are intentionally world-connectable - // (0666) so the active (non-root) user process can connect. Authorization is - // enforced at accept-time for these channels, and the protected `_service` - // channel is further restricted by an explicit message allowlist (SyncConfig - // only). - let socket_mode = if config::is_service_ipc_postfix(postfix) { - 0o0666 - } else { - 0o0600 - }; - if let Err(err) = - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(socket_mode)) - { - log::error!( - "Failed to set permissions on ipc{} socket at path {}: {}", - postfix, - &path, - err - ); - std::fs::remove_file(&path).ok(); - return Err(err.into()); - } + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); write_pid(postfix); } Ok(incoming) @@ -829,29 +632,8 @@ async fn handle(data: Data, stream: &mut Connection) { value = Some(Config::get_id()); } else if name == "temporary-password" { value = Some(password::temporary_password()); - } else if name == "permanent-password-storage-and-salt" { - let (storage, salt) = Config::get_local_permanent_password_storage_and_salt(); - value = Some(storage + "\n" + &salt); - } else if name == "permanent-password-set" { - value = Some(if Config::has_permanent_password() { - "Y".to_owned() - } else { - "N".to_owned() - }); - } else if name == "permanent-password-is-preset" { - let hard = config::HARD_SETTINGS - .read() - .unwrap() - .get("password") - .cloned() - .unwrap_or_default(); - let is_preset = - !hard.is_empty() && Config::matches_permanent_password_plain(&hard); - value = Some(if is_preset { - "Y".to_owned() - } else { - "N".to_owned() - }); + } else if name == "permanent-password" { + value = Some(Config::get_permanent_password()); } else if name == "salt" { value = Some(Config::get_salt()); } else if name == "rendezvous_server" { @@ -887,24 +669,13 @@ async fn handle(data: Data, stream: &mut Connection) { allow_err!(stream.send(&Data::Config((name, value))).await); } Some(value) => { - let mut updated = true; if name == "id" { Config::set_key_confirmed(false); Config::set_id(&value); } else if name == "temporary-password" { password::update_temporary_password(); } else if name == "permanent-password" { - if Config::is_disable_change_permanent_password() { - log::warn!("Changing permanent password is disabled"); - updated = false; - } else { - Config::set_permanent_password(&value); - } - // Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to - // distinguish "accepted by daemon" vs "IPC send succeeded" without - // reading back any secret. - let ack = if updated { "Y" } else { "N" }.to_owned(); - allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await); + Config::set_permanent_password(&value); } else if name == "salt" { Config::set_salt(&value); } else if name == "voice-call-input" { @@ -914,9 +685,7 @@ async fn handle(data: Data, stream: &mut Connection) { } else { return; } - if updated { - log::info!("{} updated", name); - } + log::info!("{} updated", name); } }, Data::Options(value) => match value { @@ -967,12 +736,6 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } - Data::Deployed => { - crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst); - crate::rendezvous_mediator::RendezvousMediator::restart(); - } - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::SwitchSidesRequest(id) => { let uuid = uuid::Uuid::new_v4(); crate::server::insert_switch_sides_uuid(id, uuid.clone()); @@ -982,19 +745,6 @@ async fn handle(data: Data, stream: &mut Connection) { .await ); } - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - Data::SwitchSidesUuid(uuid, id, None) => { - let allowed = uuid - .parse::() - .map(|uuid| crate::server::remove_pending_switch_sides_uuid(&id, &uuid)) - .unwrap_or(false); - allow_err!( - stream - .send(&Data::SwitchSidesUuid(uuid, id, Some(allowed))) - .await - ); - } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::Plugin(plugin) => crate::plugin::ipc::handle_plugin(plugin, stream).await, @@ -1146,210 +896,13 @@ async fn handle(data: Data, stream: &mut Connection) { ); } _ => {} - }; -} - -#[cfg(target_os = "windows")] -pub(crate) fn generate_one_time_ipc_token() -> ResultType { - use hbb_common::rand::{rngs::OsRng, RngCore as _}; - use std::fmt::Write as _; - - let mut random_bytes = [0u8; IPC_TOKEN_RANDOM_BYTES]; - let mut rng = OsRng; - rng.try_fill_bytes(&mut random_bytes).map_err(|err| { - hbb_common::anyhow::anyhow!( - "failed to generate portable service ipc token from OsRng: {}", - err - ) - })?; - - let mut token = String::with_capacity(IPC_TOKEN_LEN); - for byte in random_bytes { - let _ = write!(token, "{:02x}", byte); } - Ok(token) -} - -#[cfg(target_os = "windows")] -pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool { - if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN { - return false; - } - expected - .as_bytes() - .iter() - .zip(candidate.as_bytes().iter()) - .fold(0u8, |diff, (left, right)| diff | (*left ^ *right)) - == 0 -} - -#[cfg(target_os = "windows")] -pub(crate) async fn portable_service_ipc_handshake_as_client( - stream: &mut ConnectionTmpl, - token: &str, -) -> ResultType<()> -where - T: AsyncRead + AsyncWrite + std::marker::Unpin, -{ - stream - .send(&Data::DataPortableService(DataPortableService::AuthToken( - token.to_owned(), - ))) - .await?; - match stream - .next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS) - .await? - { - Some(Data::DataPortableService(DataPortableService::AuthResult(true))) => Ok(()), - Some(Data::DataPortableService(DataPortableService::AuthResult(false))) => { - bail!("portable service ipc handshake was rejected by server") - } - Some(_) | None => bail!("portable service ipc handshake returned an unexpected response"), - } -} - -#[cfg(target_os = "windows")] -pub(crate) async fn portable_service_ipc_handshake_as_server( - stream: &mut ConnectionTmpl, - mut validate_token: F, -) -> ResultType<()> -where - T: AsyncRead + AsyncWrite + std::marker::Unpin, - // Token validators must use `constant_time_ipc_token_eq` or an equivalent - // fixed-length comparison; this handshake is part of the privilege boundary. - F: FnMut(&str) -> bool, -{ - let authorized = match stream - .next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS) - .await? - { - Some(Data::DataPortableService(DataPortableService::AuthToken(token))) => { - validate_token(&token) - } - Some(_) | None => false, - }; - stream - .send(&Data::DataPortableService(DataPortableService::AuthResult( - authorized, - ))) - .await?; - if !authorized { - bail!("portable service ipc handshake failed") - } - Ok(()) -} - -#[inline] -async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType> { - let client = timeout(ms_timeout, Endpoint::connect(path)).await??; - Ok(ConnectionTmpl::new(client)) -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -fn select_server_uid_for_user_main_ipc( - server_uids: &[u32], - active_uid: Option, - prefer_root: bool, -) -> ResultType { - let mut server_uids = server_uids.to_vec(); - server_uids.sort_unstable(); - server_uids.dedup(); - - match server_uids.as_slice() { - [] => { - if let Some(uid) = active_uid { - // If no `--server` processes are found but the active user is identifiable, - // try the active user anyway because the main process may also listen on "" IPC. - return Ok(uid); - } else { - bail!("No --server process found for user main IPC") - } - } - [uid] => return Ok(*uid), - _ => {} - } - - if prefer_root && server_uids.contains(&0) { - return Ok(0); - } - if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) { - return Ok(active_uid); - } - bail!("Multiple --server processes found for user main IPC"); -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn running_server_uids_for_current_exe() -> ResultType> { - let current_exe = std::env::current_exe()?; - let current_exe_path = std::fs::canonicalize(¤t_exe)?; - let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id()); - let mut sys = hbb_common::sysinfo::System::new(); - sys.refresh_processes(); - let mut server_uids = Vec::new(); - for process in sys.processes().values() { - if process.pid() == current_pid { - continue; - } - if process.cmd().get(1).map_or(true, |arg| arg != "--server") { - continue; - } - let Ok(process_path) = std::fs::canonicalize(process.exe()) else { - continue; - }; - if process_path != current_exe_path { - continue; - } - let Some(uid) = process.user_id().map(|uid| **uid as u32) else { - // Root CLI management commands need a stable matching `--server` target. - // If this key process races during enumeration, failing the command is clearer - // than silently skipping it; `--server` is not expected to exit frequently. - bail!("Failed to read --server process uid"); - }; - server_uids.push(uid); - } - Ok(server_uids) -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -fn user_main_ipc_server_uid() -> ResultType { - let server_uids = running_server_uids_for_current_exe()?; - #[cfg(target_os = "linux")] - let prefer_root = crate::platform::linux::is_login_screen_wayland(); - #[cfg(target_os = "macos")] - let prefer_root = false; - select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root) } pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { - #[cfg(any(target_os = "linux", target_os = "macos"))] - { - let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get()); - let is_root_main_ipc = - unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc; - if is_root_main_ipc { - let uid = user_main_ipc_server_uid()?; - let path = Config::ipc_path_for_uid(uid, postfix); - return connect_with_path(ms_timeout, &path).await; - } - let path = Config::ipc_path(postfix); - return connect_with_path(ms_timeout, &path).await; - } - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - let path = Config::ipc_path(postfix); - connect_with_path(ms_timeout, &path).await - } -} - -#[cfg(target_os = "linux")] -pub async fn connect_for_uid( - ms_timeout: u64, - uid: u32, - postfix: &str, -) -> ResultType> { - let path = Config::ipc_path_for_uid(uid, postfix); - connect_with_path(ms_timeout, &path).await + let path = Config::ipc_path(postfix); + let client = timeout(ms_timeout, Endpoint::connect(&path)).await??; + Ok(ConnectionTmpl::new(client)) } #[cfg(target_os = "linux")] @@ -1429,6 +982,54 @@ pub async fn start_pa() { } } +#[inline] +#[cfg(not(windows))] +fn get_pid_file(postfix: &str) -> String { + let path = Config::ipc_path(postfix); + format!("{}.pid", path) +} + +#[cfg(not(any(windows, target_os = "android", target_os = "ios")))] +async fn check_pid(postfix: &str) { + let pid_file = get_pid_file(postfix); + if let Ok(mut file) = File::open(&pid_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let pid = content.parse::().unwrap_or(0); + if pid > 0 { + use hbb_common::sysinfo::System; + let mut sys = System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(pid.into()) { + if let Some(current) = sys.process((std::process::id() as usize).into()) { + if current.name() == p.name() { + // double check with connect + if connect(1000, postfix).await.is_ok() { + return; + } + } + } + } + } + } + // if not remove old ipc file, the new ipc creation will fail + // if we remove a ipc file, but the old ipc process is still running, + // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive + std::fs::remove_file(&Config::ipc_path(postfix)).ok(); +} + +#[inline] +#[cfg(not(windows))] +fn write_pid(postfix: &str) { + let path = get_pid_file(postfix); + if let Ok(mut file) = File::create(&path) { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + file.write_all(&std::process::id().to_string().into_bytes()) + .ok(); + } +} + pub struct ConnectionTmpl { inner: Framed, } @@ -1542,57 +1143,13 @@ pub fn update_temporary_password() -> ResultType<()> { set_config("temporary-password", "".to_owned()) } -fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> { - let Some(payload) = payload else { - return Ok(()); - }; - let Some((storage, salt)) = payload.split_once('\n') else { - bail!("Invalid permanent-password-storage-and-salt payload"); - }; - - if storage.is_empty() { - Config::set_permanent_password_storage_for_sync("", "")?; - return Ok(()); +pub fn get_permanent_password() -> String { + if let Ok(Some(v)) = get_config("permanent-password") { + Config::set_permanent_password(&v); + v + } else { + Config::get_permanent_password() } - - Config::set_permanent_password_storage_for_sync(storage, salt)?; - Ok(()) -} - -pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> { - let v = get_config("permanent-password-storage-and-salt")?; - apply_permanent_password_storage_and_salt_payload(v.as_deref()) -} - -async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> { - let ms_timeout = 1_000; - let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?; - apply_permanent_password_storage_and_salt_payload(v.as_deref()) -} - -pub fn is_permanent_password_set() -> bool { - match get_config("permanent-password-set") { - Ok(Some(v)) => { - let v = v.trim(); - return v == "Y"; - } - Ok(None) => { - // No response/value (timeout). - } - Err(_) => { - // Connection error. - } - } - log::warn!("Failed to query permanent password state from daemon"); - false -} - -pub fn is_permanent_password_preset() -> bool { - if let Ok(Some(v)) = get_config("permanent-password-is-preset") { - let v = v.trim(); - return v == "Y"; - } - false } pub fn get_fingerprint() -> String { @@ -1602,41 +1159,8 @@ pub fn get_fingerprint() -> String { } pub fn set_permanent_password(v: String) -> ResultType<()> { - if Config::is_disable_change_permanent_password() { - bail!("Changing permanent password is disabled"); - } - if set_permanent_password_with_ack(v)? { - Ok(()) - } else { - bail!("Changing permanent password was rejected by daemon"); - } -} - -#[tokio::main(flavor = "current_thread")] -pub async fn set_permanent_password_with_ack(v: String) -> ResultType { - set_permanent_password_with_ack_async(v).await -} - -async fn set_permanent_password_with_ack_async(v: String) -> ResultType { - // The daemon ACK/NACK is expected quickly since it applies the config in-process. - let ms_timeout = 1_000; - let mut c = connect(ms_timeout, "").await?; - c.send_config("permanent-password", v).await?; - if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? { - if name2 == "permanent-password" { - let v = v.trim(); - let ok = v == "Y"; - if ok { - // Ensure the hashed permanent password storage is written to the user config file. - // This sync must not affect the daemon ACK outcome. - if let Err(err) = sync_permanent_password_storage_from_daemon_async().await { - log::warn!("Failed to sync permanent password storage from daemon: {err}"); - } - } - return Ok(ok); - } - } - Ok(false) + Config::set_permanent_password(&v); + set_config("permanent-password", v) } #[cfg(feature = "flutter")] @@ -1875,13 +1399,6 @@ pub async fn test_rendezvous_server() -> ResultType<()> { Ok(()) } -#[tokio::main(flavor = "current_thread")] -pub async fn notify_deployed() -> ResultType<()> { - let mut c = connect(1000, "").await?; - c.send(&Data::Deployed).await?; - Ok(()) -} - #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { connect(1_000, "_url") @@ -1899,10 +1416,9 @@ pub fn close_all_instances() -> ResultType { } } -#[cfg(windows)] #[tokio::main(flavor = "current_thread")] pub async fn connect_to_user_session(usid: Option) -> ResultType<()> { - let mut stream = crate::ipc::connect_service(1000).await?; + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??; Ok(()) } @@ -2028,76 +1544,13 @@ pub async fn update_controlling_session_count(count: usize) -> ResultType<()> { #[cfg(target_os = "linux")] #[tokio::main(flavor = "current_thread")] pub async fn get_terminal_session_count() -> ResultType { - let timeout_ms = 1_000; - let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - let candidate_uids = terminal_count_candidate_uids(effective_uid); - let mut last_err: Option = None; - for candidate_uid in candidate_uids { - let socket_path = Config::ipc_path_for_uid(candidate_uid, ""); - let connect_result = timeout(timeout_ms, Endpoint::connect(&socket_path)) - .await - .map_err(|err| { - anyhow::anyhow!( - "Timeout connecting to terminal ipc at {}: {}", - socket_path, - err - ) - }); - let connection = match connect_result { - Ok(Ok(connection)) => connection, - Ok(Err(err)) => { - last_err = Some(anyhow::anyhow!( - "Failed to connect to terminal ipc at {}: {}", - socket_path, - err - )); - continue; - } - Err(err) => { - last_err = Some(err); - continue; - } - }; - let mut ipc_conn = ConnectionTmpl::new(connection); - if let Err(err) = ipc_conn.send(&Data::TerminalSessionCount(0)).await { - last_err = Some(anyhow::anyhow!( - "Failed to request terminal session count via ipc at {}: {}", - socket_path, - err - )); - continue; - } - match ipc_conn.next_timeout(timeout_ms).await { - Ok(Some(Data::TerminalSessionCount(session_count))) => { - return Ok(session_count); - } - Ok(None) => { - last_err = Some(anyhow::anyhow!( - "Invalid response when requesting terminal session count via ipc at {}", - socket_path - )); - } - Ok(other) => { - last_err = Some(anyhow::anyhow!( - "Unexpected response when requesting terminal session count via ipc at {}: {:?}", - socket_path, - other.map(|v| std::mem::discriminant(&v)) - )); - } - Err(err) => { - last_err = Some(anyhow::anyhow!( - "Failed to read terminal session count via ipc at {}: {}", - socket_path, - err - )); - } - } - } - if let Some(err) = last_err { - Err(err.into()) - } else { - Ok(0) + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::TerminalSessionCount(0)).await?; + if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? { + return Ok(c); } + Ok(0) } async fn handle_wayland_screencast_restore_token( @@ -2128,81 +1581,9 @@ pub async fn set_install_option(k: String, v: String) -> ResultType<()> { #[cfg(test)] mod test { use super::*; - #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); assert!(std::mem::size_of::() <= 120); } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_service_ipc_path_is_shared_across_uids() { - assert_eq!( - Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE), - Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE) - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_ipc_path_differs_by_uid_for_cm() { - let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - let other_uid = effective_uid.saturating_add(1); - let postfix = "_cm"; - - // Default connect path targets the current effective uid. - assert_eq!( - Config::ipc_path(postfix), - Config::ipc_path_for_uid(effective_uid, postfix) - ); - // A different uid yields a different socket path - this is the root cause of the - // cross-user regression when root spawns a user process but still connects as uid 0. - assert_ne!( - Config::ipc_path(postfix), - Config::ipc_path_for_uid(other_uid, postfix) - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_uses_active_uid_when_no_server_found() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(), - 501 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_uses_single_server_uid() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(), - 501 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_prefers_active_uid_with_multiple_servers() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(), - 501 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_prefers_root_on_wayland_login_screen() { - assert_eq!( - select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(), - 0 - ); - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() { - assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err()); - } } diff --git a/src/ipc/auth.rs b/src/ipc/auth.rs deleted file mode 100644 index 77fd148c6..000000000 --- a/src/ipc/auth.rs +++ /dev/null @@ -1,1075 +0,0 @@ -use crate::ipc::{Connection, ConnectionTmpl}; -#[cfg(all(windows, not(feature = "flutter")))] -use hbb_common::sha2::{Digest, Sha256}; -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -use hbb_common::{anyhow, bail, log, ResultType}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use hbb_common::{ - libc, - tokio::io::{AsyncRead, AsyncWrite}, -}; -#[cfg(windows)] -use parity_tokio_ipc::SecurityAttributes; -#[cfg(windows)] -use std::io; -#[cfg(all(windows, not(feature = "flutter")))] -use std::io::Read; -#[cfg(target_os = "macos")] -use std::os::unix::fs::MetadataExt; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::os::unix::io::RawFd; -#[cfg(windows)] -use std::os::windows::io::AsRawHandle; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -use std::{ - fs, - path::{Path, PathBuf}, - sync::{Mutex, OnceLock}, -}; -#[cfg(windows)] -use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; - -#[cfg(windows)] -#[inline] -pub(crate) fn should_allow_everyone_create_on_windows(postfix: &str) -> bool { - postfix.is_empty() || hbb_common::config::is_service_ipc_postfix(postfix) -} - -#[cfg(windows)] -#[inline] -pub(crate) fn portable_service_listener_security_attributes() -> io::Result { - let user_sid = crate::platform::windows::current_process_user_sid_string().map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("failed to resolve current process SID: {}", err), - ) - })?; - debug_assert!( - user_sid.starts_with("S-1-") - && user_sid - .bytes() - .all(|byte| byte.is_ascii_digit() || byte == b'-'), - "current_process_user_sid_string returned a non-SDDL SID: {}", - user_sid - ); - // SDDL: - // - `D:P` => protected DACL (no inherited ACEs) - // - `(A;;GA;;;SY)` => allow GENERIC_ALL to LocalSystem - // - `(A;;GA;;;{user_sid})` => allow GENERIC_ALL to current process user SID - // References: - // - Security Descriptor String Format: https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format - // - ACE strings in SDDL: https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings - let sddl = format!("D:P(A;;GA;;;SY)(A;;GA;;;{user_sid})"); - SecurityAttributes::from_sddl(&sddl).map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!( - "failed to build portable service listener security attributes from SDDL '{}': {}", - sddl, err - ), - ) - }) -} - -#[cfg(target_os = "macos")] -#[inline] -fn macos_service_ipc_allows_gui_and_service_binaries( - peer_exe: &Path, - current_exe: &Path, - postfix: &str, -) -> bool { - if postfix != crate::POSTFIX_SERVICE { - return false; - } - let Some(peer_dir) = peer_exe.parent() else { - return false; - }; - let Some(current_dir) = current_exe.parent() else { - return false; - }; - if !executable_paths_match(peer_dir, current_dir) { - return false; - } - - // On installed macOS builds, `_service` is listened by the `service` binary while the GUI - // process connects from the app executable within the same app bundle. - let gui_exe_name = std::ffi::OsString::from(crate::get_app_name()); - let gui_exe = gui_exe_name.as_os_str(); - let service_exe = std::ffi::OsStr::new("service"); - let allowed_exe = [Some(gui_exe), Some(service_exe)]; - let peer_name = peer_exe.file_name(); - let current_name = current_exe.file_name(); - allowed_exe - .iter() - .any(|name| os_str_eq_ignore_ascii_case(peer_name, *name)) - && allowed_exe - .iter() - .any(|name| os_str_eq_ignore_ascii_case(current_name, *name)) -} - -#[cfg(target_os = "windows")] -#[inline] -fn windows_portable_service_ipc_allows_logon_helper_executable( - _peer_exe: &Path, - postfix: &str, -) -> bool { - if postfix != "_portable_service" { - return false; - } - #[cfg(feature = "flutter")] - { - false - } - #[cfg(not(feature = "flutter"))] - { - let Some((_, expected)) = crate::platform::windows::portable_service_logon_helper_paths() - else { - return false; - }; - let Ok(expected) = fs::canonicalize(expected) else { - return false; - }; - let Ok(current_exe) = current_exe_canonical_path() else { - return false; - }; - portable_service_helper_is_trusted(_peer_exe, &expected, ¤t_exe) - } -} - -#[cfg(windows)] -#[inline] -pub(crate) fn is_allowed_windows_session_scoped_peer( - client_is_system: bool, - client_session_id: Option, - expected_session_id: Option, -) -> bool { - client_is_system - || matches!( - (client_session_id, expected_session_id), - (Some(client), Some(expected)) if client == expected - ) -} - -#[cfg(windows)] -#[inline] -fn is_allowed_windows_portable_service_peer( - client_is_system: Option, - _client_session_id: Option, - _expected_session_id: Option, -) -> bool { - // Portable-service listener DACL includes SYSTEM and current-process SID. - // In the portable-service path, current process is expected to run as SYSTEM, - // and the higher-layer peer policy stays SYSTEM-only. - matches!(client_is_system, Some(true)) -} - -#[cfg(any(target_os = "macos", target_os = "linux"))] -#[inline] -pub(crate) fn is_allowed_service_peer_uid(peer_uid: u32, active_uid: Option) -> bool { - // Root is allowed at the UID gate because the service side may run as root. - // Callers still enforce executable matching before accepting service-scoped peers. - peer_uid == 0 || active_uid.is_some_and(|uid| uid == peer_uid) -} - -#[cfg(target_os = "macos")] -#[inline] -fn console_owner_uid() -> Option { - fs::metadata("/dev/console") - .ok() - .map(|metadata| metadata.uid()) -} - -#[cfg(target_os = "macos")] -#[inline] -fn active_uid_strict() -> Option { - // Prefer the filesystem metadata over parsing external command output. - console_owner_uid() -} - -#[cfg(target_os = "linux")] -#[inline] -fn active_uid_strict() -> Option { - let reported_uid_raw = crate::platform::linux::get_active_userid(); - let trimmed = reported_uid_raw.trim(); - if let Ok(uid) = trimmed.parse::() { - return Some(uid); - } - if trimmed.is_empty() { - log::debug!("Failed to resolve active user uid on linux: active uid is empty"); - } else { - log::warn!("Failed to parse active user uid on linux: '{}'", trimmed); - } - None -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -pub(crate) fn active_uid() -> Option { - active_uid_strict() -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -pub(crate) fn peer_uid_from_fd(fd: RawFd) -> Option { - #[cfg(target_os = "linux")] - { - return peer_cred_from_fd(fd).map(|cred| cred.uid as u32); - } - #[cfg(target_os = "macos")] - { - let mut uid = 0; - let mut gid = 0; - if unsafe { libc::getpeereid(fd, &mut uid, &mut gid) } == 0 { - Some(uid as u32) - } else { - None - } - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -fn peer_pid_from_fd(fd: RawFd) -> Option { - #[cfg(target_os = "linux")] - { - return peer_cred_from_fd(fd).and_then(|cred| (cred.pid > 0).then_some(cred.pid as u32)); - } - #[cfg(target_os = "macos")] - { - let mut pid = 0; - let mut len = std::mem::size_of::() as _; - let rc = unsafe { - libc::getsockopt( - fd, - libc::SOL_LOCAL, - libc::LOCAL_PEERPID, - &mut pid as *mut _ as *mut libc::c_void, - &mut len, - ) - }; - if rc == 0 && pid > 0 { - Some(pid as _) - } else { - None - } - } -} - -#[cfg(target_os = "linux")] -#[inline] -fn peer_cred_from_fd(fd: RawFd) -> Option { - let mut cred: libc::ucred = unsafe { std::mem::zeroed() }; - let mut len = std::mem::size_of::() as _; - let rc = unsafe { - libc::getsockopt( - fd, - libc::SOL_SOCKET, - libc::SO_PEERCRED, - &mut cred as *mut _ as *mut libc::c_void, - &mut len, - ) - }; - if rc == 0 { - Some(cred) - } else { - None - } -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -fn current_exe_canonical_path() -> ResultType { - let current = std::env::current_exe() - .map_err(|err| anyhow::anyhow!("Failed to resolve current executable path: {}", err))?; - fs::canonicalize(¤t).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize current executable path '{}': {}", - current.display(), - err - ) - .into() - }) -} - -#[cfg(target_os = "linux")] -#[inline] -fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { - let proc_exe = PathBuf::from(format!("/proc/{peer_pid}/exe")); - let peer_exe = fs::read_link(&proc_exe).map_err(|err| { - anyhow::anyhow!( - "Failed to read peer executable link '{}': {}", - proc_exe.display(), - err - ) - })?; - fs::canonicalize(&peer_exe).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize peer executable path '{}': {}", - peer_exe.display(), - err - ) - .into() - }) -} - -#[cfg(target_os = "macos")] -#[inline] -fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { - const PROC_PIDPATH_BUF_SIZE: usize = libc::PROC_PIDPATHINFO_MAXSIZE as _; - let mut buffer = vec![0u8; PROC_PIDPATH_BUF_SIZE]; - let length = unsafe { - libc::proc_pidpath( - peer_pid as _, - buffer.as_mut_ptr() as _, - PROC_PIDPATH_BUF_SIZE as _, - ) - }; - if length <= 0 { - bail!("Failed to query peer process path from pid {}", peer_pid); - } - buffer.truncate(length as _); - let path = PathBuf::from(String::from_utf8_lossy(&buffer).to_string()); - fs::canonicalize(&path).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize peer executable path '{}': {}", - path.display(), - err - ) - .into() - }) -} - -#[cfg(target_os = "windows")] -#[inline] -fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { - let path = crate::platform::windows::get_process_executable_path(peer_pid)?; - fs::canonicalize(&path).map_err(|err| { - anyhow::anyhow!( - "Failed to canonicalize peer executable path '{}': {}", - path.display(), - err - ) - .into() - }) -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -pub(crate) fn executable_paths_match(left: &Path, right: &Path) -> bool { - #[cfg(target_os = "windows")] - { - // Callers pass paths resolved through fs::canonicalize() first, so NT - // namespace paths and 8.3 short names are expected to be resolved before - // this check. Keep this normalization limited to remaining Win32 spelling - // differences. - fn normalize(path: &Path) -> String { - let mut normalized = path.to_string_lossy().replace('/', "\\"); - if let Some(stripped) = normalized.strip_prefix(r"\\?\") { - normalized = stripped.to_owned(); - } - normalized.to_ascii_lowercase() - } - return normalize(left) == normalize(right); - } - #[cfg(target_os = "macos")] - { - return paths_refer_to_same_file(left, right); - } - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - { - left == right - } -} - -#[cfg(target_os = "macos")] -#[inline] -fn paths_refer_to_same_file(left: &Path, right: &Path) -> bool { - if left == right { - return true; - } - let (Ok(left), Ok(right)) = (fs::metadata(left), fs::metadata(right)) else { - return false; - }; - left.dev() == right.dev() && left.ino() == right.ino() -} - -#[cfg(target_os = "macos")] -#[inline] -fn os_str_eq_ignore_ascii_case( - left: Option<&std::ffi::OsStr>, - right: Option<&std::ffi::OsStr>, -) -> bool { - let (Some(left), Some(right)) = (left, right) else { - return false; - }; - left.to_string_lossy() - .eq_ignore_ascii_case(&right.to_string_lossy()) -} - -#[cfg(all(windows, not(feature = "flutter")))] -#[inline] -fn file_sha256(path: &Path) -> ResultType<[u8; 32]> { - let mut file = fs::File::open(path)?; - let mut hasher = Sha256::new(); - let mut buffer = [0u8; 8 * 1024]; - loop { - let read_bytes = file.read(&mut buffer)?; - if read_bytes == 0 { - break; - } - hasher.update(&buffer[..read_bytes]); - } - Ok(hasher.finalize().into()) -} - -#[cfg(all(windows, not(feature = "flutter")))] -#[inline] -fn portable_service_helper_is_trusted( - peer_exe: &Path, - expected_exe: &Path, - current_exe: &Path, -) -> bool { - if !executable_paths_match(peer_exe, expected_exe) { - return false; - } - let peer_hash = match file_sha256(peer_exe) { - Ok(hash) => hash, - Err(err) => { - log::warn!( - "Failed to hash peer portable helper executable '{}': {}", - peer_exe.display(), - err - ); - return false; - } - }; - let current_hash = match file_sha256(current_exe) { - Ok(hash) => hash, - Err(err) => { - log::warn!( - "Failed to hash current executable '{}' for portable helper trust check: {}", - current_exe.display(), - err - ); - return false; - } - }; - peer_hash == current_hash -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -fn ensure_peer_executable_matches_current_by_pid(peer_pid: u32, postfix: &str) -> ResultType<()> { - let peer_exe = peer_exe_canonical_path_by_pid(peer_pid)?; - let current_exe = current_exe_canonical_path()?; - if executable_paths_match(&peer_exe, ¤t_exe) { - return Ok(()); - } - #[cfg(target_os = "macos")] - if macos_service_ipc_allows_gui_and_service_binaries(&peer_exe, ¤t_exe, postfix) { - return Ok(()); - } - #[cfg(target_os = "windows")] - if windows_portable_service_ipc_allows_logon_helper_executable(&peer_exe, postfix) { - return Ok(()); - } - bail!( - "Peer executable path mismatch on ipc channel '{}': peer_pid={}, peer_exe='{}', current_exe='{}'", - postfix, - peer_pid, - peer_exe.display(), - current_exe.display() - ); -} - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] -#[inline] -pub(crate) fn ensure_peer_executable_matches_current_by_pid_opt( - peer_pid: Option, - postfix: &str, -) -> ResultType<()> { - let peer_pid = peer_pid.ok_or_else(|| { - anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix) - })?; - ensure_peer_executable_matches_current_by_pid(peer_pid, postfix) -} - -#[cfg(target_os = "linux")] -#[inline] -pub(crate) fn ensure_peer_executable_matches_current_by_fd( - fd: RawFd, - postfix: &str, -) -> ResultType<()> { - let peer_pid = peer_pid_from_fd(fd).ok_or_else(|| { - anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix) - })?; - ensure_peer_executable_matches_current_by_pid(peer_pid, postfix) -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -const UNAUTHORIZED_IPC_LOG_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[derive(Default)] -struct UnauthorizedIpcLogThrottle { - last_log_at: Option, - suppressed: u64, -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -impl UnauthorizedIpcLogThrottle { - #[inline] - fn on_reject(&mut self, now: std::time::Instant) -> Option { - if let Some(last) = self.last_log_at { - if now.saturating_duration_since(last) < UNAUTHORIZED_IPC_LOG_INTERVAL { - self.suppressed += 1; - return None; - } - } - self.last_log_at = Some(now); - Some(std::mem::take(&mut self.suppressed)) - } -} - -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -#[inline] -fn throttled_unauthorized_ipc_log( - throttle_cell: &OnceLock>, - emit: impl FnOnce(u64), -) { - let throttle = throttle_cell.get_or_init(|| Mutex::new(UnauthorizedIpcLogThrottle::default())); - let should_log = match throttle.lock() { - Ok(mut throttle) => throttle.on_reject(std::time::Instant::now()), - Err(_) => Some(0), - }; - if let Some(suppressed) = should_log { - emit(suppressed); - } -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[inline] -fn log_rejected_service_connection(postfix: &str, peer_uid: Option, active_uid: Option) { - static LOG_THROTTLE: OnceLock> = OnceLock::new(); - throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { - if suppressed > 0 { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)", - postfix, - peer_uid, - active_uid, - suppressed - ); - } else { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?}", - postfix, - peer_uid, - active_uid - ); - } - }); -} - -#[cfg(target_os = "linux")] -#[inline] -pub(crate) fn log_rejected_uinput_connection( - postfix: &str, - peer_uid: Option, - active_uid: Option, -) { - static LOG_THROTTLE: OnceLock> = OnceLock::new(); - throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { - if suppressed > 0 { - log::warn!( - "Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)", - postfix, - peer_uid, - active_uid, - suppressed - ); - } else { - log::warn!( - "Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?}", - postfix, - peer_uid, - active_uid - ); - } - }); -} - -#[cfg(windows)] -#[inline] -pub(crate) fn log_rejected_windows_ipc_connection( - postfix: &str, - peer_pid: Option, - peer_session_id: Option, - expected_session_id: Option, - peer_is_system: Option, - peer_is_elevated: Option, -) { - static LOG_THROTTLE: OnceLock> = OnceLock::new(); - throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { - if suppressed > 0 { - log::warn!( - "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)", - postfix, - peer_pid, - peer_session_id, - expected_session_id, - peer_is_system, - peer_is_elevated, - suppressed - ); - } else { - log::warn!( - "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}", - postfix, - peer_pid, - peer_session_id, - expected_session_id, - peer_is_system, - peer_is_elevated - ); - } - }); -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postfix: &str) -> bool { - let peer_pid = stream.peer_pid(); - let (authorized, peer_uid, active_uid) = stream.service_authorization_status(); - if !authorized { - log_rejected_service_connection(postfix, peer_uid, active_uid); - return false; - } - if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", - postfix, - peer_pid, - err - ); - return false; - } - true -} - -#[cfg(windows)] -pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool { - let ( - authorized, - peer_pid, - peer_session_id, - server_session_id, - peer_is_system, - peer_is_elevated, - ) = stream.server_authorization_status(); - if !authorized { - log_rejected_windows_ipc_connection( - postfix, - peer_pid, - peer_session_id, - server_session_id, - peer_is_system, - peer_is_elevated, - ); - return false; - } - if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { - log::warn!( - "Rejected unauthorized connection on ipc channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", - postfix, - peer_pid, - err - ); - return false; - } - true -} - -#[cfg(windows)] -pub(crate) fn authorize_windows_portable_service_ipc_connection( - stream: &Connection, - postfix: &str, -) -> bool { - // Portable service IPC policy: - // - only SYSTEM peers are authorized by is_allowed_windows_portable_service_peer() - // - expected_session_id is still collected for diagnostics and identity checks - // - final privilege boundary is enforced by named-pipe ACL + one-time token handshake - // - when peer identity is unavailable on some hosts, executable verification remains - // best-effort telemetry (not fail-closed) to avoid breaking valid SYSTEM bootstrap - // flows that cannot be fully introspected - let expected_session_id = crate::platform::windows::get_current_process_session_id(); - let (authorized, peer_pid, peer_session_id, peer_is_system) = - stream.portable_service_authorization_status_for_session(expected_session_id); - if !authorized { - // Session lookup may succeed while SYSTEM identity lookup fails, so only the - // SYSTEM identity result determines whether peer identity is unavailable here. - // Don't use `peer_pid.is_some() && peer_session_id.is_none() && peer_is_system.is_none();` here. - let identity_unavailable = peer_pid.is_some() && peer_is_system.is_none(); - if identity_unavailable { - // In portable-service startup, resolving SYSTEM peer identity may fail on some hosts. - // `ProcessIdToSessionId` can still succeed while `OpenProcessToken(TOKEN_QUERY)` is - // denied by the peer token DACL or missing privileges. Treat that partial identity - // failure as unavailable and defer final authorization to pipe ACL + token handshake. - if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { - log::warn!( - "Portable service ipc peer identity unavailable and executable verification failed; continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, err={}", - postfix, - peer_pid, - err - ); - } else { - log::warn!( - "Portable service ipc peer identity unavailable; executable verification matched, continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, expected_session_id={:?}", - postfix, - peer_pid, - expected_session_id - ); - } - return true; - } - log::warn!( - "Rejected unauthorized connection on portable service ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}", - postfix, - peer_pid, - peer_session_id, - expected_session_id, - peer_is_system - ); - return false; - } - true -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl ConnectionTmpl -where - T: AsyncRead + AsyncWrite + std::marker::Unpin + std::os::unix::io::AsRawFd, -{ - pub(super) fn peer_uid(&self) -> Option { - peer_uid_from_fd(self.inner.get_ref().as_raw_fd()) - } - - fn service_authorization_status(&self) -> (bool, Option, Option) { - let peer_uid = self.peer_uid(); - // On Linux, `_service` can use the cached active UID from the service loop for - // stable config sync. Uinput does a fresh active-UID lookup in its own authorizer. - let active_uid = active_uid(); - let authorized = peer_uid.is_some_and(|uid| is_allowed_service_peer_uid(uid, active_uid)); - (authorized, peer_uid, active_uid) - } - - pub(super) fn peer_pid(&self) -> Option { - peer_pid_from_fd(self.inner.get_ref().as_raw_fd()) - } -} - -#[cfg(windows)] -impl ConnectionTmpl { - fn peer_pid(&self) -> Option { - let pipe_handle = self.inner.get_ref().as_raw_handle(); - if pipe_handle.is_null() { - return None; - } - let mut pid = 0u32; - let ok = unsafe { GetNamedPipeClientProcessId(HANDLE(pipe_handle), &mut pid as *mut u32) } - .is_ok(); - if ok && pid != 0 { - Some(pid) - } else { - None - } - } - - fn server_authorization_status( - &self, - ) -> ( - bool, - Option, - Option, - Option, - Option, - Option, - ) { - let peer_pid = self.peer_pid(); - let server_session_id = crate::platform::windows::get_current_process_session_id(); - let peer_session_id = - peer_pid.and_then(crate::platform::windows::get_session_id_of_process); - let peer_is_system_result = - peer_pid.map(crate::platform::windows::is_process_running_as_system); - let peer_is_system = peer_is_system_result - .as_ref() - .and_then(|r| r.as_ref().ok().copied()); - let session_authorized = is_allowed_windows_session_scoped_peer( - peer_is_system.unwrap_or(false), - peer_session_id, - server_session_id, - ); - let peer_is_elevated_result = if session_authorized { - None - } else { - peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid))) - }; - let peer_is_elevated = peer_is_elevated_result - .as_ref() - .and_then(|r| r.as_ref().ok().copied()); - if server_session_id.is_none() - && !peer_is_system.unwrap_or(false) - && !peer_is_elevated.unwrap_or(false) - { - // When the server session id cannot be determined, the session-id allow-path is - // disabled and only privileged peers can be authorized. - log::debug!( - "IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}", - peer_pid, - peer_session_id - ); - } - // Main IPC trusts same-session peers, LocalSystem, and elevated administrators. - // Service-scoped IPC channels keep their own stricter authorization paths. - let authorized = session_authorized || peer_is_elevated.unwrap_or(false); - if !authorized { - if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { - log::debug!( - "Failed to determine whether peer process is SYSTEM, pid={}, err={}", - pid, - err - ); - } - if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) { - log::debug!( - "Failed to determine whether peer process is elevated, pid={}, err={}", - pid, - err - ); - } - } - ( - authorized, - peer_pid, - peer_session_id, - server_session_id, - peer_is_system, - peer_is_elevated, - ) - } - - pub(crate) fn service_authorization_status_for_session( - &self, - expected_active_session_id: Option, - ) -> (bool, Option, Option, Option) { - let peer_pid = self.peer_pid(); - let peer_session_id = - peer_pid.and_then(crate::platform::windows::get_session_id_of_process); - let peer_is_system_result = - peer_pid.map(crate::platform::windows::is_process_running_as_system); - let peer_is_system = peer_is_system_result - .as_ref() - .and_then(|r| r.as_ref().ok().copied()); - let authorized = is_allowed_windows_session_scoped_peer( - peer_is_system.unwrap_or(false), - peer_session_id, - expected_active_session_id, - ); - if !authorized { - if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { - log::debug!( - "Failed to determine whether peer process is SYSTEM, pid={}, err={}", - pid, - err - ); - } - } - (authorized, peer_pid, peer_session_id, peer_is_system) - } - - pub(crate) fn portable_service_authorization_status_for_session( - &self, - expected_active_session_id: Option, - ) -> (bool, Option, Option, Option) { - // Portable-service policy: - // only SYSTEM peers are allowed. - let (_service_authorized, peer_pid, peer_session_id, peer_is_system) = - self.service_authorization_status_for_session(expected_active_session_id); - ( - is_allowed_windows_portable_service_peer( - peer_is_system, - peer_session_id, - expected_active_session_id, - ), - peer_pid, - peer_session_id, - peer_is_system, - ) - } -} - -#[cfg(test)] -mod tests { - #[test] - #[cfg(any(target_os = "macos", target_os = "linux"))] - fn test_service_peer_uid_policy() { - assert!(super::is_allowed_service_peer_uid(0, None)); - assert!(super::is_allowed_service_peer_uid(501, Some(501))); - assert!(!super::is_allowed_service_peer_uid(502, Some(501))); - assert!(!super::is_allowed_service_peer_uid(501, None)); - } - - #[test] - #[cfg(windows)] - fn test_windows_server_peer_policy() { - assert!(super::is_allowed_windows_session_scoped_peer( - true, None, None - )); - assert!(super::is_allowed_windows_session_scoped_peer( - false, - Some(1), - Some(1) - )); - assert!(!super::is_allowed_windows_session_scoped_peer( - false, - Some(1), - Some(2) - )); - assert!(!super::is_allowed_windows_session_scoped_peer( - false, - None, - Some(1) - )); - } - - #[test] - #[cfg(windows)] - fn test_windows_portable_service_peer_policy() { - assert!(super::is_allowed_windows_portable_service_peer( - Some(true), - None, - None - )); - assert!(!super::is_allowed_windows_portable_service_peer( - Some(false), - Some(1), - Some(1) - )); - assert!(!super::is_allowed_windows_portable_service_peer( - Some(false), - Some(1), - Some(2) - )); - assert!(!super::is_allowed_windows_portable_service_peer( - None, - Some(1), - Some(1) - )); - } - - #[test] - #[cfg(windows)] - fn test_should_allow_everyone_create_on_windows_policy() { - assert!(super::should_allow_everyone_create_on_windows("")); - assert!(super::should_allow_everyone_create_on_windows("_service")); - assert!(!super::should_allow_everyone_create_on_windows( - "_portable_service" - )); - } - - #[test] - #[cfg(windows)] - fn test_executable_paths_match_windows_normalization() { - let left = std::path::PathBuf::from(r"\\?\C:\Program Files\RustDesk\RustDesk.exe"); - let right = std::path::PathBuf::from(r"c:\program files\rustdesk\rustdesk.exe"); - assert!(super::executable_paths_match(&left, &right)); - } - - #[test] - #[cfg(target_os = "macos")] - fn test_os_str_eq_ignore_ascii_case_for_process_names() { - assert!(super::os_str_eq_ignore_ascii_case( - Some(std::ffi::OsStr::new("RustDesk")), - Some(std::ffi::OsStr::new("rustdesk")) - )); - assert!(!super::os_str_eq_ignore_ascii_case( - Some(std::ffi::OsStr::new("RustDesk")), - Some(std::ffi::OsStr::new("service")) - )); - } - - #[cfg(all(windows, not(feature = "flutter")))] - struct TempDirGuard(std::path::PathBuf); - - #[cfg(all(windows, not(feature = "flutter")))] - impl Drop for TempDirGuard { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(&self.0); - } - } - - #[test] - #[cfg(all(windows, not(feature = "flutter")))] - fn test_portable_service_helper_trust_requires_content_match() { - let unique = format!( - "rustdesk-portable-helper-trust-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - let _cleanup = TempDirGuard(base.clone()); - - let current_exe = base.join("current.exe"); - let helper_exe = base.join("helper.exe"); - std::fs::write(¤t_exe, b"trusted-binary").unwrap(); - std::fs::write(&helper_exe, b"tampered-binary").unwrap(); - - assert!( - !super::portable_service_helper_is_trusted(&helper_exe, &helper_exe, ¤t_exe), - "helper trust check must reject path-match-only binaries with mismatched content" - ); - } - - #[test] - #[cfg(all(windows, not(feature = "flutter")))] - fn test_portable_service_helper_trust_accepts_matching_content() { - let unique = format!( - "rustdesk-portable-helper-trust-match-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - let _cleanup = TempDirGuard(base.clone()); - - let current_exe = base.join("current.exe"); - let helper_exe = base.join("helper.exe"); - std::fs::write(¤t_exe, b"trusted-binary").unwrap(); - std::fs::write(&helper_exe, b"trusted-binary").unwrap(); - - assert!(super::portable_service_helper_is_trusted( - &helper_exe, - &helper_exe, - ¤t_exe - )); - } - - #[cfg(target_os = "macos")] - #[test] - fn test_console_owner_uid_matches_get_active_userid() { - let console_uid = - super::console_owner_uid().expect("/dev/console must have a resolvable uid"); - let raw_uid = crate::platform::macos::get_active_userid(); - let parsed_uid: u32 = raw_uid - .trim() - .parse() - .unwrap_or_else(|_| panic!("failed to parse get_active_userid() output: '{raw_uid}'")); - assert_eq!(parsed_uid, console_uid); - } -} diff --git a/src/ipc/fs.rs b/src/ipc/fs.rs deleted file mode 100644 index e0157f3a9..000000000 --- a/src/ipc/fs.rs +++ /dev/null @@ -1,951 +0,0 @@ -#[cfg(target_os = "linux")] -use super::ipc_auth::active_uid; -use crate::ipc::{connect, Data}; -use hbb_common::{config, log, ResultType}; -use std::{ - ffi::CString, - io::{Error, ErrorKind}, - os::unix::ffi::OsStrExt, - path::Path, -}; - -struct FdGuard(i32); -impl Drop for FdGuard { - fn drop(&mut self) { - unsafe { - hbb_common::libc::close(self.0); - } - } -} - -#[cfg(target_os = "linux")] -#[inline] -pub(crate) fn terminal_count_candidate_uids(effective_uid: u32) -> Vec { - if effective_uid != 0 { - return vec![effective_uid]; - } - let mut candidates = Vec::with_capacity(2); - if let Some(uid) = active_uid().filter(|uid| *uid != 0) { - candidates.push(uid); - } - candidates.push(0); - candidates -} - -#[inline] -fn expected_ipc_parent_mode(postfix: &str) -> u32 { - if config::is_service_ipc_postfix(postfix) { - 0o0711 - } else { - 0o0700 - } -} - -fn open_ipc_parent_dir_fd(parent_c: &CString) -> std::io::Result { - let fd = unsafe { - hbb_common::libc::open( - parent_c.as_ptr(), - hbb_common::libc::O_RDONLY - | hbb_common::libc::O_DIRECTORY - | hbb_common::libc::O_CLOEXEC - | hbb_common::libc::O_NOFOLLOW, - ) - }; - if fd < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(fd) - } -} - -// Remove one preexisting IPC artifact via an already-opened parent directory FD. -// -// Security intent: -// - Bind cleanup to the exact parent inode that passed O_NOFOLLOW + fstat checks. -// - Avoid path-based TOCTOU during scrub (e.g., parent path rename/swap race). -// -// Flow: -// 1) fstatat(..., AT_SYMLINK_NOFOLLOW) to inspect the target entry under parent_fd. -// 2) Decide file vs directory from st_mode. -// 3) unlinkat relative to parent_fd (AT_REMOVEDIR for directories). -// -// Error policy: -// - NotFound is treated as benign (already removed / raced away). -// - Other errors are surfaced explicitly. -fn remove_parent_entry_via_fd( - parent_fd: i32, - parent_dir: &Path, - entry_name: &str, -) -> ResultType<()> { - if entry_name.contains('/') { - return Err(Error::new( - ErrorKind::InvalidInput, - format!( - "invalid ipc parent entry name (contains '/'): parent={}, entry={}", - parent_dir.display(), - entry_name - ), - ) - .into()); - } - let entry_c = CString::new(entry_name.as_bytes().to_vec()).map_err(|err| { - Error::new( - ErrorKind::InvalidInput, - format!( - "invalid ipc parent entry name: parent={}, entry={}, err={}", - parent_dir.display(), - entry_name, - err - ), - ) - })?; - let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - let stat_rc = unsafe { - hbb_common::libc::fstatat( - parent_fd, - entry_c.as_ptr(), - &mut stat, - hbb_common::libc::AT_SYMLINK_NOFOLLOW, - ) - }; - if stat_rc != 0 { - let err = std::io::Error::last_os_error(); - if err.kind() == ErrorKind::NotFound { - return Ok(()); - } - return Err(Error::new( - err.kind(), - format!( - "failed to stat preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}", - parent_dir.display(), - entry_name, - err - ), - ) - .into()); - } - - let is_dir = (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) - == hbb_common::libc::S_IFDIR; - let unlink_flags = if is_dir { - hbb_common::libc::AT_REMOVEDIR - } else { - 0 - }; - let unlink_rc = - unsafe { hbb_common::libc::unlinkat(parent_fd, entry_c.as_ptr(), unlink_flags) }; - if unlink_rc != 0 { - let err = std::io::Error::last_os_error(); - if err.kind() == ErrorKind::NotFound { - return Ok(()); - } - return Err(Error::new( - err.kind(), - format!( - "failed to remove preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}", - parent_dir.display(), - entry_name, - err - ), - ) - .into()); - } - Ok(()) -} - -fn scrub_preexisting_ipc_parent_entries( - parent_fd: i32, - parent_dir: &Path, - postfix: &str, -) -> ResultType<()> { - let ipc_basename = format!("ipc{}", postfix); - remove_parent_entry_via_fd(parent_fd, parent_dir, &ipc_basename)?; - remove_parent_entry_via_fd(parent_fd, parent_dir, &format!("{}.pid", ipc_basename))?; - Ok(()) -} - -fn remove_ipc_socket_via_secure_parent_fd(postfix: &str) -> ResultType<()> { - let path = config::Config::ipc_path(postfix); - let parent_dir = Path::new(&path) - .parent() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; - let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; - let fd = match open_ipc_parent_dir_fd(&parent_c) { - Ok(fd) => fd, - Err(open_err) => { - if open_err.kind() == ErrorKind::NotFound { - return Ok(()); - } - return Err(Error::new( - open_err.kind(), - format!( - "failed to open ipc parent dir for stale socket cleanup (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - open_err - ), - ) - .into()); - } - }; - let _fd_guard = FdGuard(fd); - remove_parent_entry_via_fd(fd, parent_dir, &format!("ipc{}", postfix)) -} - -// Purpose: -// - Harden the IPC parent directory before creating/listening socket files. -// - Prevent symlink/path-race abuse and reject unsafe owner/mode. -// -// Approach: -// - Open parent dir with O_NOFOLLOW/O_DIRECTORY and operate on that fd. -// - Validate inode type/owner/mode via fstat. -// - For protected service postfix, optionally adopt owner (root only), then scrub stale -// rustdesk IPC artifacts when directory trust boundary changed. -// -// Main steps: -// 1) Resolve parent path and open/create directory securely. -// 2) Verify directory inode type and owner uid. -// 3) Enforce expected mode via fchmod on opened fd. -// 4) Scrub stale IPC artifacts when owner/mode was unsafe before hardening. -// -// References: -// - open(2): O_NOFOLLOW/O_DIRECTORY/O_CLOEXEC -// https://man7.org/linux/man-pages/man2/open.2.html -// - fstat(2): verify file type/metadata on opened fd -// https://man7.org/linux/man-pages/man2/fstat.2.html -// - fchown(2): adopt ownership when running as root -// https://man7.org/linux/man-pages/man2/chown.2.html -// - fchmod(2): enforce exact mode on opened fd -// https://man7.org/linux/man-pages/man2/fchmod.2.html -pub(crate) fn ensure_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType { - let parent_dir = Path::new(path) - .parent() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; - // Harden against common TOCTOU by opening the parent directory with O_NOFOLLOW (so the parent - // itself cannot be a symlink) and then operating on its FD (fstat/fchown/fchmod). This ensures - // we mutate the inode we opened, though it does not protect against symlinks in ancestor path - // components. - let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; - let fd = match open_ipc_parent_dir_fd(&parent_c) { - Ok(fd) => fd, - Err(open_err) => { - // If the directory doesn't exist yet, create it with the expected mode. The parent - // dir is intended to be a single-level /tmp path, so mkdir is sufficient here. - if open_err.raw_os_error() == Some(hbb_common::libc::ENOENT) { - let expected_mode = expected_ipc_parent_mode(postfix); - let rc = unsafe { - hbb_common::libc::mkdir( - parent_c.as_ptr(), - expected_mode as hbb_common::libc::mode_t, - ) - }; - if rc != 0 { - let mkdir_err = std::io::Error::last_os_error(); - // Handle a race where another process created the directory first. - if mkdir_err.raw_os_error() != Some(hbb_common::libc::EEXIST) { - return Err(Error::new( - mkdir_err.kind(), - format!( - "failed to mkdir ipc parent dir: postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - mkdir_err - ), - ) - .into()); - } - } - match open_ipc_parent_dir_fd(&parent_c) { - Ok(fd) => fd, - Err(err) => { - return Err(Error::new( - err.kind(), - format!( - "failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - err - ), - ) - .into()); - } - } - } else { - return Err(Error::new( - open_err.kind(), - format!( - "failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - open_err - ), - ) - .into()); - } - } - }; - let _fd_guard = FdGuard(fd); - - let mut st: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut st as *mut _) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to stat ipc parent dir: postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - os_err - ), - ) - .into()); - } - let mode = st.st_mode as u32; - let is_dir = (mode & (hbb_common::libc::S_IFMT as u32)) == (hbb_common::libc::S_IFDIR as u32); - if !is_dir { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "ipc parent is not directory: postfix={}, parent={}", - postfix, - parent_dir.display() - ), - ) - .into()); - } - - let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - let mut owner_uid = st.st_uid as u32; - let mut adopted_foreign_service_parent = false; - // Service-scoped IPC may be created by different privilege contexts historically. - // If running as root on protected service postfix, try adopting ownership first. - if owner_uid != expected_uid && expected_uid == 0 && config::is_service_ipc_postfix(postfix) { - let rc = unsafe { - hbb_common::libc::fchown( - fd, - expected_uid as hbb_common::libc::uid_t, - hbb_common::libc::gid_t::MAX, - ) - }; - if rc == 0 { - let mut st2: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut st2 as *mut _) } == 0 { - owner_uid = st2.st_uid as u32; - st = st2; - adopted_foreign_service_parent = true; - } - } else { - // Keep behavior unchanged; capture errno to ease diagnosing why chown failed. - let err = std::io::Error::last_os_error(); - log::warn!( - "Failed to chown ipc parent dir, parent={}, postfix={}, expected_uid={}, rc={}, err={:?}", - parent_dir.display(), - postfix, - expected_uid, - rc, - err - ); - } - } - if owner_uid != expected_uid { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "unsafe ipc parent owner, postfix={}, expected uid {expected_uid}, got {owner_uid}: {}", - postfix, - parent_dir.display() - ), - ) - .into()); - } - - let expected_mode = expected_ipc_parent_mode(postfix); - // Include special bits (setuid/setgid/sticky) to ensure the directory is hardened to the exact - // expected mode. - let current_mode = (st.st_mode as u32) & 0o7777; - let repaired_parent_mode = current_mode != expected_mode; - let had_untrusted_parent_mode = (current_mode & 0o022) != 0; - if repaired_parent_mode { - // Use fchmod on the opened fd to avoid path-race between check and chmod. - if unsafe { hbb_common::libc::fchmod(fd, expected_mode as hbb_common::libc::mode_t) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to chmod ipc parent dir: postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - os_err - ), - ) - .into()); - } - } - let should_scrub = - repaired_parent_mode || adopted_foreign_service_parent || had_untrusted_parent_mode; - Ok(should_scrub) -} - -pub(crate) fn scrub_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<()> { - let parent_dir = Path::new(path) - .parent() - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; - let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; - let fd = open_ipc_parent_dir_fd(&parent_c).map_err(|err| { - Error::new( - err.kind(), - format!( - "failed to open ipc parent dir for scrub (no-follow): postfix={}, parent={}, err={}", - postfix, - parent_dir.display(), - err - ), - ) - })?; - let _fd_guard = FdGuard(fd); - scrub_preexisting_ipc_parent_entries(fd, parent_dir, postfix) -} - -#[inline] -pub(crate) fn get_pid_file(postfix: &str) -> String { - let path = config::Config::ipc_path(postfix); - format!("{}.pid", path) -} - -// Purpose: -// - Write current process pid to pid file without following attacker-controlled symlinks. -// - Ensure the pid file is a regular file owned by the opened inode path. -// -// Approach: -// - Use libc open/fstat/write syscalls (FFI) so flags and inode validation are explicit. -// - Open file with O_NOFOLLOW/O_CLOEXEC and verify S_IFREG with fstat before write. -// - Keep unsafe scopes minimal and check syscall return values immediately. -// -// Main steps: -// 1) Secure-open pid file (without truncation). -// 2) Validate opened inode is a regular file owned by current euid. -// 3) Enforce pid file mode to 0600 and truncate via ftruncate after validation. -// 4) Write process id bytes through fd. -// -// Why not plain std::fs::write? -// - std::fs helpers cannot enforce this exact open-time hardening sequence -// (especially "open with O_NOFOLLOW, then fstat the same opened inode"). -// -// References: -// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK -// https://man7.org/linux/man-pages/man2/open.2.html -// - fstat(2): verify file type on opened fd -// https://man7.org/linux/man-pages/man2/fstat.2.html -// - fchmod(2): enforce secure mode on reused pid file -// https://man7.org/linux/man-pages/man2/fchmod.2.html -// - ftruncate(2): truncate after validation -// https://man7.org/linux/man-pages/man2/ftruncate.2.html -// - write(2): write bytes via fd -// https://man7.org/linux/man-pages/man2/write.2.html -fn write_pid_file(path: &Path) -> ResultType<()> { - let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).map_err(|err| { - Error::new( - ErrorKind::InvalidInput, - format!("invalid pid file path '{}': {}", path.display(), err), - ) - })?; - let flags = hbb_common::libc::O_WRONLY - | hbb_common::libc::O_CREAT - | hbb_common::libc::O_CLOEXEC - | hbb_common::libc::O_NOFOLLOW - | hbb_common::libc::O_NONBLOCK; - let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags, 0o0600) }; - if fd < 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to open pid file with no-follow '{}': {}", - path.display(), - os_err - ), - ) - .into()); - } - let _fd_guard = FdGuard(fd); - let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!("failed to stat pid file '{}': {}", path.display(), os_err), - ) - .into()); - } - if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) - != (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t) - { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!("pid file path is not a regular file: '{}'", path.display()), - ) - .into()); - } - let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 }; - if stat.st_uid as u32 != expected_uid { - return Err(Error::new( - ErrorKind::PermissionDenied, - format!( - "pid file owner mismatch: expected uid {}, got {} for '{}'", - expected_uid, - stat.st_uid, - path.display() - ), - ) - .into()); - } - if unsafe { hbb_common::libc::fchmod(fd, 0o600) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!("failed to chmod pid file '{}': {}", path.display(), os_err), - ) - .into()); - } - if unsafe { hbb_common::libc::ftruncate(fd, 0) } != 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!( - "failed to truncate pid file '{}': {}", - path.display(), - os_err - ), - ) - .into()); - } - - let bytes = std::process::id().to_string(); - let buf = bytes.as_bytes(); - // `write(2)` is allowed to return a short write even for regular files. - // PID content is tiny and usually written in one shot, but we still loop - // until all bytes are persisted so this path is semantically correct. - let mut written = 0usize; - while written < buf.len() { - let rc = unsafe { - hbb_common::libc::write( - fd, - buf[written..].as_ptr() as *const hbb_common::libc::c_void, - buf.len() - written, - ) - }; - if rc < 0 { - let os_err = std::io::Error::last_os_error(); - return Err(Error::new( - os_err.kind(), - format!("failed to write pid file '{}': {}", path.display(), os_err), - ) - .into()); - } - if rc == 0 { - return Err(Error::new( - ErrorKind::WriteZero, - format!( - "failed to write pid file '{}': write returned 0 bytes", - path.display() - ), - ) - .into()); - } - written += rc as usize; - } - Ok(()) -} - -#[inline] -pub(crate) fn write_pid(postfix: &str) { - let path = std::path::PathBuf::from(get_pid_file(postfix)); - if let Err(err) = write_pid_file(&path) { - log::warn!( - "Failed to write pid file for postfix '{}', path='{}', err={}", - postfix, - path.display(), - err - ); - } -} - -// Purpose: -// - Read pid file safely and avoid trusting symlink/non-regular files. -// -// Approach: -// - Use libc open/fstat/read syscalls (FFI) to control flags and inode checks. -// - Open path with O_NOFOLLOW, validate opened fd via fstat, then read and parse. -// - Keep unsafe scopes minimal and check syscall return values immediately. -// -// Main steps: -// 1) Secure-open pid file read-only. -// 2) Ensure fd points to regular file. -// 3) Read bytes and parse usize pid. -// -// References: -// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK -// https://man7.org/linux/man-pages/man2/open.2.html -// - fstat(2): validate S_IFREG on opened fd -// https://man7.org/linux/man-pages/man2/fstat.2.html -// - read(2): read bytes via fd -// https://man7.org/linux/man-pages/man2/read.2.html -#[inline] -fn read_pid_file_secure(path: &Path) -> Option { - let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).ok()?; - let flags = hbb_common::libc::O_RDONLY - | hbb_common::libc::O_CLOEXEC - | hbb_common::libc::O_NOFOLLOW - | hbb_common::libc::O_NONBLOCK; - let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags) }; - if fd < 0 { - return None; - } - let _fd_guard = FdGuard(fd); - - let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; - if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 { - return None; - } - if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) - != (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t) - { - return None; - } - - let mut buffer = [0u8; 64]; - let read_len = unsafe { - hbb_common::libc::read( - fd, - buffer.as_mut_ptr() as *mut hbb_common::libc::c_void, - buffer.len(), - ) - }; - if read_len <= 0 { - return None; - } - let content = String::from_utf8_lossy(&buffer[..read_len as usize]).to_string(); - content.trim().parse::().ok() -} - -#[inline] -async fn probe_existing_listener(postfix: &str) -> bool { - let Ok(mut stream) = connect(1000, postfix).await else { - return false; - }; - if postfix != crate::POSTFIX_SERVICE { - return true; - } - if stream.send(&Data::SyncConfig(None)).await.is_err() { - return false; - } - matches!( - stream.next_timeout(1000).await, - Ok(Some(Data::SyncConfig(Some(_)))) - ) -} - -pub(crate) async fn check_pid(postfix: &str) -> bool { - let pid_file = std::path::PathBuf::from(get_pid_file(postfix)); - if let Some(pid) = read_pid_file_secure(&pid_file) { - if pid > 0 { - let mut sys = hbb_common::sysinfo::System::new(); - sys.refresh_processes(); - if let Some(p) = sys.process(pid.into()) { - if let Some(current) = sys.process((std::process::id() as usize).into()) { - if current.name() == p.name() && probe_existing_listener(postfix).await { - return true; - } - } - } - } - } - if probe_existing_listener(postfix).await { - return true; - } - // if not remove old ipc file, the new ipc creation will fail - // if we remove a ipc file, but the old ipc process is still running, - // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive - if let Err(err) = remove_ipc_socket_via_secure_parent_fd(postfix) { - log::debug!( - "Failed to remove stale ipc socket via secure parent fd: postfix={}, err={}", - postfix, - err - ); - } - false -} - -#[inline] -pub(crate) fn should_scrub_parent_entries_after_check_pid( - should_scrub_parent_entries: bool, - existing_listener_alive: bool, -) -> bool { - should_scrub_parent_entries && !existing_listener_alive -} - -#[cfg(test)] -mod tests { - #[test] - fn test_write_pid_file_rejects_symlink() { - use std::os::unix::fs::symlink; - - let unique = format!( - "rustdesk-ipc-pid-file-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let target = base.join("target_pid"); - std::fs::write(&target, b"origin").unwrap(); - let link = base.join("pid_link"); - symlink(&target, &link).unwrap(); - - let res = super::write_pid_file(&link); - assert!(res.is_err()); - assert_eq!(std::fs::read_to_string(&target).unwrap(), "origin"); - - std::fs::remove_file(&link).ok(); - std::fs::remove_file(&target).ok(); - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_rejects_symlink_parent() { - use std::os::unix::fs::symlink; - - let unique = format!( - "rustdesk-ipc-secure-dir-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - let real_dir = base.join("real"); - let link_dir = base.join("link"); - std::fs::create_dir_all(&real_dir).unwrap(); - symlink(&real_dir, &link_dir).unwrap(); - let ipc_path = link_dir.join("ipc_service"); - let res = - super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "_service"); - assert!(res.is_err()); - std::fs::remove_file(&link_dir).ok(); - std::fs::remove_dir_all(&real_dir).ok(); - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_creates_parent_with_expected_mode() { - use std::os::unix::fs::PermissionsExt; - - let unique = format!( - "rustdesk-ipc-secure-dir-create-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - // Intentionally choose a parent that does not exist to exercise the ENOENT -> mkdir branch. - let parent_dir = base.join("parent"); - assert!(!parent_dir.exists()); - let ipc_path = parent_dir.join("ipc"); - - let res = super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), ""); - // Restrictive umask can make mkdir create a stricter initial mode. In that case - // ensure_secure_ipc_parent_dir repairs it with fchmod and may request a scrub. - res.unwrap(); - - let md = std::fs::metadata(&parent_dir).unwrap(); - assert!(md.is_dir()); - let mode = md.permissions().mode() & 0o777; - assert_eq!(mode, 0o0700); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_scrub_preexisting_ipc_parent_entries_only_removes_target_postfix_artifacts() { - use std::os::unix::ffi::OsStrExt; - - let unique = format!( - "rustdesk-ipc-scrub-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let ipc_file = base.join("ipc_service"); - let ipc_pid_file = base.join("ipc_service.pid"); - let ipc_other_postfix_file = base.join("ipc_uinput_1"); - let keep_file = base.join("keep.txt"); - let keep_dir = base.join("keep_dir"); - - std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); - std::fs::write(&ipc_pid_file, b"1234").unwrap(); - std::fs::write(&ipc_other_postfix_file, b"other-postfix").unwrap(); - std::fs::write(&keep_file, b"keep").unwrap(); - std::fs::create_dir_all(&keep_dir).unwrap(); - - let base_c = std::ffi::CString::new(base.as_os_str().as_bytes().to_vec()).unwrap(); - let base_fd = super::open_ipc_parent_dir_fd(&base_c).unwrap(); - let _base_guard = super::FdGuard(base_fd); - super::scrub_preexisting_ipc_parent_entries(base_fd, &base, "_service").unwrap(); - - assert!(!ipc_file.exists()); - assert!(!ipc_pid_file.exists()); - assert!(ipc_other_postfix_file.exists()); - assert!(keep_file.exists()); - assert!(keep_dir.exists()); - - std::fs::remove_file(&ipc_other_postfix_file).ok(); - std::fs::remove_file(&keep_file).ok(); - std::fs::remove_dir_all(&keep_dir).ok(); - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_scrub_preexisting_ipc_parent_entries_should_bind_to_opened_inode_not_path() { - use std::os::unix::ffi::OsStrExt; - - let unique = format!( - "rustdesk-ipc-scrub-fd-bind-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let trusted_parent = base.join("trusted_parent"); - let trusted_parent_moved = base.join("trusted_parent_moved"); - let attacker_parent = base.join("attacker_parent"); - std::fs::create_dir_all(&trusted_parent).unwrap(); - std::fs::create_dir_all(&attacker_parent).unwrap(); - - let trusted_ipc_file = trusted_parent.join("ipc_service"); - let attacker_ipc_file = attacker_parent.join("ipc_service"); - std::fs::write(&trusted_ipc_file, b"trusted").unwrap(); - std::fs::write(&attacker_ipc_file, b"attacker").unwrap(); - - let trusted_parent_c = - std::ffi::CString::new(trusted_parent.as_os_str().as_bytes().to_vec()).unwrap(); - let trusted_parent_fd = super::open_ipc_parent_dir_fd(&trusted_parent_c).unwrap(); - let _trusted_parent_guard = super::FdGuard(trusted_parent_fd); - - // Swap the path after the trusted inode has been opened. - std::fs::rename(&trusted_parent, &trusted_parent_moved).unwrap(); - std::fs::rename(&attacker_parent, &trusted_parent).unwrap(); - - super::scrub_preexisting_ipc_parent_entries(trusted_parent_fd, &trusted_parent, "_service") - .unwrap(); - - // Expected secure behavior: scrub should target the inode that was opened before path swap. - assert!( - !trusted_parent_moved.join("ipc_service").exists(), - "trusted inode artifact should be removed even after path swap" - ); - assert!( - trusted_parent.join("ipc_service").exists(), - "path-swapped attacker directory should not be scrubbed" - ); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_keeps_service_artifacts_before_liveness_probe() { - use std::os::unix::fs::PermissionsExt; - - let unique = format!( - "rustdesk-ipc-secure-dir-order-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let parent_dir = base.join("service_parent"); - std::fs::create_dir_all(&parent_dir).unwrap(); - // Trigger "had_untrusted_service_parent_mode". - std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o777)).unwrap(); - - let ipc_file = parent_dir.join("ipc_service"); - let ipc_pid_file = parent_dir.join("ipc_service.pid"); - std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); - std::fs::write(&ipc_pid_file, b"1234").unwrap(); - - let res = - super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "_service"); - assert_eq!(res.unwrap(), true); - - // Parent hardening should run first; artifacts should stay until liveness probe completes. - assert!(ipc_file.exists(), "ipc socket marker should be preserved"); - assert!(ipc_pid_file.exists(), "pid marker should be preserved"); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_ensure_secure_ipc_parent_dir_marks_non_service_mode_repair_for_scrub() { - use std::os::unix::fs::PermissionsExt; - - let unique = format!( - "rustdesk-ipc-nonservice-mode-repair-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - ); - let base = std::env::temp_dir().join(unique); - std::fs::create_dir_all(&base).unwrap(); - - let parent_dir = base.join("non_service_parent"); - std::fs::create_dir_all(&parent_dir).unwrap(); - std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o755)).unwrap(); - - let ipc_file = parent_dir.join("ipc"); - std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); - - let res = super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), ""); - assert_eq!(res.unwrap(), true); - - std::fs::remove_dir_all(&base).ok(); - } - - #[test] - fn test_should_scrub_parent_entries_after_check_pid_only_when_requested_and_not_alive() { - assert!(!super::should_scrub_parent_entries_after_check_pid( - false, false - )); - assert!(!super::should_scrub_parent_entries_after_check_pid( - false, true - )); - assert!(super::should_scrub_parent_entries_after_check_pid( - true, false - )); - assert!(!super::should_scrub_parent_entries_after_check_pid( - true, true - )); - } -} diff --git a/src/keyboard.rs b/src/keyboard.rs index b9cf4da2d..c5d4dfde8 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -82,67 +82,8 @@ lazy_static::lazy_static! { pub mod client { use super::*; - /// Tracks grab ownership and serializes transitions across threads. - /// - /// Multiple Flutter isolates (one per session window) call - /// `change_grab_status(Run/Wait)` concurrently. Without serialization a - /// stale `Wait` from session A can clobber session B's freshly acquired - /// grab on any desktop OS. - /// - /// Windows and macOS are less susceptible in practice because the Flutter - /// side triggers `enterView` only after a mouse click inside the window, - /// but we cannot rely on that. On Linux/X11, `XGrabKeyboard` can also - /// cause a focus-change feedback loop (~10 Hz), so `last_grab` debounces - /// spurious `Wait` events that arrive shortly after a `Run`. - #[derive(Default)] - struct GrabOwnerState { - owner: Option, - last_grab: Option, - /// True while a deferred-release thread is in flight. Prevents - /// spawning redundant threads during the X11 feedback loop. - deferred_pending: bool, - } - - /// How long after a grab acquisition we suppress Wait from the same session. - /// Must exceed one full X11 feedback cycle (~100 ms: 50 ms enable + 50 ms disable). - #[cfg(target_os = "linux")] - const GRAB_DEBOUNCE_MS: u128 = 300; - lazy_static::lazy_static! { static ref IS_GRAB_STARTED: Arc> = Arc::new(Mutex::new(false)); - static ref GRAB_STATE: Arc> = Arc::new(Mutex::new(GrabOwnerState::default())); - } - - #[cfg(target_os = "linux")] - lazy_static::lazy_static! { - static ref GRAB_OP_LOCK: Mutex<()> = Mutex::new(()); - } - - #[cfg(target_os = "linux")] - fn apply_run_grab_if_owner(session_id: u128, disable_first: bool) { - let _lock = GRAB_OP_LOCK.lock().unwrap(); - let gs = GRAB_STATE.lock().unwrap(); - if gs.owner != Some(session_id) { - return; - } - drop(gs); - if disable_first { - log::debug!("[grab] handoff: disable_grab before re-grab"); - rdev::disable_grab(); - } - rdev::enable_grab(); - } - - #[cfg(target_os = "linux")] - fn disable_grab_if_released() { - let _lock = GRAB_OP_LOCK.lock().unwrap(); - let should_disable = { - let gs = GRAB_STATE.lock().unwrap(); - gs.owner.is_none() && gs.last_grab.is_none() - }; - if should_disable { - rdev::disable_grab(); - } } pub fn start_grab_loop() { @@ -155,167 +96,36 @@ pub mod client { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn change_grab_status(state: GrabState, keyboard_mode: &str, session_id: u128) { + pub fn change_grab_status(state: GrabState, keyboard_mode: &str) { #[cfg(feature = "flutter")] if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { return; } - // Serialize transitions so a stale `Wait` from a previous owner cannot - // clobber a fresh `Run` from a different session window. - let mut release_after_unlock = None; - #[cfg(target_os = "linux")] - let mut run_grab_after_unlock = None; - #[cfg(target_os = "linux")] - let mut disable_after_unlock = false; - let mut gs = GRAB_STATE.lock().unwrap(); match state { GrabState::Ready => {} GrabState::Run => { #[cfg(windows)] update_grab_get_key_name(keyboard_mode); - - // Idempotent: if this session already owns the grab, just - // refresh the debounce timer (proves the session is still - // actively focused) and skip the actual grab call. - if gs.owner == Some(session_id) { - gs.last_grab = Some(std::time::Instant::now()); - // Reset so the next Wait can spawn a fresh deferred-release - // timer with an up-to-date snapshot of last_grab. - gs.deferred_pending = false; - log::debug!( - "[grab] Run(0x{:x}): already owner, refresh debounce", - session_id - ); - return; - } - - log::debug!( - "[grab] Run(0x{:x}): prev_owner={}, mode={}", - session_id, - gs.owner - .map_or("none".to_string(), |id| format!("0x{:x}", id)), - keyboard_mode, - ); - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - KEYBOARD_HOOKED.store(true, Ordering::SeqCst); + KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); #[cfg(target_os = "linux")] - let had_owner = gs.owner.is_some(); - gs.owner = Some(session_id); - gs.last_grab = Some(std::time::Instant::now()); - // Invalidate any in-flight deferred release from the previous - // owner so it cannot suppress a fresh timer for the new owner. - gs.deferred_pending = false; - #[cfg(target_os = "linux")] - { - run_grab_after_unlock = Some(had_owner); - } + rdev::enable_grab(); } GrabState::Wait => { - // Drop stale `Wait` events that do not correspond to the - // current grab owner. This prevents a late PointerExit from - // session A from releasing session B's freshly acquired grab. - if gs.owner != Some(session_id) { - log::debug!( - "[grab] Wait(0x{:x}): ignored, owner={}", - session_id, - gs.owner - .map_or("none".to_string(), |id| format!("0x{:x}", id)), - ); - return; - } - - // Debounce: on Linux/X11, XGrabKeyboard causes a focus-change - // feedback loop (grab -> PointerExit -> ungrab -> PointerEnter -> - // grab -> ...). Suppress Wait if the grab was acquired recently - // by this same session -- it is X11 feedback, not a real leave. - // A deferred release is scheduled so that a genuine leave within - // the debounce window is not permanently lost. - #[cfg(target_os = "linux")] - if let Some(t) = gs.last_grab { - let elapsed = t.elapsed().as_millis(); - if elapsed < GRAB_DEBOUNCE_MS { - if !gs.deferred_pending { - log::debug!( - "[grab] Wait(0x{:x}): debounced ({}ms < {}ms), scheduling deferred release", - session_id, elapsed, GRAB_DEBOUNCE_MS, - ); - gs.deferred_pending = true; - let remaining = (GRAB_DEBOUNCE_MS - elapsed) as u64 + 50; - let snapshot = gs.last_grab; - let mode = keyboard_mode.to_string(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(remaining)); - let release_keys = { - let mut gs = GRAB_STATE.lock().unwrap(); - // Release only if no new Run has refreshed the grab since. - if gs.owner == Some(session_id) && gs.last_grab == snapshot { - let to_release = take_remote_keys(); - gs.deferred_pending = false; - log::debug!( - "[grab] Wait(0x{:x}): deferred release", - session_id - ); - KEYBOARD_HOOKED.store(false, Ordering::SeqCst); - gs.owner = None; - gs.last_grab = None; - Some(to_release) - } else { - log::debug!( - "[grab] Wait(0x{:x}): deferred release cancelled (grab refreshed)", - session_id, - ); - None - } - }; - if let Some(to_release) = release_keys { - disable_grab_if_released(); - release_remote_keys_for_events(&mode, to_release); - } - }); - } else { - log::debug!( - "[grab] Wait(0x{:x}): debounced, deferred release already pending", - session_id, - ); - } - return; - } - } - - log::debug!("[grab] Wait(0x{:x}): releasing grab", session_id); - #[cfg(windows)] rdev::set_get_key_unicode(false); - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - KEYBOARD_HOOKED.store(false, Ordering::SeqCst); + release_remote_keys(keyboard_mode); + + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] + KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); - gs.owner = None; - gs.last_grab = None; - gs.deferred_pending = false; - release_after_unlock = Some(take_remote_keys()); #[cfg(target_os = "linux")] - { - disable_after_unlock = true; - } + rdev::disable_grab(); } GrabState::Exit => {} } - drop(gs); - #[cfg(target_os = "linux")] - { - if disable_after_unlock { - disable_grab_if_released(); - } - if let Some(disable_first) = run_grab_after_unlock { - apply_run_grab_if_owner(session_id, disable_first); - } - } - if let Some(to_release) = release_after_unlock { - release_remote_keys_for_events(keyboard_mode, to_release); - } } pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option) { @@ -531,6 +341,7 @@ fn notify_exit_relative_mouse_mode() { flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]); } + /// Handle relative mouse mode shortcuts in the rdev grab loop. /// Returns true if the event should be blocked from being sent to the peer. #[cfg(feature = "flutter")] @@ -729,12 +540,10 @@ pub fn is_long_press(event: &Event) -> bool { return false; } -fn take_remote_keys() -> HashMap { - let mut to_release = TO_RELEASE.lock().unwrap(); - std::mem::take(&mut *to_release) -} - -fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap) { +pub fn release_remote_keys(keyboard_mode: &str) { + // todo!: client quit suddenly, how to release keys? + let to_release = TO_RELEASE.lock().unwrap().clone(); + TO_RELEASE.lock().unwrap().clear(); for (key, mut event) in to_release.into_iter() { event.event_type = EventType::KeyRelease(key); client::process_event(keyboard_mode, &event, None); @@ -749,12 +558,6 @@ fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap KeyboardMode { match keyboard_mode { "map" => KeyboardMode::Map, @@ -945,6 +748,7 @@ pub fn event_to_key_events( ) -> Vec { peer.retain(|c| !c.is_whitespace()); + let mut key_event = KeyEvent::new(); update_modifiers_state(event); match event.event_type { @@ -957,7 +761,6 @@ pub fn event_to_key_events( _ => {} } - let mut key_event = KeyEvent::new(); key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { diff --git a/src/lang.rs b/src/lang.rs index 6302c2aed..4c49c48ca 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -16,10 +16,8 @@ mod es; mod et; mod eu; mod fa; -mod gu; mod fr; mod he; -mod hi; mod hr; mod hu; mod id; @@ -49,7 +47,6 @@ mod vi; mod ta; mod ge; mod fi; -mod ml; pub const LANGS: &[(&str, &str)] = &[ ("en", "English"), @@ -98,9 +95,6 @@ pub const LANGS: &[(&str, &str)] = &[ ("ta", "தமிழ்"), ("ge", "ქართული"), ("fi", "Suomi"), - ("ml", "മലയാളം"), - ("hi", "हिंदी"), - ("gu", "ગુજરાતી"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -179,9 +173,6 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sc" => sc::T.deref(), "ta" => ta::T.deref(), "ge" => ge::T.deref(), - "ml" => ml::T.deref(), - "hi" => hi::T.deref(), - "gu" => gu::T.deref(), _ => en::T.deref(), }; let (name, placeholder_value) = extract_placeholder(&name); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index e13404802..fc1f79c38 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "اعدادات لوحة المفاتيح"), ("Full Access", "وصول كامل"), ("Screen Share", "مشاركة الشاشة"), - ("ubuntu-21-04-required", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."), - ("wayland-requires-higher-linux-version", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."), - ("xdp-portal-unavailable", "لاقط شاشة Wayland فشل. بوابة سطح مكتب XDG ربما توقفت عن العمل او حدث خطأ بها. جرب اعادة تشغليها عن طريق 'systemctl --user restart xdg-desktop-portal'."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."), ("JumpLink", "رابط القفز"), ("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."), ("Show RustDesk", "عرض RustDesk"), @@ -729,21 +728,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"), ("input note here", "أدخل الملاحظة هنا"), ("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"), - ("Show terminal extra keys", "إظهار مفاتيح إضافية في الطرفية"), - ("Relative mouse mode", "وضع الماوس النسبي"), - ("rel-mouse-not-supported-peer-tip", "وضع الماوس النسبي غير مدعوم على الجهاز الآخر"), - ("rel-mouse-not-ready-tip", "وضع الماوس النسبي غير جاهز"), - ("rel-mouse-lock-failed-tip", "فشل قفل الماوس النسبي"), - ("rel-mouse-exit-{}-tip", "للخروج من وضع الماوس النسبي اضغط على {}"), - ("rel-mouse-permission-lost-tip", "تم فقدان إذن الماوس النسبي"), - ("Changelog", "سجل التغييرات"), - ("keep-awake-during-outgoing-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الصادرة"), - ("keep-awake-during-incoming-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الواردة"), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "متابعة مع {}"), - ("Display Name", "اسم العرض"), - ("password-hidden-tip", "كلمة المرور مخفية"), - ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 9f6b69c8b..a7656782d 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -1,19 +1,19 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Стан"), + ("Status", "Статус"), ("Your Desktop", "Ваш працоўны стол"), ("desk_tip", "Ваш працоўны стол даступны з гэтым ID і паролем."), ("Password", "Пароль"), - ("Ready", "Гатова"), + ("Ready", "Гатовы"), ("Established", "Усталявана"), - ("connecting_status", "Ідзе падключэнне да сеткі RustDesk..."), + ("connecting_status", "Падключэнне да сеткі RustDesk..."), ("Enable service", "Уключыць службу"), ("Start service", "Запусціць службу"), ("Service is running", "Служба запушчана"), ("Service is not running", "Служба не запушчана"), - ("not_ready_status", "Не падключана. Праверце падключэнне."), - ("Control Remote Desktop", "Новае падключэнне"), + ("not_ready_status", "Не падключана. Праверце злучэнне."), + ("Control Remote Desktop", "Кіраванне выдаленым працоўным сталом"), ("Transfer file", "Перадаць файлы"), ("Connect", "Падключыцца"), ("Recent sessions", "Апошнія сеансы"), @@ -22,7 +22,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP tunneling", "TCP-тунэляванне"), ("Remove", "Выдаліць"), ("Refresh random password", "Абнавіць выпадковы пароль"), - ("Set your own password", "Задаць свой пароль"), + ("Set your own password", "Усталяваць свой пароль"), ("Enable keyboard/mouse", "Выкарыстоўваць клавіятуру/мыш"), ("Enable clipboard", "Выкарыстоўваць буфер абмену"), ("Enable file transfer", "Выкарыстоўваць перадачу файлаў"), @@ -41,17 +41,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "даўжыня %min%...%max%"), ("starts with a letter", "пачынаецца з літары"), ("allowed characters", "дазволеныя сімвалы"), - ("id_change_tip", "Дазволена выкарыстоўваць толькі сімвалы a-z, A-Z, 0-9, - (dash) і _ (падкрэсліванне). Першай павінна быць літара a-z, A-Z. Даўжыня ад 6 да 16."), + ("id_change_tip", "Дапускаюцца толькі сімвалы a-z, A-Z, 0-9, - (dash) і _ (падкрэсліванне). Першай павінна быць літара a-z, A-Z. Даўжыня ад 6 да 16."), ("Website", "Сайт"), ("About", "Пра праграму"), ("Slogan_tip", "Зроблена з душой у гэтым вар'яцкім свеце!"), - ("Privacy Statement", "Заява аб канфідэнцыйнасці"), + ("Privacy Statement", "Заява аб канфідэнцыяльнасці"), ("Mute", "Адключыць гук"), ("Build Date", "Дата зборкі"), ("Version", "Версія"), ("Home", "Галоўная"), - ("Audio Input", "Аўдыяўваход"), - ("Enhancements", "Паляпшэнні"), + ("Audio Input", "Аўдыёўваход"), + ("Enhancements", "Палепшанні"), ("Hardware Codec", "Апаратны кодэк"), ("Adaptive bitrate", "Адаптыўны бітрэйт"), ("ID Server", "Сервер ID"), @@ -63,10 +63,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server_not_support", "Пакуль не падтрымліваецца серверам"), ("Not available", "Недаступна"), ("Too frequent", "Занадта часта"), - ("Cancel", "Скасаваць"), + ("Cancel", "Адмяніць"), ("Skip", "Прапусціць"), ("Close", "Закрыць"), - ("Retry", "Паўтарыць спробу"), + ("Retry", "Паўтор"), ("OK", "ОК"), ("Password Required", "Патрабуецца пароль"), ("Please enter your password", "Увядзіце пароль"), @@ -75,14 +75,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you want to enter again?", "Паўтарыць уваход?"), ("Connection Error", "Памылка падключэння"), ("Error", "Памылка"), - ("Reset by the peer", "Скінута абанентам"), + ("Reset by the peer", "Скінута выдаленым вузлом"), ("Connecting...", "Падключэнне..."), - ("Connection in progress. Please wait.", "Ідзе падключэнне. Пачакайце."), + ("Connection in progress. Please wait.", "Выконваецца падключэнне. Пачакайце."), ("Please try 1 minute later", "Паспрабуйце праз хвіліну"), ("Login Error", "Памылка ўваходу"), ("Successful", "Паспяхова"), - ("Connected, waiting for image...", "Падключана, чаканне відарыса..."), - ("Name", "Назва"), + ("Connected, waiting for image...", "Падключана, чаканне выявы..."), + ("Name", "Імя"), ("Type", "Тып"), ("Modified", "Зменена"), ("Size", "Памер"), @@ -91,78 +91,78 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Send", "Адправіць"), ("Refresh File", "Абнавіць файл"), ("Local", "Лакальны"), - ("Remote", "Аддалены"), - ("Remote Computer", "Аддалены камп'ютар"), + ("Remote", "Выдалены"), + ("Remote Computer", "Выдалены камп'ютар"), ("Local Computer", "Лакальны камп'ютар"), ("Confirm Delete", "Пацвердзіць выдаленне"), ("Delete", "Выдаліць"), ("Properties", "Уласцівасці"), ("Multi Select", "Шматлікі выбар"), - ("Select All", "Выбраць усе"), - ("Unselect All", "Скасаваць выбар усіх"), - ("Empty Directory", "Пусты каталог"), - ("Not an empty directory", "Каталог не пусты"), + ("Select All", "Абраць усе"), + ("Unselect All", "Зняць усе"), + ("Empty Directory", "Пустая тэчка"), + ("Not an empty directory", "Тэчка не пустая"), ("Are you sure you want to delete this file?", "Выдаліць гэты файл?"), - ("Are you sure you want to delete this empty directory?", "Выдаліць пусты каталог?"), - ("Are you sure you want to delete the file of this directory?", "Выдаліць файл з гэтага каталога?"), + ("Are you sure you want to delete this empty directory?", "Выдаліць пустую тэчку?"), + ("Are you sure you want to delete the file of this directory?", "Выдаліць файл з гэтай тэчкі?"), ("Do this for all conflicts", "Прымяніць да ўсіх канфліктаў"), - ("This is irreversible!", "Гэтага нельга адрабіць!"), - ("Deleting", "Ідзе выдаленне"), + ("This is irreversible!", "Гэта неабаротна!"), + ("Deleting", "Выдаленне"), ("files", "файлы"), ("Waiting", "Чаканне"), ("Finished", "Завершана"), ("Speed", "Хуткасць"), - ("Custom Image Quality", "Карыстальніцкая якасць відарыса"), - ("Privacy mode", "Рэжым канфідэнцыйнасці"), - ("Block user input", "Заблакіраваць увод на аддаленай прыладзе"), - ("Unblock user input", "Разблакіраваць увод на аддаленай прыладзе"), + ("Custom Image Quality", "Якасць выявы па запыце"), + ("Privacy mode", "Рэжым прыватнасці"), + ("Block user input", "Забараніць ўвод на аддаленай прыладзе"), + ("Unblock user input", "Адблакіраваць ўвод на аддаленай прыладзе"), ("Adjust Window", "Наладзіць акно"), ("Original", "Арыгінал"), ("Shrink", "Сціснуць"), ("Stretch", "Расцягнуць"), - ("Scrollbar", "Паласа прагортвання"), - ("ScrollAuto", "Аўта-прагортванне"), - ("Good image quality", "Добрая якасць відарыса"), - ("Balanced", "Баланс паміж якасцю і хуткасцю"), - ("Optimize reaction time", "Аптымізацыя хуткасці рэакцыі"), - ("Custom", "Карыстальніцкая"), + ("Scrollbar", "Паласа пракруткі"), + ("ScrollAuto", "Аўта-пракрутка"), + ("Good image quality", "Добрая якасць выявы"), + ("Balanced", "Баланс паміж якасцю і адказам"), + ("Optimize reaction time", "Оптымізацыя часу адказу"), + ("Custom", "Зададзена карыстальнікам"), ("Show remote cursor", "Паказваць аддалены курсор"), ("Show quality monitor", "Паказваць манітор якасці"), ("Disable clipboard", "Адключыць буфер абмену"), - ("Lock after session end", "Заблакіраваць уліковы запіс пасля сеанса"), + ("Lock after session end", "Заблакаваць уліковы запіс пасля сеансу"), ("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"), - ("Insert Lock", "Заблакіраваць уліковы запіс"), + ("Insert Lock", "Заблакаваць уліковы запіс"), ("Refresh", "Абнавіць"), ("ID does not exist", "ID не існуе"), - ("Failed to connect to rendezvous server", "Немагчыма падключыцца да прамежкавага сервера"), + ("Failed to connect to rendezvous server", "Немагчыма падключыцца да паседкавага сервера"), ("Please try later", "Паспрабуйце пазней"), ("Remote desktop is offline", "Аддаленая прылада не ў сетцы"), ("Key mismatch", "Неадпаведнасць ключоў"), ("Timeout", "Час чакання скончыўся"), ("Failed to connect to relay server", "Немагчыма падключыцца да рэтранслятара"), - ("Failed to connect via rendezvous server", "Немагчыма падключыцца праз прамежкавы сервер"), + ("Failed to connect via rendezvous server", "Немагчыма падключыцца праз паседкавы сервер"), ("Failed to connect via relay server", "Немагчыма падключыцца праз рэтранслятар"), - ("Failed to make direct connection to remote desktop", "Не ўдалося ўсталяваць прамога падключэння да аддаленай прылады"), - ("Set Password", "Задаць пароль"), - ("OS Password", "Пароль уваходу ў аперацыйную сістэму"), - ("install_tip", "У некаторых выпадках з-за UAC, RustDesk можа працаваць на баку абанента неадпаведным чынам. Каб пазбегнуць магчымых праблем з UAC, націсніце кнопку ніжэй для ўсталявання RustDesk у сістэме."), + ("Failed to make direct connection to remote desktop", "Не ўдалося ўсталяваць прамое падключэнне да аддаленага працоўнага стала"), + ("Set Password", "Усталяваць пароль"), + ("OS Password", "Пароль ўваходу ў аперацыйную сістэму"), + ("install_tip", "У некаторых выпадках RustDesk можа працаваць няправільна на аддаленым вузле з-за UAC. Каб пазбегнуць магчымых праблем з UAC, націсніце кнопку ніжэй для ўстаноўкі RustDesk у сістэме."), ("Click to upgrade", "Абнавіць"), ("Configure", "Наладзіць"), - ("config_acc", "Каб аддаленна кіраваць сваім працоўным сталом, вам трэба дазволіць RustDesk правы \"доступу\""), - ("config_screen", "Для аддаленага доступу да працоўнага стала вам трэба даць RustDesk правы \"здымку экрана\"."), - ("Installing ...", "Ідзе ўсталёўванне..."), + ("config_acc", "Каб аддаленна кіраваць сваім працоўным сталом, вам неабходна дазволіць RustDesk правы доступу."), + ("config_screen", "Для аддаленага доступу да працоўнага сталу вам неабходна дазволіць RustDesk правы здымку экрана."), + ("Installing ...", "Ідзе ўстаноўка..."), ("Install", "Усталяваць"), - ("Installation", "Усталёўванне"), - ("Installation Path", "Шлях усталёўвання"), + ("Installation", "Устаноўка"), + ("Installation Path", "Шлях устаноўкі"), ("Create start menu shortcuts", "Стварыць ярлыкі ў меню \"Пуск\""), ("Create desktop icon", "Стварыць значок на працоўным стале"), - ("agreement_tip", "Пачынаючы ўсталёўванне, вы прымаеце ўмовы ліцэнзійнага пагаднення."), + ("agreement_tip", "Пачынаючы ўстаноўку, вы прымаеце ўмовы ліцэнзійнага ўгоды."), ("Accept and Install", "Прыняць і ўсталяваць"), - ("End-user license agreement", "Ліцэнзійнае пагадненне з канчатковым карыстальнікам"), - ("Generating ...", "Ідзе генерыраванне..."), - ("Your installation is lower version.", "Усталявана ранейшая версія"), - ("not_close_tcp_tip", "Не закрываць гэтага акна пры выкарыстанні тунэлю."), - ("Listening ...", "Чаканне..."), + ("End-user license agreement", "Ліцэнзійная ўгода з канчатковым карыстальнікам"), + ("Generating ...", "Генеруецца..."), + ("Your installation is lower version.", "Ваша ўстаноўка ніжэйшай версіі"), + ("not_close_tcp_tip", "Не зачыняць гэта акно пры выкарыстанні тунэлю."), + ("Listening ...", "Праслухоўванне..."), ("Remote Host", "Аддалены хост"), ("Remote Port", "Аддалены порт"), ("Action", "Дзеянне"), @@ -170,120 +170,120 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Лакальны порт"), ("Local Address", "Лакальны адрас"), ("Change Local Port", "Змяніць лакальны порт"), - ("setup_server_tip", "Для хутчэйшага падключэння наладзьце ўласны сервер."), + ("setup_server_tip", "Для хуткага падключэння наладзьце свой сервер."), ("Too short, at least 6 characters.", "Занадта кароткі, мінімум 6 сімвалаў."), - ("The confirmation is not identical.", "Пацвярджэнне не супадае."), + ("The confirmation is not identical.", "Пацверджанне не супадае."), ("Permissions", "Дазволы"), ("Accept", "Прыняць"), ("Dismiss", "Адхіліць"), ("Disconnect", "Адключыць"), - ("Enable file copy and paste", "Дазволіць капіяванне і ўстаўку файлаў"), + ("Enable file copy and paste", "Дазволіць капіраванне і ўстаўку файлаў"), ("Connected", "Падключана"), ("Direct and encrypted connection", "Прамое і зашыфраванае падключэнне"), ("Relayed and encrypted connection", "Рэтрансляванае і зашыфраванае падключэнне"), ("Direct and unencrypted connection", "Прамое і незашыфраванае падключэнне"), ("Relayed and unencrypted connection", "Рэтрансляванае і незашыфраванае падключэнне"), - ("Enter Remote ID", "Увядзіце ID абанента"), + ("Enter Remote ID", "Увядзіце дыстанцыйны ID"), ("Enter your password", "Увядзіце пароль"), - ("Logging in...", "Уваходжанне..."), - ("Enable RDP session sharing", "Уключыць абагульванне сеанса RDP"), - ("Auto Login", "Аўтаматычны ўваход ва ўліковы запіс"), + ("Logging in...", "Уваход..."), + ("Enable RDP session sharing", "Дазволіць абмен сеансамі RDP"), + ("Auto Login", "Аўтаматычны ўваход у ўліковы запіс"), ("Enable direct IP access", "Дазволіць прамы доступ па IP-адрасе"), ("Rename", "Перайменаваць"), ("Space", "Месца"), ("Create desktop shortcut", "Стварыць ярлык на працоўным стале"), ("Change Path", "Змяніць шлях"), - ("Create Folder", "Стварыць папку"), - ("Please enter the folder name", "Увядзіце імя папкі"), + ("Create Folder", "Стварыць тэчку"), + ("Please enter the folder name", "Калі ласка, увядзіце імя тэчкі"), ("Fix it", "Выправіць"), ("Warning", "Папярэджанне"), ("Login screen using Wayland is not supported", "Уваход у сістэму з выкарыстаннем Wayland не падтрымліваецца"), ("Reboot required", "Патрабуецца перазагрузка"), - ("Unsupported display server", "Сервер адлюстравання не падтрымліваецца"), + ("Unsupported display server", "Непадтрымліваемы сервер адлюстравання"), ("x11 expected", "Чакаецца X11"), ("Port", "Порт"), ("Settings", "Налады"), ("Username", "Імя карыстальніка"), - ("Invalid port", "Памылковы порт"), - ("Closed manually by the peer", "Закрыта абанентам уручную"), - ("Enable remote configuration modification", "Дазволіць аддаленае змяненне канфігурацыі"), - ("Run without install", "Запусціць без усталявання"), + ("Invalid port", "Няправільны порт"), + ("Closed manually by the peer", "Зачынена аддаленым вузлом уручную"), + ("Enable remote configuration modification", "Дазволіць змену канфігурацыі аддалена"), + ("Run without install", "Запусціць без ўстаноўкі"), ("Connect via relay", "Падключыцца праз рэтранслятар"), ("Always connect via relay", "Заўсёды падключацца праз рэтранслятар"), - ("whitelist_tip", "Атрымліваць доступ да маёй прылады могуць толькі IP-адрасы з белага спісу."), + ("whitelist_tip", "Толькі IP-адрэсы з белага спісу могуць атрымаць доступ да маёй прылады."), ("Login", "Увайсці"), ("Verify", "Праверыць"), - ("Remember me", "Запомніць"), - ("Trust this device", "Давяраць гэтай прыладзе"), + ("Remember me", "Запомніць мяне"), + ("Trust this device", "Даверыць гэтую прыладу"), ("Verification code", "Праверачны код"), - ("verification_tip", "Выяўлена новая прылада, на зарэгістраваны адрас электроннай пошты адпраўлены праверачны код. Увядзіце яго, каб працягнуць уваходжанне ў сістэму."), + ("verification_tip", "Выяўлена новая прылада, на зарэгістраваны адрас электроннай пошты адпраўлены праверачны код. Увядзіце яго, каб працягнуць уваход у сістэму."), ("Logout", "Выйсці"), - ("Tags", "Цэтлікі"), + ("Tags", "Тэгі"), ("Search ID", "Пошук по ID"), - ("whitelist_sep", "Падзяленне коскай, кропкай з коскай, прабелам або новым радком."), + ("whitelist_sep", "Аддзяліць запятой, коскай з запятой, прабелам ці новым радком."), ("Add ID", "Дадаць ID"), - ("Add Tag", "Дадаць цэтлік"), - ("Unselect all tags", "Скасаваць выбар усіх цэтлікаў"), + ("Add Tag", "Дадаць тэг"), + ("Unselect all tags", "Скасаваць выбар усіх тэгаў"), ("Network error", "Памылка сеткі"), - ("Username missed", "Прапушчана імя карыстальніка"), - ("Password missed", "Прапушчаны пароль"), - ("Wrong credentials", "Памылковае імя або пароль"), - ("The verification code is incorrect or has expired", "Памылковы або пратэрмінаваны праверачны код"), - ("Edit Tag", "Рэдагаваць цэтлік"), - ("Forget Password", "Не захоўваць пароль"), + ("Username missed", "Адсутнічае імя карыстальніка"), + ("Password missed", "Забыты пароль"), + ("Wrong credentials", "Няправільныя імя ці пароль"), + ("The verification code is incorrect or has expired", "Праверачны код няправільны або скончыўся тэрмін яго дзеяння"), + ("Edit Tag", "Рэдагаваць тэг"), + ("Forget Password", "Забыць пароль"), ("Favorites", "Абранае"), ("Add to Favorites", "Дадаць у абранае"), ("Remove from Favorites", "Выдаліць з абранага"), ("Empty", "Пуста"), - ("Invalid folder name", "Недапушчальная назва папкі"), + ("Invalid folder name", "Недапушчальнае імя тэчкі"), ("Socks5 Proxy", "Socks5-проксі"), ("Socks5/Http(s) Proxy", "Socks5/Http(s)-проксі"), ("Discovered", "Знойдзена"), - ("install_daemon_tip", "Для запуску пры загрузцы трэба ўсталяваць сістэмную службу"), - ("Remote ID", "ID абанента"), + ("install_daemon_tip", "Для запуску пры загрузцы неабходна ўстанавіць сістэмную службу"), + ("Remote ID", "Аддалены ID"), ("Paste", "Уставіць"), - ("Paste here?", "Уставіць сюды?"), - ("Are you sure to close the connection?", "Закрыць падключэнне?"), + ("Paste here?", "Уставіць тут?"), + ("Are you sure to close the connection?", "Ці ўпэўненыя, што жадаеце закрыць падключэнне?"), ("Download new version", "Спампаваць новую версію"), ("Touch mode", "Рэжым сэнсарнага экрана"), - ("Mouse mode", "Рэжым мышы/сэнсарнай панэлі"), - ("One-Finger Tap", "Націсканне адным пальцам"), + ("Mouse mode", "Рэжым мышы/трэкпада"), + ("One-Finger Tap", "Націск адным пальцам"), ("Left Mouse", "Левая кнопка мышы"), - ("One-Long Tap", "Доўгае націсканне адным пальцам"), - ("Two-Finger Tap", "Націсканне двума пальцамі"), + ("One-Long Tap", "Доўгі націск адным пальцам"), + ("Two-Finger Tap", "Націск двума пальцамі"), ("Right Mouse", "Правая кнопка мышы"), ("One-Finger Move", "Перамяшчэнне адным пальцам"), - ("Double Tap & Move", "Двайное націсканне і перамяшчэнне"), + ("Double Tap & Move", "Двайны націск і перамяшчэнне"), ("Mouse Drag", "Перацягванне мышшу"), ("Three-Finger vertically", "Трыма пальцамі па вертыкалі"), - ("Mouse Wheel", "Колца мышы"), + ("Mouse Wheel", "Кола мышы"), ("Two-Finger Move", "Перамяшчэнне двума пальцамі"), ("Canvas Move", "Перамяшчэнне палатна"), - ("Pinch to Zoom", "Маштабаванне шчыпком"), - ("Canvas Zoom", "Маштабаванне палатна"), - ("Reset canvas", "Скінуць маштабаванне палатна"), + ("Pinch to Zoom", "Маштабаванне сціскам"), + ("Canvas Zoom", "Маштаб палатна"), + ("Reset canvas", "Скінуць палатно"), ("No permission of file transfer", "Няма дазволу на перадачу файлаў"), ("Note", "Нататка"), ("Connection", "Падключэнне"), - ("Share screen", "Дэманстрацыя экрана"), + ("Share screen", "Дзяліцца экранам"), ("Chat", "Чат"), ("Total", "Усяго"), ("items", "элементы"), ("Selected", "Выбрана"), ("Screen Capture", "Захоп экрана"), ("Input Control", "Кіраванне ўводам"), - ("Audio Capture", "Захоп аўдыя"), - ("Do you accept?", "Вы згодныя?"), + ("Audio Capture", "Захоп аўдыё"), + ("Do you accept?", "Ці вы згодны?"), ("Open System Setting", "Адкрыць налады сістэмы"), ("How to get Android input permission?", "Як атрымаць дазвол на ўвод Android?"), - ("android_input_permission_tip1", "Каб аддаленая прылада магла кіраваць вашай Android-прыладай з дапамогай мышы або націсканняў, трэба дазволіць RustDesk выкарыстоўваць службу \"Спецыяльныя магчымасці\"."), - ("android_input_permission_tip2", "Зайдзіце на адпаведную старонку сістэмных налад, знайдзіце і перайдзіце ва \"Усталяваныя службы\", уключыце службу \"RustDesk Input\"."), - ("android_new_connection_tip", "Новы запыт на кіраванне вашай бягучай прыладай."), - ("android_service_will_start_tip", "Уключэнне захопу экрана аўтаматычна запускае службу, дазваляючы іншым прыладам запытаць падключэнне да гэтай прылады."), - ("android_stop_service_tip", "Закрыццё службы аўтаматычна закрые ўсе ўстаноўленыя падключэнні."), - ("android_version_audio_tip", "Бягучая версія Android не падтрымлівае захопу гуку, абнавіце яе да Android 10 ці вышэй."), + ("android_input_permission_tip1", "Каб аддалёная прылада магла кіраваць вашай Android-прыладай з дапамогай мышы або націсканняў, неабходна дазволіць RustDesk выкарыстоўваць паслугу \"Асаблівыя магчымасці\"."), + ("android_input_permission_tip2", "Зайдзіце на адпаведную старонку сістэмных налад, знайдзіце і ўступіце ў \"Устаноўленыя паслугі\", уключыце паслугу \"RustDesk Input\"."), + ("android_new_connection_tip", "Атрыманы запыт на кіраванне вашай бягучай прыладай."), + ("android_service_will_start_tip", "Уключэнне захопу экрана аўтаматычна запускае службу, дазваляючы іншым прыладам запытаць падлучэнне да гэтай прылады."), + ("android_stop_service_tip", "Закрыццё службы аўтаматычна зачыніць усе ўстаноўленыя падлучэнні."), + ("android_version_audio_tip", "Бягучая версія Android не падтрымлівае захоп звуку, абнавіце яе да Android 10 ці вышэй."), ("android_start_service_tip", "Націсніце [Запусціць службу] або дазвольце [Захоп экрана], каб запусціць службу дэманстрацыі экрана."), - ("android_permission_may_not_change_tip", "Дазволы для ўстаноўленых падключэнняў не могуць быць зменены, патрабуецца перападключэнне."), + ("android_permission_may_not_change_tip", "Дазволы для ўстаноўленых падлучэнняў не могуць быць змененыя, неабходна перападключэнне."), ("Account", "Уліковы запіс"), ("Overwrite", "Перазапісаць"), ("This file exists, skip or overwrite this file?", "Файл існуе, прапусціць ці перазапісаць яго?"), @@ -291,47 +291,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Help", "Дапамога"), ("Failed", "Не ўдалося"), ("Succeeded", "Выканана"), - ("Someone turns on privacy mode, exit", "Хтосьці ўключыў рэжым канфідэнцыйнасці, выхад"), + ("Someone turns on privacy mode, exit", "Хтосьці ўключыў рэжым прыватнасці, выхад"), ("Unsupported", "Не падтрымліваецца"), - ("Peer denied", "Забаронена абанентам"), - ("Please install plugins", "Усталюйце ўбудовы"), - ("Peer exit", "Абанент выйшаў"), - ("Failed to turn off", "Немагчыма выключыць"), - ("Turned off", "Выключаны"), + ("Peer denied", "Адмоўлена аддаленым вузлом"), + ("Please install plugins", "Усталюйце плагіны"), + ("Peer exit", "Аддалены вузел адключаны"), + ("Failed to turn off", "Немагчыма адключыць"), + ("Turned off", "Адключаны"), ("Language", "Мова"), ("Keep RustDesk background service", "Захаваць фонавую службу RustDesk"), - ("Ignore Battery Optimizations", "Ігнараваць аптымізацыю ўжывання батарэі"), + ("Ignore Battery Optimizations", "Ігнараваць аптымізацыю патрэблення батарэі"), ("android_open_battery_optimizations_tip", "Перайдзіце на наступную старонку налад"), ("Start on boot", "Запускаць пры загрузцы"), ("Start the screen sharing service on boot, requires special permissions", "Запускаць службу дэманстрацыі экрана пры загрузцы (патрабуюцца спецыяльныя дазволы)"), ("Connection not allowed", "Падключэнне не дазволена"), - ("Legacy mode", "Састарэлы рэжым"), + ("Legacy mode", "Стары рэжым"), ("Map mode", "Рэжым супастаўлення"), ("Translate mode", "Рэжым перакладу"), ("Use permanent password", "Выкарыстоўваць пастаянны пароль"), ("Use both passwords", "Выкарыстоўваць абодва паролі"), - ("Set permanent password", "Задаць пастаянны пароль"), + ("Set permanent password", "Устанавіць пастаянны пароль"), ("Enable remote restart", "Дазволіць аддалены перазапуск"), ("Restart remote device", "Перазапусціць аддаленую прыладу"), - ("Are you sure you want to restart", "Вы ўпэўненыя, што хочаце зрабіць перазапуск?"), - ("Restarting remote device", "Ідзе перазапуск аддаленай прылады"), - ("remote_restarting_tip", "Аддаленая прылада перазапускаецца. Закрыйце гэта паведамленне і праз некаторы час перападключыцеся, выкарыстоўваючы пастаянны пароль."), - ("Copied", "Скапіявана"), + ("Are you sure you want to restart", "Вы ўпэўненыя, што хочаце перазагрузіць?"), + ("Restarting remote device", "Перазапуск аддаленай прылады"), + ("remote_restarting_tip", "Аддаленая прылада перазапускаецца. Закрыйце гэтае паведамленне і праз некаторы час перападключыцеся, выкарыстоўваючы пастаянны пароль."), + ("Copied", "Скапіравана"), ("Exit Fullscreen", "Выйсці з поўнаэкраннага рэжыму"), ("Fullscreen", "Поўнаэкранны рэжым"), ("Mobile Actions", "Мабільныя дзеянні"), - ("Select Monitor", "Выберыце манітор"), - ("Control Actions", "Дзеянні па кіраванні"), + ("Select Monitor", "Выбраць манітор"), + ("Control Actions", "Дзеянні па кіраванню"), ("Display Settings", "Налады адлюстравання"), ("Ratio", "Суадносіны"), - ("Image Quality", "Якасць відарыса"), - ("Scroll Style", "Стыль прагортвання"), + ("Image Quality", "Якасць выявы"), + ("Scroll Style", "Стыль пракруткі"), ("Show Toolbar", "Паказаць панэль інструментаў"), ("Hide Toolbar", "Схаваць панэль інструментаў"), - ("Direct Connection", "Прамое падключэнне"), - ("Relay Connection", "Рэтрансляванае падключэнне"), - ("Secure Connection", "Бяспечнае падключэнне"), - ("Insecure Connection", "Нябяспечнае падключэнне"), + ("Direct Connection", "Прамаое злучэнне"), + ("Relay Connection", "Рэтрансляванае злучэнне"), + ("Secure Connection", "Бяспечнае злучэнне"), + ("Insecure Connection", "Нябяспечнае злучэнне"), ("Scale original", "Арыгінальны маштаб"), ("Scale adaptive", "Адаптыўны маштаб"), ("General", "Агульныя"), @@ -339,13 +339,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Theme", "Тэма"), ("Dark Theme", "Цёмная тэма"), ("Light Theme", "Светлая тэма"), - ("Dark", "Цёмная"), - ("Light", "Светлая"), - ("Follow System", "Сістэмная"), + ("Dark", "Цёмны"), + ("Light", "Светлы"), + ("Follow System", "Прытрымлівацца сістэмы"), ("Enable hardware codec", "Уключыць апаратны кодэк"), - ("Unlock Security Settings", "Разблакіраваць налады бяспекі"), + ("Unlock Security Settings", "Разблакаваць налады бяспекі"), ("Enable audio", "Уключыць перадачу гуку"), - ("Unlock Network Settings", "Разблакіраваць сеткавыя налады"), + ("Unlock Network Settings", "Разблакаваць сеткавыя налады"), ("Server", "Сервер"), ("Direct IP Access", "Прамы IP-доступ"), ("Proxy", "Проксі"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pin Toolbar", "Закрэпіць панэль інструментаў"), ("Unpin Toolbar", "Адкрэпіць панэль інструментаў"), ("Recording", "Запіс"), - ("Directory", "Каталог"), + ("Directory", "Тэчка"), ("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"), ("Automatically record outgoing sessions", ""), ("Change", "Змяніць"), @@ -370,35 +370,34 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Напісаць паведамленне"), ("Prompt", "Падказка"), ("Please wait for confirmation of UAC...", "Дачакайцеся пацверджання UAC..."), - ("elevated_foreground_window_tip", "Бягучае акно аддаленага працоўнага стала патрабуе вышэйшых прывілегій для працы, таму часова немагчыма выкарыстоўваць мыш і клавіятуру. Можна папрасіць абанента згарнуць бягучае акно або націснуць кнопку павышэння правоў у акне кіравання падключэннем. Каб прадухіліць гэту праблему ў будучыні, рэкамендуецца ўсталяваць праграмнае забеспячэнне на аддаленай прыладзе."), + ("elevated_foreground_window_tip", "Бягучае акно аддаленага працоўнага стала патрабуе вышэйшых прывілегій для працы, таму часова немагчыма выкарыстоўваць мыш і клавіятуру. Можна папрасіць аддаленага карыстальніка згорнуць бягучае акно або націснуць кнопку павышэння правоў у акне кіравання падлучэннем. Каб прадухіліць гэтую праблему ў будучыні, рэкамендуецца ўстанавіць праграмнае забеспячэнне на аддаленай прыладзе."), ("Disconnected", "Адключана"), ("Other", "Іншае"), - ("Confirm before closing multiple tabs", "Пацвердзіць закрыццё некалькіх укладак"), + ("Confirm before closing multiple tabs", "Пацвердзіць закрыццё некалькіх ўкладак"), ("Keyboard Settings", "Налады клавіятуры"), ("Full Access", "Поўны доступ"), ("Screen Share", "Дэманстрацыя экрана"), - ("ubuntu-21-04-required", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."), - ("wayland-requires-higher-linux-version", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыва Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), - ("xdp-portal-unavailable", ""), - ("JumpLink", "Прагляд"), - ("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца на баку абанента)."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыву Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), + ("JumpLink", "Перайсці па спасылцы"), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца аддаленай стараной)."), ("Show RustDesk", "Паказаць RustDesk"), - ("This PC", "Гэты камп’ютар"), + ("This PC", "Гэты кампутар"), ("or", "або"), ("Elevate", "Павысіць"), - ("Zoom cursor", "Маштабаванне курсора"), + ("Zoom cursor", "Павялічэнне курсора"), ("Accept sessions via password", "Прымаць сеансы па паролю"), ("Accept sessions via click", "Прымаць сеансы націскам кнопкі"), ("Accept sessions via both", "Прымаць сеансы па паролю і націскам кнопкі"), - ("Please wait for the remote side to accept your session request...", "Дачакайцеся, пакуль абанент прымае ваш запыт на сеанс..."), + ("Please wait for the remote side to accept your session request...", "Дачакайцеся, пакуль аддаленая старана прыме ваш запыт на сеанс..."), ("One-time Password", "Аднаразовы пароль"), ("Use one-time password", "Выкарыстоўваць аднаразовы пароль"), ("One-time password length", "Даўжыня аднагаразовага пароля"), ("Request access to your device", "Запыт на доступ да вашай прылады"), - ("Hide connection management window", "Схаваць акно кіравання падключэннямі"), + ("Hide connection management window", "Схаваць акно кіравання падлучэннямі"), ("hide_cm_tip", "Дазваляць схаванне акна ў выпадку, калі прымаюцца сесіі па паролю або выкарыстоўваецца пастаянны пароль"), - ("wayland_experiment_tip", "Падтрымка Wayland знаходзіцца на эксперыментальнай стадыі, калі вам трэба аўтаматычны доступ, выкарыстоўвайце X11."), - ("Right click to select tabs", "Выбар укладак націсканнем правай кнопкі мышы"), + ("wayland_experiment_tip", "Падтрымка Wayland знаходзіцца на эксперыментальнай стадыі, калі вам неабходны аўтаматычны доступ, выкарыстоўвайце X11."), + ("Right click to select tabs", "Правы клік для выбару ўкладак"), ("Skipped", "Прапушчана"), ("Add to address book", "Дадаць у адрасную кнігу"), ("Group", "Група"), @@ -406,71 +405,71 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Закрыта ўручную праз вэб-кансоль"), ("Local keyboard type", "Тып лакальнай клавіятуры"), ("Select local keyboard type", "Выберыце тып лакальнай клавіятуры"), - ("software_render_tip", "Калі ў вас ёсць відэакарта Nvidia і аддаленае акно закрываецца адразу пасля падключэння, магчыма, дапаможа ўсталяванне драйвера Nouveau і выбар выкарыстання праграмнай візуалізацыі. Патрабуецца перазагрузка."), + ("software_render_tip", "Калі ў вас ёсць відэакарта Nvidia і аддаленае акно зачыняецца адразу пасля падлучэння, магчыма, дапаможа ўстаноўка драйвера Nouveau і выбар выкарыстання праграмнай візуалізацыі. Патрабуецца перазагрузка."), ("Always use software rendering", "Заўсёды выкарыстоўваць праграмную візуалізацыю"), - ("config_input", "Каб кіраваць аддаленым працоўным сталом праз клавіятуру, трэба дазволіць RustDesk \"Маніторынг уводу\"."), - ("config_microphone", "Каб размаўляць з абанентам, трэба дазволіць RustDesk запіс аўдыя."), - ("request_elevation_tip", "Таксама можна запытаць павышэння правоў, калі хто-небудзь знаходзіцца на баку абанента."), + ("config_input", "Каб кіраваць аддаленым працоўным сталом праз клавіятуру, неабходна дазволіць RustDesk маніторынг уводу."), + ("config_microphone", "Каб размаўляць з аддаленай старонкай, неабходна дазволіць RustDesk запіс аўдыё."), + ("request_elevation_tip", "Таксама можна запытаць павышэнне правоў, калі хто-небудзь знаходзіцца на аддаленай старонцы."), ("Wait", "Чакайце"), ("Elevation Error", "Памылка павышэння правоў"), - ("Ask the remote user for authentication", "Запытаць праверку сапраўднасці ў абанента"), - ("Choose this if the remote account is administrator", "Выберыце гэта, калі абанент з'яўляецца адміністратарам"), + ("Ask the remote user for authentication", "Запытаць аўтэнтыфікацыю ў аддаленага карыстальніка"), + ("Choose this if the remote account is administrator", "Выберыце гэта, калі аддалены акаўнт з'яўляецца адміністратарам"), ("Transmit the username and password of administrator", "Перадаць імя карыстальніка і пароль адміністратара"), - ("still_click_uac_tip", "Дагэтуль патрэбна, каб абанент націснуў \"OK\" ў акне UAC пры запуску RustDesk."), - ("Request Elevation", "Запытаць павышэння"), - ("wait_accept_uac_tip", "Пачакайце, пакуль абанент пацвердзіць запыт UAC."), - ("Elevate successfully", "Правы павышаны"), - ("uppercase", "верхні рэгістр"), - ("lowercase", "ніжні рэгістр"), - ("digit", "лічбы"), - ("special character", "спецыяльныя сімвалы"), - ("length>=8", "8+ сімвалаў"), + ("still_click_uac_tip", "Дагэтуль патрэбна, каб аддалены карыстальнік націснуў \"OK\" ў акне UAC пры запуску RustDesk."), + ("Request Elevation", "Запыт на павышэнне"), + ("wait_accept_uac_tip", "Пачакайце, пакуль аддалены карыстальнік пацвердзіць запыт UAC."), + ("Elevate successfully", "Павышэнне паспяхова выканана"), + ("uppercase", "Вялікія літары"), + ("lowercase", "Малыя літары"), + ("digit", "Лічбы"), + ("special character", "Спецыяльныя сімвалы"), + ("length>=8", "Даўжыня >= 8 сімвалаў"), ("Weak", "Слабы"), ("Medium", "Сярэдні"), ("Strong", "Моцны"), ("Switch Sides", "Пераключыць бакі"), - ("Please confirm if you want to share your desktop?", "Вы сапраўды дазваляеце дэманстрацыю працоўнага стала?"), + ("Please confirm if you want to share your desktop?", "Пацвердзіце, калі хочаце дазволіць паказ вашага працоўнага стала?"), ("Display", "Адлюстраванне"), - ("Default View Style", "Стандартны стыль адлюстравання"), - ("Default Scroll Style", "Стандартны стыль прагортвання"), - ("Default Image Quality", "Стандартная якасць відарыса"), - ("Default Codec", "Стандартны кодэк"), + ("Default View Style", "Стыль адлюстравання па змаўчанні"), + ("Default Scroll Style", "Стыль пракруткі па змаўчанні"), + ("Default Image Quality", "Якасць выявы па змаўчанні"), + ("Default Codec", "Кодэк па змаўчанні"), ("Bitrate", "Бітрэйт"), ("FPS", "Колькасць кадраў у секунду"), ("Auto", "Аўта"), - ("Other Default Options", "Іншыя стандартныя параметры"), + ("Other Default Options", "Іншыя параметры па змаўчанні"), ("Voice call", "Галасавы выклік"), ("Text chat", "Тэкставы чат"), ("Stop voice call", "Спыніць галасавы выклік"), - ("relay_hint_tip", "Непасрэднае падключэнне можа быць немагчымым. У гэтым выпадку можна спрабаваць падключыцца праз рэтранслятар.\nАкрамя таго, калі вы хочаце адразу выкарыстоўваць рэтранслятар, можна дадаць да ідэнтыфікатара суфікс \"/r\" або ўключыць \"Заўсёды падключацца праз рэтранслятар\" у наладах абанента."), + ("relay_hint_tip", "Непасрэднае падключэнне можа быць немагчымым. У гэтым выпадку можна спрабаваць падключыцца праз рэлей.\nАкрамя таго, калі вы хочаце адразу выкарыстоўваць рэлей, можна дадаць да ідэнтыфікатара суфікс \"/r\" або ўключыць \"Заўсёды падключацца праз рэлей\" ў наладах аддаленага вузла."), ("Reconnect", "Перападключыць"), ("Codec", "Кодэк"), - ("Resolution", "Раздзяляльнасць"), + ("Resolution", "Разрознасць"), ("No transfers in progress", "Перадача не ажыццяўляецца"), ("Set one-time password length", "Усталяваць даўжыню аднаразовага пароля"), ("RDP Settings", "Налады RDP"), ("Sort by", "Сартаваць па"), - ("New Connection", "Новае падключэнне"), + ("New Connection", "Новае злучэнне"), ("Restore", "Аднавіць"), ("Minimize", "Згарнуць"), ("Maximize", "Разгарнуць"), ("Your Device", "Ваша прылада"), ("empty_recent_tip", "Няма апошніх сеансаў!\nЧас запланаваць новы."), - ("empty_favorite_tip", "Яшчэ няма абраных абанентаў?\nДавайце знойдзем, каго можна дадаць у абранае."), - ("empty_lan_tip", "Абанентаў не знойдзена."), - ("empty_address_book_tip", "У адраснай кнізе няма абанентаў."), + ("empty_favorite_tip", "Яшчэ няма выбраных аддаленых вузлоў?\nДавайце знойдзем, каго можна дадаць у выбранае."), + ("empty_lan_tip", "Не знойдзены аддаленыя вузлы."), + ("empty_address_book_tip", "У адраснай кнізе няма аддаленых вузлоў."), ("Empty Username", "Пустае імя карыстальніка"), ("Empty Password", "Пусты пароль"), ("Me", "Я"), - ("identical_file_tip", "Файл ідэнтычны файлу абанента"), + ("identical_file_tip", "Файл ідэнтычны файлу на аддаленым вузле"), ("show_monitors_tip", "Паказваць маніторы на панэлі інструментаў"), ("View Mode", "Рэжым прагляду"), - ("login_linux_tip", "Каб уключыць сеанс працоўнага стала X, трэба ўвайсці ў аддалены ўліковы запіс Linux."), + ("login_linux_tip", "Каб ўключыць сеанс працоўнага стала X, неабходна ўвайсці ў аддалены акаўнт Linux."), ("verify_rustdesk_password_tip", "Пацвердзіць пароль RustDesk"), - ("remember_account_tip", "Запомніць гэты ўліковы запіс"), - ("os_account_desk_tip", "Гэты ўліковы запіс выкарыстоўваецца для ўваходу ў аддаленую аперацыйную сістэму і ўключэння сеанса працоўнага стала ў рэжыме headless."), + ("remember_account_tip", "Запомніць гэты акаўнт"), + ("os_account_desk_tip", "Гэты акаўнт выкарыстоўваецца для ўваходу ў аддаленую аперацыйную сістэму і ўключэння сеансу працоўнага сталу ў рэжыме headless."), ("OS Account", "Акаўнт АС"), - ("another_user_login_title_tip", "Іншы карыстальнік ужо ўвайшоў у сістэму"), + ("another_user_login_title_tip", "Іншы карыстальнік ўжо ўвайшоў у сістэму"), ("another_user_login_text_tip", "Адключыць"), ("xorg_not_found_title_tip", "Xorg не знойдзены"), ("xorg_not_found_text_tip", "Усталюйце Xorg"), @@ -478,39 +477,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "Усталюйце GNOME Desktop"), ("No need to elevate", "Павышэнне правоў не патрабуецца"), ("System Sound", "Сістэмны гук"), - ("Default", "Стандартна"), + ("Default", "Па змаўчанні"), ("New RDP", "Новы RDP"), ("Fingerprint", "Адбітак"), - ("Copy Fingerprint", "Капіяваць адбітак"), + ("Copy Fingerprint", "Капіраваць адбітак"), ("no fingerprints", "адбіткі адсутнічаюць"), - ("Select a peer", "Выберыце абанента"), - ("Select peers", "Выберыце абанентаў"), - ("Plugins", "Убудовы"), + ("Select a peer", "Выберыце аддалены ўзел"), + ("Select peers", "Выберыце аддаленыя ўзлы"), + ("Plugins", "Плагіны"), ("Uninstall", "Выдаліць"), ("Update", "Абнавіць"), ("Enable", "Уключыць"), ("Disable", "Адключыць"), ("Options", "Параметры"), - ("resolution_original_tip", "Арыгінальная раздзяляльнасць"), - ("resolution_fit_local_tip", "Супадзенне з лакальнай раздзяляльнасцю"), - ("resolution_custom_tip", "Карыстацкая раздзяляльнасць"), + ("resolution_original_tip", "Арыгінальнае разознасць"), + ("resolution_fit_local_tip", "Супадзенне з лакальнай разрознасцю"), + ("resolution_custom_tip", "Карыстацкая разрознасць"), ("Collapse toolbar", "Згарнуць панэль інструментаў"), ("Accept and Elevate", "Прыняць і павысіць"), - ("accept_and_elevate_btn_tooltip", "Дазволіць падключэнне і павысіць правы UAC."), - ("clipboard_wait_response_timeout_tip", "Час чакання адказу капіявання буфера абмену скончыўся"), - ("Incoming connection", "Уваходнае падключэнне"), - ("Outgoing connection", "Выходнае падключэнне"), - ("Exit", "Выйсці"), + ("accept_and_elevate_btn_tooltip", "Дазволіць падлучэнне і павысіць правы UAC."), + ("clipboard_wait_response_timeout_tip", "Час чакання адказу капіравання буфера абмену скончыўся"), + ("Incoming connection", "Уваходнае падлучэнне"), + ("Outgoing connection", "Выходнае падлучэнне"), + ("Exit", "Выхад"), ("Open", "Адкрыць"), - ("logout_tip", "Вы сапраўды хочаце выйсці?"), + ("logout_tip", "Вы сапраўды жадаеце выйсці?"), ("Service", "Служба"), ("Start", "Запусціць"), ("Stop", "Спыніць"), - ("exceed_max_devices", "Дасягнута максімальная колькасць кантраляваных прылад."), + ("exceed_max_devices", "Дасягнута максімальная колькасць кіруемых прылад."), ("Sync with recent sessions", "Сінхранізацыя з апошнімі сеансамі"), - ("Sort tags", "Сартаваць цэтлікі"), - ("Open connection in new tab", "Адкрыць падключэнне ў новай укладцы"), - ("Move tab to new window", "Перамясціць укладку ў новае акно"), + ("Sort tags", "Сартаваць тэгі"), + ("Open connection in new tab", "Адкрыць падлучэнне ў новай ўкладцы"), + ("Move tab to new window", "Перамясціць ўкладку ў новае акно"), ("Can not be empty", "Ня можа быць пустым"), ("Already exists", "Ужо існуе"), ("Change Password", "Змяніць пароль"), @@ -519,231 +518,227 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Grid View", "Сетка"), ("List View", "Спіс"), ("Select", "Выбар"), - ("Toggle Tags", "Пераключыць цэтлікі"), + ("Toggle Tags", "Пераключыць тэгі"), ("pull_ab_failed_tip", "Немагчыма абнавіць адрасную кнігу"), ("push_ab_failed_tip", "Немагчыма сінхранізаваць адрасную кнігу з серверам"), ("synced_peer_readded_tip", "Прылады, якія былі на апошніх сеансах, будуць сінхранізаваны з адраснай кнігай."), ("Change Color", "Змяніць колер"), ("Primary Color", "Асноўны колер"), ("HSV Color", "Колер HSV"), - ("Installation Successful!", "Усталяванне выканана!"), - ("Installation failed!", "Усталяванне не ўдалося."), - ("Reverse mouse wheel", "Адваротнае прагортванне мышшу"), - ("{} sessions", "Колькасць сеансаў: {}"), - ("scam_title", "Вас могуць ПАДМАНУЦЬ!"), - ("scam_text1", "Калі вы размаўляеце па тэлефоне з кімсьці НЕЗНАЁМЫМ і каму вы НЕ ДАВЕРАЕЦЕ, і гэта асоба просіць вас выкарыстаць RustDesk і запусціць яго службу, не працягвайце і неадкладна скончыце размову."), - ("scam_text2", "Магчыма, гэта аферыст, які спрабуе скрасці вашы грошы або іншую асабістую інфармацыю."), + ("Installation Successful!", "Інсталяцыя прайшла паспяхова!"), + ("Installation failed!", "Інсталяцыя не ўдалася!"), + ("Reverse mouse wheel", "Рэверс кола мышы"), + ("{} sessions", "{} сеансаў"), + ("scam_title", "Вы можаце быць АБМАНУТЫ!"), + ("scam_text1", "Калі вы размаўляеце па тэлефоне з кімсці, каго вы НЕ ВЕДАЕЦЕ і каму НЕ ДАВЕРАЕЦЕ, і ён просіць вас выкарыстаць RustDesk і запусціць яго службу, не працягвайце і неадкладна адмяніце размову."), + ("scam_text2", "Магчыма, гэта аферыст, які паспрабуе ўкрасць вашыя грошы або іншую асабістую інфармацыю."), ("Don't show again", "Не паказваць больш"), - ("I Agree", "Згаджаюся"), + ("I Agree", "Я згодны"), ("Decline", "Адхіліць"), ("Timeout in minutes", "Час чакання (у хвілінах)"), - ("auto_disconnect_option_tip", "Аўтаматычна закрываць уваходныя сеансы пры неактыўнасці карыстальніка"), - ("Connection failed due to inactivity", "Збой падключэння з-за неактыўнасці"), + ("auto_disconnect_option_tip", "Аўтаматычна зачыняць уваходныя сеансы пры неактыўнасці карыстальніка"), + ("Connection failed due to inactivity", "Падлучэнне не ўдалося з-за неактыўнасці"), ("Check for software update on startup", "Праверка абнаўленняў праграмы пры запуску"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Абнавіце RustDesk Server Pro да версіі {} або навейшай!"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Абнавіце RustDesk Server Pro да версіі {} або новейшай!"), ("pull_group_failed_tip", "Немагчыма абнавіць групу"), ("Filter by intersection", "Фільтраваць па перасячэнні"), - ("Remove wallpaper during incoming sessions", "Схаваць шпалеры працоўнага стала ў часе ўваходнага сеанса"), + ("Remove wallpaper during incoming sessions", "Схаваць фон працоўнага стала падчас ўваходнага сеансу"), ("Test", "Тэст"), - ("display_is_plugged_out_msg", "Дысплэй адключаны, пераключыцеся на першы дысплэй."), - ("No displays", "Няма дысплэяў"), + ("display_is_plugged_out_msg", "Дысплей адключаны, пераключыцеся на першы дысплей."), + ("No displays", "Няма дысплеяў"), ("Open in new window", "Адкрыць у новым акне"), - ("Show displays as individual windows", "Паказваць дысплэі ў асобных вокнах"), - ("Use all my displays for the remote session", "Выкарыстоўваць усе мае дысплэі для аддаленага сеанса"), - ("selinux_tip", "На вашай прыладзе ўключаны SELinux, што можа ствараць перашкоды ў працы RustDesk на баку абанента."), - ("Change view", "Рэжым"), + ("Show displays as individual windows", "Паказваць дысплеі ў асобных акнах"), + ("Use all my displays for the remote session", "Выкарыстоўваць усе мае дысплеі для аддаленага сеансу"), + ("selinux_tip", "На вашай прыладзе ўключаны SELinux, што можа перашкаджаць правільнай працы RustDesk на кіруючым баку."), + ("Change view", "Змяніць выгляд"), ("Big tiles", "Вялікія пліткі"), ("Small tiles", "Маленькія пліткі"), ("List", "Спіс"), - ("Virtual display", "Віртуальны дысплэй"), + ("Virtual display", "Віртуальны дысплей"), ("Plug out all", "Адключыць усё"), ("True color (4:4:4)", "True color (4:4:4)"), - ("Enable blocking user input", "Дазволіць блакіраванне ўводу на прыладзе"), - ("id_input_tip", "Можна ўвесці ідэнтыфікатар, прамы IP-адрас або дамен з портам (<дамен>:<порт>).\nКаб атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ_значэнне>), напрыклад:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі трэба атрымаць доступ да прылады на агульнадаступным серверы, увядзіце \"@public\", ключ для публічнага сервера не патрабуецца."), + ("Enable blocking user input", "Дазволіць блакаванне ўводу карыстальніка на прыладзе"), + ("id_input_tip", "Можна ўвесці ідэнтыфікатар, просты IP-адрас або дамен з портам (<дамен>:<порт>).\nКаб атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ_значэнне>), напрыклад:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі неабходна атрымаць доступ да прылады на грамадскім серверы, увядзіце \"@public\", ключ для грамадскага сервера не патрабуецца."), ("privacy_mode_impl_mag_tip", "Рэжым 1"), ("privacy_mode_impl_virtual_display_tip", "Рэжым 2"), - ("Enter privacy mode", "Уключыць рэжым канфідэнцыйнасці"), - ("Exit privacy mode", "Адключыць рэжым канфідэнцыйнасці"), - ("idd_not_support_under_win10_2004_tip", "Драйвер непрамога адлюстравання не падтрымліваецца. Патрабуецца Windows 10 версіі 2004 або навейшая."), + ("Enter privacy mode", "Уключыць рэжым канфідэнцыяльнасці"), + ("Exit privacy mode", "Адключыць рэжым канфідэнцыяльнасці"), + ("idd_not_support_under_win10_2004_tip", "Драйвер непрамога адлюстравання не падтрымліваецца. Патрабуецца Windows 10 версіі 2004 ці навейшая."), ("input_source_1_tip", "Крыніца ўводу 1"), ("input_source_2_tip", "Крыніца ўводу 2"), ("Swap control-command key", "Памяняць месцамі значэнні кнопак Ctrl і Command"), ("swap-left-right-mouse", "Памяняць месцамі значэнні левай і правай кнопак мышы"), - ("2FA code", "Код двухфактарнай праверкі сапраўднасці"), + ("2FA code", "Код двухфактарнай аўтэнтыфікацыі"), ("More", "Яшчэ"), - ("enable-2fa-title", "Выкарыстоўваць двухфактарную праверку сапраўднасці"), - ("enable-2fa-desc", "Наладзьце праграму праверкі сапраўднасці. Выкарыстоўвайце, напрыклад, Authy, Microsoft або Google Authenticator на тэлефоне ці камп’ютары.\n\nАдскануйце QR-код з дапамогай праграмы праверкі сапраўднасці і ўвядзіце код, які пакажа гэта праграма, каб уключыць двухфактарную праверку сапраўднасці."), + ("enable-2fa-title", "Выкарыстоўваць двухфактарную аўтэнтыфікацыю"), + ("enable-2fa-desc", "Наладзьце праграму аўтэнтыфікацыі. Выкарыстоўвайце, напрыклад, Authy, Microsoft або Google Authenticator на тэлефоне ці кампутары.\n\nСкануйце QR-код з дапамогай праграмы аўтэнтыфікацыі і ўвядзіце код, які пакажа гэта праграма, каб уключыць двухфактарную аўтэнтыфікацыю."), ("wrong-2fa-code", "Немагчыма пацвердзіць код. Праверце код і налады мясцовага часу."), - ("enter-2fa-title", "Двухфактарная праверка сапраўднасці"), - ("Email verification code must be 6 characters.", "Код пацвярджэння па электроннай пошце павінен складацца з 6 сімвалаў."), - ("2FA code must be 6 digits.", "Код двухфактарнай праверкі сапраўднасці павінен складацца з 6 лічбаў."), + ("enter-2fa-title", "Двухфактарная аутэнтыфікацыя"), + ("Email verification code must be 6 characters.", "Код верыфікацыі па электроннай пошце павінен складацца з 6 сімвалаў."), + ("2FA code must be 6 digits.", "Код двухфактарнай аутэнтыфікацыі павінен складацца з 6 лічбаў."), ("Multiple Windows sessions found", "Знойдзена некалькі сеансаў Windows"), - ("Please select the session you want to connect to", "Выберыце сеанс, да якога вы хочаце падключыцца"), - ("powered_by_me", "Заснавана на RustDesk"), + ("Please select the session you want to connect to", "Выберыце сеанс, да якога вы жадаеце падключыцца"), + ("powered_by_me", "На аснове RustDesk"), ("outgoing_only_desk_tip", "Гэта спецыялізаваная версія.\nВы можаце падключацца да іншых прылад, але іншыя прылады не могуць падключацца да вашай."), - ("preset_password_warning", "Гэта спецыялізаваная версія з прадвызначаным паролем. Любы, хто ведае гэты пароль, можа атрымаць поўны кантроль над вашай прыладай. Калі гэта для вас нечакана, адразу выдаліце гэта праграмнае забеспячэнне."), + ("preset_password_warning", "Гэта спецыялізаваная версія з устаноўленым загадзя паролем. Любы, хто ведае гэты пароль, можа атрымаць поўны кантроль над вашай прыладай. Калі гэта для вас нечакана, адразу выдаліце гэта праграмнае забеспячэнне."), ("Security Alert", "Папярэджанне аб бяспецы"), ("My address book", "Мая адрасная кніга"), - ("Personal", "Асабістая"), + ("Personal", "Асабісты"), ("Owner", "Уладальнік"), - ("Set shared password", "Задаць агульны пароль"), + ("Set shared password", "Устанавіць агульны пароль"), ("Exist in", "Існуе ў"), ("Read-only", "Толькі для чытання"), ("Read/Write", "Чытанне і запіс"), - ("Full Control", "Поўны доступ"), + ("Full Control", "Поўны кантроль"), ("share_warning_tip", "Палі вышэй з'яўляюцца агульнымі і бачнымі іншым."), ("Everyone", "Усе"), ("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", "Прытрымлівацца фокуса аддаленага акна"), - ("default_proxy_tip", "Стандартныя пратакол і порт: Socks5 і 1080"), + ("allow-only-conn-window-open-tip", "Дазволіць толькі падключэнне пры адкрытым акне RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Фізічныя дысплеі адсутнічаюць, няма патрэбы выкарыстоўваць рэжым канфідэнцыяльнасці."), + ("Follow remote cursor", "Сачыць за аддаленага курсарам"), + ("Follow remote window focus", "Сачыць за фокусам аддаленага акна"), + ("default_proxy_tip", "Пратакол і порт па змаўчанні: Socks5 і 1080"), ("no_audio_input_device_tip", "Прылада ўваходнага аудыё не знойдзена."), ("Incoming", "Уваходныя"), ("Outgoing", "Выходныя"), - ("Clear Wayland screen selection", "Скасаваць выбар экрана Wayland"), - ("clear_Wayland_screen_selection_tip", "Пасля скасавання можна зноў выбраць экран для дэманстрацыі."), - ("confirm_clear_Wayland_screen_selection_tip", "Скасаваць выбар экрана Wayland?"), - ("android_new_voice_call_tip", "Прыйшоў новы запыт на галасавы выклік. Калі вы прымеце яго, гук пераключыцца на галасавае падключэнне."), - ("texture_render_tip", "Выкарыстоўваць візуалізацыю тэкстур, каб зрабіць відарысы больш плаўнымі."), - ("Use texture rendering", "Візуалізацыя тэкстур"), - ("Floating window", "Нефіксаванае акно"), + ("Clear Wayland screen selection", "Адмяніць выбар экрана Wayland"), + ("clear_Wayland_screen_selection_tip", "Пасля адмены можна зноў выбраць экран для дэманстрацыі."), + ("confirm_clear_Wayland_screen_selection_tip", "Адмяніць выбар экрана Wayland?"), + ("android_new_voice_call_tip", "Атрыман новы запыт на галасавы выклік. Калі вы прымеце яго, гук пераключыцца на галасавае злучэнне."), + ("texture_render_tip", "Выкарыстоўваць візуалізацыю тэкстураў для павышэння каб плаўнасці выявы."), + ("Use texture rendering", "Візуалізацыя тэкстураў"), + ("Floating window", "Плавучае акно"), ("floating_window_tip", "Дапамагае падтрымліваць фонавую службу RustDesk"), ("Keep screen on", "Трымаць экран уключаным"), ("Never", "Ніколі"), ("During controlled", "Пры кіраванні"), ("During service is on", "Пры запушчанай службе"), ("Capture screen using DirectX", "Захоп экрана з выкарыстаннем DirectX"), - ("Back", "Назад"), - ("Apps", "Праграмы"), - ("Volume up", "Гучнасць+"), - ("Volume down", "Гучнасць-"), - ("Power", "Сілкаванне"), - ("Telegram bot", "Telegram-бот"), - ("enable-bot-tip", "Калі ўключана, можна атрымліваць код двухфактарнай праверкі сапраўднасці ад бота. Таксама ён можа выконваць функцыю апавяшчэння пра падключэнне."), - ("enable-bot-desc", "1) Адкрыйце чат з @BotFather.\n2) Адпраўце каманду \"/newbot\". Пасля выканання гэтага кроку вы атрымаеце токен.\n3) Пачніце чат з вашым толькі што створаным ботам. Адпраўце паведамленне, якое пачынаецца з касой рысы (\"/\"), напрыклад, \"/hello\", каб яго актываваць.\n"), - ("cancel-2fa-confirm-tip", "Адключыць двухфактарную праверку сапраўднасці?"), - ("cancel-bot-confirm-tip", "Адключыць Telegram-бота"), - ("About RustDesk", "Пра RustDesk"), - ("Send clipboard keystrokes", "Адпраўляць націсканні клавіш у буфер абмену"), - ("network_error_tip", "Праверце падключэнне да сеткі, пасля чаго націсніце \"Паўтарыць спробу\"."), - ("Unlock with PIN", "Разблакіраваць PIN-кодам"), - ("Requires at least {} characters", "Патрабуецца больш сімвалаў (ад {})"), - ("Wrong PIN", "Памылковы PIN-код"), - ("Set PIN", "Задаць PIN-код"), - ("Enable trusted devices", "Уключэнне давераных прылад"), - ("Manage trusted devices", "Кіраванне даверанымі прыладамі"), - ("Platform", "Платформа"), - ("Days remaining", "Засталося дзён"), - ("enable-trusted-devices-tip", "Дазволіць давераным прыладам прапускаць праверку сапраўднасці 2FA"), - ("Parent directory", "Бацькоўскі каталог"), - ("Resume", "Працягнуць"), - ("Invalid file name", "Памылковая назва файла"), - ("one-way-file-transfer-tip", "На баку абанента ўключана аднабаковая перадача файлаў."), - ("Authentication Required", "Патрабуецца праверка сапраўднасці"), - ("Authenticate", "Прайсці праверку"), - ("web_id_input_tip", "Можна ўвесці ID на тым самым серверы, прамы доступ па IP у вэб-кліенце не падтрымліваецца.\nКалі вы хочаце атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ>), напрыклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі вы хочаце атрымаць доступ да прылады на публічным серверы, увядзіце \"@public\", для публічнага сервера ключ не патрэбны."), - ("Download", "Спампаваць"), - ("Upload folder", "Запампаваць папку"), - ("Upload files", "Запампаваць файлы"), - ("Clipboard is synchronized", "Буфер абмену сінхранізаваны"), - ("Update client clipboard", "Абнавіць буфер абмену кліента"), - ("Untagged", "Без цэтліка"), - ("new-version-of-{}-tip", "Даступна новая версія {}"), - ("Accessible devices", "Даступныя прылады"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "Абнавіце кліент RustDesk да версіі {} або навейшай на баку абанента!"), - ("d3d_render_tip", "Пры ўключэнні візуалізацыі D3D на некаторых прыладах аддалены экран можа быць чорным."), - ("Use D3D rendering", "Выкарыстоўваць візуалізацыю D3D"), - ("Printer", "Прынтар"), - ("printer-os-requirement-tip", "Для работы функцыі выходнай сувязі з прынтарам патрабуецца Windows 10 або навейшай версіі."), - ("printer-requires-installed-{}-client-tip", "Каб выкарыстоўваць аддалены друк, {} павінен быць усталяваны на гэтай прыладзе."), - ("printer-{}-not-installed-tip", "Прынтар {} не ўсталяваны."), - ("printer-{}-ready-tip", "Прынтар {} усталяваны і гатовы да выкарыстання."), - ("Install {} Printer", "Усталюйце прынтар {}"), - ("Outgoing Print Jobs", "Выходныя заданні друку"), - ("Incoming Print Jobs", "Уваходныя заданні друку"), - ("Incoming Print Job", "Уваходнае заданне друку"), - ("use-the-default-printer-tip", "Выкарыстоўваць прынтар стандартна"), - ("use-the-selected-printer-tip", "Выкарыстоўваць выбраны прынтар"), - ("auto-print-tip", "Аўтаматычна выконваць друк на выбраным прынтары"), - ("print-incoming-job-confirm-tip", "З аддаленай прылады атрымана заданне на друк. Выканаць яго лакальна?"), - ("remote-printing-disallowed-tile-tip", "Аддалены друк забаронены"), - ("remote-printing-disallowed-text-tip", "Налады дазволаў на баку абанента забараняюць аддалены друк."), - ("save-settings-tip", "Захаваць налады"), - ("dont-show-again-tip", "Больш не паказваць"), - ("Take screenshot", "Зрабіць здымак экрана"), - ("Taking screenshot", "Робіцца здымак экрана"), - ("screenshot-merged-screen-not-supported-tip", "Аб’яднанне здымкаў экранаў з некалькіх дысплэяў у дадзены момант не падтрымліваецца. Пераключыцеся на адзін з дысплэяў і паўтарыце дзеянне."), - ("screenshot-action-tip", "Выберыце, што рабіць з атрыманым здымкам экрана."), - ("Save as", "Захаваць у файл"), - ("Copy to clipboard", "Скапіяваць у буфер абмену"), - ("Enable remote printer", "Выкарыстоўваць аддалены прынтар"), - ("Downloading {}", "Ідзе спампоўванне {}"), - ("{} Update", "Абнавіць {}"), - ("{}-to-update-tip", "{} закрыецца і ўсталюе новую версію."), - ("download-new-version-failed-tip", "Памылка спампоўвання. Можна паўтарыць спробу або націснуць кнопку \"Спампаваць\", каб спампаваць праграму з афіцыйнага сайта і абнавіць уручную."), - ("Auto update", "Аўтаматычнае абнаўленне"), - ("update-failed-check-msi-tip", "Немагчыма вызначыць метад усталявання. Націсніце кнопку \"Спампаваць\", каб спампаваць праграму з афіцыйнага сайта і абнавіце яго ўручную."), - ("websocket_tip", "WebSocket падтрымлівае толькі падключэнні да рэтранслятара."), - ("Use WebSocket", "Выкарыстоўваць WebSocket"), - ("Trackpad speed", "Хуткасць трэкпада"), - ("Default trackpad speed", "Стандартная хуткасць трэкпада"), - ("Numeric one-time password", "Лічбавы аднаразовы пароль"), - ("Enable IPv6 P2P connection", "Выкарыстоўваць падключэнне IPv6 P2P"), - ("Enable UDP hole punching", "Выкарыстоўваць UDP hole punching"), - ("View camera", "Рэжым камеры"), - ("Enable camera", "Уключыць камеру"), - ("No cameras", "Камера адсутнічае"), - ("view_camera_unsupported_tip", "Аддаленая прылада не падтрымлівае рэжыму камеры."), - ("Terminal", "Тэрмінал"), - ("Enable terminal", "Уключыць тэрмінал"), - ("New tab", "Новая ўкладка"), - ("Keep terminal sessions on disconnect", "Захоўваць сеансы тэрмінала пры адключэнні"), - ("Terminal (Run as administrator)", "Тэрмінал (адміністратар)"), - ("terminal-admin-login-tip", "Увядзіце імя карыстальніка і пароль адміністратара абанента."), - ("Failed to get user token.", "Не ўдалося атрымаць токен карыстальніка."), - ("Incorrect username or password.", "Памылковае імя карыстальніка або пароль."), - ("The user is not an administrator.", "Карыстальнік не з’яўляецца адміністратарам."), - ("Failed to check if the user is an administrator.", "Немагчыма праверыць, ці з’яўляецца карыстальнік адміністратарам."), - ("Supported only in the installed version.", "Падтрымліваецца толькі ва ўсталёвачнай версіі."), - ("elevation_username_tip", "Увядзіце карыстальніка або дамен\\карыстальніка"), - ("Preparing for installation ...", "Ідзе падрыхтоўка да ўсталявання..."), - ("Show my cursor", "Паказваць мой курсор"), - ("Scale custom", "Карыстальніцкае маштабаванне"), - ("Custom scale slider", "Карыстальніцкі паўзунок маштабавання"), - ("Decrease", "Паменшыць"), - ("Increase", "Павялічыць"), - ("Show virtual mouse", "Паказаць віртуальную мыш"), - ("Virtual mouse size", "Памер віртуальнай мышы"), - ("Small", "Маленькі"), - ("Large", "Вялікі"), - ("Show virtual joystick", "Паказваць віртуальны джойстык"), - ("Edit note", "Змяніць нататку"), - ("Alias", "Псеўданім"), - ("ScrollEdge", "Прагортваць з краю"), - ("Allow insecure TLS fallback", "Дазволіць небяспечныя TLS"), - ("allow-insecure-tls-fallback-tip", "Стандартна RustDesk правярае сертыфікат сервера на наяўнасць пратаколаў, якія выкарыстоўваюць TLS.\nКалі гэта функцыя ўключана, RustDesk прапусціць дадзены этап і працягне працу ў выпадку няўдалай праверкі."), - ("Disable UDP", "Выключыць UDP"), - ("disable-udp-tip", "Вызначае, ці варта выкарыстоўваць толькі TCP.\nКалі ўключана, RustDesk не будзе выкарыстоўваць UDP 21116, замест чаго будзе выкарыстоўвацца TCP 21116."), - ("server-oss-not-support-tip", "ЗАЎВАГА! у OSS-серверы RustDesk гэта функцыя адсутнічае."), - ("input note here", "увядзіце нататку"), - ("note-at-conn-end-tip", "Запытваць нататку ў канцы сеанса"), - ("Show terminal extra keys", "Паказваць дадатковыя кнопкі тэрмінала"), - ("Relative mouse mode", "Рэжым адноснага перамяшчэння мышы"), - ("rel-mouse-not-supported-peer-tip", "Рэжым адноснага перамяшчэння мышы не падтрымліваецца падключаным абанентам."), - ("rel-mouse-not-ready-tip", "Рэжым адноснага перамяшчэння мышы яшчэ не гатовы. Паспрабуйце зноў."), - ("rel-mouse-lock-failed-tip", "Немагчыма заблакіраваць курсор. Рэжым адноснага перамяшчэння мышы адключаны."), - ("rel-mouse-exit-{}-tip", "Націсніце {}, каб выйсці."), - ("rel-mouse-permission-lost-tip", "Дазвол на выкарыстанне клавіятуры скасаваны. Рэжым адноснага перамяшчэння мышы адключаны."), - ("Changelog", "Журнал змяненняў"), - ("keep-awake-during-outgoing-sessions-label", "Не адключаць экрана ў часе выходных сеансаў"), - ("keep-awake-during-incoming-sessions-label", "Не адключаць экрана ў часе ўваходных сеансаў"), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Прагляд камеры"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Працягнуць з {}"), - ("Display Name", "Імя для адлюстравання"), - ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), - ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 0aa61b1eb..3036e31b2 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Настройки на клавиатурата"), ("Full Access", "Пълен достъп"), ("Screen Share", "Споделяне на екрана"), - ("ubuntu-21-04-required", "Wayland изисква Ubuntu 21.04 или по-нов"), - ("wayland-requires-higher-linux-version", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), ("JumpLink", "Препратка"), ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."), ("Show RustDesk", "Покажи RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продължи с {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 2f706cc89..05a7e7899 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Configuració del teclat"), ("Full Access", "Accés complet"), ("Screen Share", "Compartició de pantalla"), - ("ubuntu-21-04-required", "Wayland requereix Ubuntu 21.04 o superior"), - ("wayland-requires-higher-linux-version", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), ("JumpLink", "Marcador"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"), ("Show RustDesk", "Mostra el RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continua amb {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a90e5e194..0cc6aacd1 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "键盘设置"), ("Full Access", "完全访问"), ("Screen Share", "仅共享屏幕"), - ("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更高版本。"), - ("wayland-requires-higher-linux-version", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), ("Show RustDesk", "显示 RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"), ("Continue with {}", "使用 {} 登录"), ("Display Name", "显示名称"), - ("password-hidden-tip", "永久密码已设置(已隐藏)"), - ("preset-password-in-use-tip", "当前使用预设密码"), - ("Enable privacy mode", "允许隐私模式"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7f50d826f..944ee4b95 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Nastavení klávesnice"), ("Full Access", "Úplný přístup"), ("Screen Share", "Sdílení obrazovky"), - ("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."), - ("wayland-requires-higher-linux-version", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."), ("Show RustDesk", "Zobrazit RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovat s {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c9d3b4eb0..8140fcaec 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastaturindstillinger"), ("Full Access", "Fuld adgang"), ("Screen Share", "Skærmdeling"), - ("ubuntu-21-04-required", "Wayland kræver Ubuntu version 21.04 eller nyere."), - ("wayland-requires-higher-linux-version", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu version 21.04 eller nyere."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."), ("Show RustDesk", "Vis RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsæt med {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e6233e91e..03e501848 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -377,10 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastatureinstellungen"), ("Full Access", "Vollzugriff"), ("Screen Share", "Bildschirmfreigabe"), - ("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), - ("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), - ("xdp-portal-unavailable", "Die Bildschirmaufnahme mit Wayland ist fehlgeschlagen. Das XDG-Desktop-Portal ist möglicherweise abgestürzt oder nicht verfügbar. Versuchen Sie, es mit `systemctl --user restart xdg-desktop-portal` neu zu starten."), - ("JumpLink", "Anzeigen"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), + ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ("Continue with {}", "Fortfahren mit {}"), ("Display Name", "Anzeigename"), - ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), - ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), - ("Enable privacy mode", "Datenschutzmodus aktivieren"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index d03bb069c..8812f7d04 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), ("Full Access", "Πλήρης πρόσβαση"), ("Screen Share", "Κοινή χρήση οθόνης"), - ("ubuntu-21-04-required", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), - ("wayland-requires-higher-linux-version", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), ("JumpLink", "Σύνδεσμος μετάβασης"), ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), ("Show RustDesk", "Εμφάνιση του RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"), ("Continue with {}", "Συνέχεια με {}"), ("Display Name", "Εμφανιζόμενο όνομα"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 595169b8a..511ddff4a 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -120,9 +120,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Keyboard settings"), ("Full Access", "Full access"), ("Screen Share", "Screen share"), - ("ubuntu-21-04-required", "Wayland requires Ubuntu 21.04 or higher version."), - ("wayland-requires-higher-linux-version", "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."), - ("xdp-portal-unavailable", "Wayland screen capture failed. The XDG Desktop Portal may have crashed or is unavailable. Try restarting it with `systemctl --user restart xdg-desktop-portal`."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Please select the screen to be shared(Operate on the peer side)."), ("One-time Password", "One-time password"), @@ -272,8 +269,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."), ("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"), ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), - ("password-hidden-tip", "Permanent password is set (hidden)."), - ("preset-password-in-use-tip", "Preset password is currently in use."), - ("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 131a85fbf..3d6b6924f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", ""), ("Full Access", ""), ("Screen Share", ""), - ("ubuntu-21-04-required", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), - ("wayland-requires-higher-linux-version", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), ("Show RustDesk", ""), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5e73b58a8..8ad0c4cab 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Connect via relay", "Conectar a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), @@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Olvidó su nombre de usuario"), ("Password missed", "Olvidó su contraseña"), ("Wrong credentials", "Credenciales incorrectas"), - ("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"), + ("The verification code is incorrect or has expired", ""), ("Edit Tag", "Editar tag"), ("Forget Password", "Olvidar contraseña"), ("Favorites", "Favoritos"), @@ -302,8 +302,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), - ("Start on boot", "Iniciar al arrancar"), - ("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -326,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Relación"), ("Image Quality", "Calidad de imagen"), ("Scroll Style", "Estilo de desplazamiento"), - ("Show Toolbar", "Mostrar herramientas"), - ("Hide Toolbar", "Ocultar herramientas"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), ("Direct Connection", "Conexión directa"), ("Relay Connection", "Conexión Relay"), ("Secure Connection", "Conexión segura"), @@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Seguridad"), ("Theme", "Tema"), ("Dark Theme", "Tema Oscuro"), - ("Light Theme", "Tema claro"), + ("Light Theme", ""), ("Dark", "Oscuro"), ("Light", "Claro"), ("Follow System", "Tema del sistema"), @@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Dispositivo de entrada de audio"), ("Use IP Whitelisting", "Usar lista de IPs admitidas"), ("Network", "Red"), - ("Pin Toolbar", "Anclar herramientas"), - ("Unpin Toolbar", "Desanclar herramientas"), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), ("Recording", "Grabando"), ("Directory", "Directorio"), ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), - ("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"), + ("Automatically record outgoing sessions", ""), ("Change", "Cambiar"), ("Start session recording", "Comenzar grabación de sesión"), ("Stop session recording", "Detener grabación de sesión"), @@ -368,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN discovery", "Habilitar descubrimiento de LAN"), ("Deny LAN discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), - ("Prompt", "Solicitud"), + ("Prompt", ""), ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), ("Disconnected", "Desconectado"), @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Ajustes de teclado"), ("Full Access", "Acceso completo"), ("Screen Share", "Compartir pantalla"), - ("ubuntu-21-04-required", "Wayland requiere Ubuntu 21.04 o una versión superior."), - ("wayland-requires-higher-linux-version", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."), ("JumpLink", "Ver"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), ("Show RustDesk", "Mostrar RustDesk"), @@ -616,9 +615,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During service is on", "Mientras el servicio está activo"), ("Capture screen using DirectX", "Capturar pantalla con DirectX"), ("Back", "Atrás"), - ("Apps", "Aplicaciones"), - ("Volume up", "Subir volumen"), - ("Volume down", "Bajar volumen"), + ("Apps", ""), + ("Volume up", "Bajar volumen"), + ("Volume down", "Subir volumen"), ("Power", "Encendido"), ("Telegram bot", "Bot de Telegram"), ("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."), @@ -651,7 +650,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Actualizar portapapeles del cliente"), ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), - ("Accessible devices", "Dispositivos accesibles"), + ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), ("Use D3D rendering", "Usar renderizado D3D"), @@ -689,9 +688,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use WebSocket", "Usar WebSocket"), ("Trackpad speed", "Velocidad de trackpad"), ("Default trackpad speed", "Velocidad predeterminada de trackpad"), - ("Numeric one-time password", "Contraseña numérica de un solo uso"), - ("Enable IPv6 P2P connection", "Habilitar conexión IPv6 P2P"), - ("Enable UDP hole punching", "Habilitar perforación de agujero UDP"), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), ("View camera", "Ver cámara"), ("Enable camera", "Habilitar cámara"), ("No cameras", "No hay cámaras"), @@ -708,8 +707,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), ("Supported only in the installed version.", "Soportado solo en la versión instalada."), ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), - ("Preparing for installation ...", "Preparando instlación..."), - ("Show my cursor", "Mostrar mi cursor"), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), ("Scale custom", "Escala personalizada"), ("Custom scale slider", "Control deslizante de escala personalizada"), ("Decrease", "Disminuir"), @@ -721,29 +720,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Mostrar joystick virtual"), ("Edit note", "Editar nota"), ("Alias", ""), - ("ScrollEdge", "Desplazamiento de pantalla"), - ("Allow insecure TLS fallback", "Permitir conexión TLS insegura de respaldo"), - ("allow-insecure-tls-fallback-tip", "De forma predeterminada, RustDesk verifica el certificado de servidor para protocolos que usen TLS.\nCon esta opción habilitada, Rustdesk volverá al paso de omisión de verificación y procederá en caso de fallo de verificación."), - ("Disable UDP", "Inhabilitar UDP"), - ("disable-udp-tip", "Controla si se usa TCP solamente.\nCuando esta opción está activa, RustDesk no usará más el puerto UDP 21116, en su lugar se usará el TCP 21116."), - ("server-oss-not-support-tip", "NOTA: El servidor RustDesk OSS no incluye esta característica."), - ("input note here", "Introducir nota aquí"), - ("note-at-conn-end-tip", "Pedir nota al finalizar la conexión"), - ("Show terminal extra keys", "Mostrar teclas extra del terminal"), - ("Relative mouse mode", "Modo de ratón relativo"), - ("rel-mouse-not-supported-peer-tip", "El modo relativo de ratón no está soportado por el par."), - ("rel-mouse-not-ready-tip", "El modo relativo de ratón aún no está preparado. Por favor, inténtalo de nuevo."), - ("rel-mouse-lock-failed-tip", "Ha fallado el bloqueo del cursor. El modo relativo del ratón ha sido inhabilitado."), - ("rel-mouse-exit-{}-tip", "Pulsa {} para salir."), - ("rel-mouse-permission-lost-tip", "Permiso de teclado revocado. El modo relativo del ratón ha sido inhabilitado."), - ("Changelog", "Registro de cambios"), - ("keep-awake-during-outgoing-sessions-label", "Mantener la pantalla activa durante sesiones salientes"), - ("keep-awake-during-incoming-sessions-label", "Mantener la pantalla activa durante sesiones entrantes"), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuar con {}"), - ("Display Name", "Nombre de pantalla"), - ("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."), - ("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."), - ("Enable privacy mode", "Habilitar modo privado"), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 76abc8563..def665ec5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Klaviatuurisätted"), ("Full Access", "Täielik ligipääs"), ("Screen Share", "Ekraanijagamine"), - ("ubuntu-21-04-required", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), - ("wayland-requires-higher-linux-version", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."), ("Show RustDesk", "Kuva RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jätka koos {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 9e19d1fea..2454dcb8a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Teklatuaren ezarpenak"), ("Full Access", "Sarbide osoa"), ("Screen Share", "Pantailaren partekatzea"), - ("ubuntu-21-04-required", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."), - ("wayland-requires-higher-linux-version", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."), ("JumpLink", "Ikusi"), ("Please Select the screen to be shared(Operate on the peer side).", "Mesedez, hautatu partekatuko den pantaila (Kudeatu parekidearen aldean)"), ("Show RustDesk", "Erakutsi RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} honekin jarraitu"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9e01b7eb0..52be56c81 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "تنظیمات صفحه کلید"), ("Full Access", "دسترسی کامل"), ("Screen Share", "اشتراک گذاری صفحه"), - ("ubuntu-21-04-required", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), - ("wayland-requires-higher-linux-version", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), ("JumpLink", "چشم انداز"), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), ("Show RustDesk", "RustDesk نمایش"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ادامه با {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index f8283685b..0d9b42ddd 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Näppäimistöasetukset"), ("Full Access", "Täysi käyttöoikeus"), ("Screen Share", "Näytönjako"), - ("ubuntu-21-04-required", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."), - ("wayland-requires-higher-linux-version", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."), ("JumpLink", "Pikalinkki"), ("Please Select the screen to be shared(Operate on the peer side).", "Valitse jaettava näyttö (toiminto etäpäässä)."), ("Show RustDesk", "Näytä RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jatka käyttäen {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f21d9b0df..fed35727e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Paramètres du clavier"), ("Full Access", "Accès total"), ("Screen Share", "Partage d’écran"), - ("ubuntu-21-04-required", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), - ("wayland-requires-higher-linux-version", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), - ("xdp-portal-unavailable", "Échec de la capture de l’écran Wayland. Le portail de bureau XDG a peut-être planté ou n’est pas disponible. Essayez de le redémarrer avec la commande `systemctl --user restart xdg-desktop-portal`."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), ("JumpLink", "Afficher"), ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."), ("Show RustDesk", "Afficher RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ("Continue with {}", "Continuer avec {}"), ("Display Name", "Nom d’affichage"), - ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), - ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), - ("Enable privacy mode", "Activer le mode de confidentialité"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 2fc8f282d..10b5e7f27 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "კლავიატურის პარამეტრები"), ("Full Access", "სრული წვდომა"), ("Screen Share", "ეკრანის გაზიარება"), - ("ubuntu-21-04-required", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."), - ("wayland-requires-higher-linux-version", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."), ("JumpLink", "ნახვა"), ("Please Select the screen to be shared(Operate on the peer side).", "აირჩიეთ ეკრანი გასაზიარებლად (იმუშავეთ პარტნიორის მხარეს)."), ("Show RustDesk", "RustDesk-ის ჩვენება"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{}-ით გაგრძელება"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs deleted file mode 100644 index ac0a588a8..000000000 --- a/src/lang/gu.rs +++ /dev/null @@ -1,749 +0,0 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "સ્થિતિ"), - ("Your Desktop", "તમારું ડેસ્કટોપ"), - ("desk_tip", "તમારું ડેસ્કટોપ આ ID અને પાસવર્ડ દ્વારા એક્સેસ કરી શકાય છે."), - ("Password", "પાસવર્ડ"), - ("Ready", "તૈયાર"), - ("Established", "સ્થાપિત"), - ("connecting_status", "નેટવર્ક સાથે જોડાઈ રહ્યું છે..."), - ("Enable service", "સેવા સક્ષમ કરો"), - ("Start service", "સેવા શરૂ કરો"), - ("Service is running", "સેવા કાર્યરત છે"), - ("Service is not running", "સેવા કાર્યરત નથી"), - ("not_ready_status", "તૈયાર નથી. કૃપા કરીને તમારું કનેક્શન તપાસો"), - ("Control Remote Desktop", "રિમોટ ડેસ્કટોપ નિયંત્રિત કરો"), - ("Transfer file", "ફાઇલ ટ્રાન્સફર"), - ("Connect", "કનેક્ટ કરો"), - ("Recent sessions", "તાજેતરના સત્રો"), - ("Address book", "એડ્રેસ બુક"), - ("Confirmation", "પુષ્ટિકરણ"), - ("TCP tunneling", "TCP ટનલિંગ"), - ("Remove", "દૂર કરો"), - ("Refresh random password", "રેન્ડમ પાસવર્ડ બદલો"), - ("Set your own password", "તમારો પોતાનો પાસવર્ડ સેટ કરો"), - ("Enable keyboard/mouse", "કીબોર્ડ/માઉસ સક્ષમ કરો"), - ("Enable clipboard", "ક્લિપબોર્ડ સક્ષમ કરો"), - ("Enable file transfer", "ફાઇલ ટ્રાન્સફર સક્ષમ કરો"), - ("Enable TCP tunneling", "TCP ટનલિંગ સક્ષમ કરો"), - ("IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગ"), - ("ID/Relay Server", "ID/રિલે સર્વર"), - ("Import server config", "સર્વર કોન્ફિગ ઈમ્પોર્ટ કરો"), - ("Export Server Config", "સર્વર કોન્ફિગ એક્સપોર્ટ કરો"), - ("Import server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક ઈમ્પોર્ટ થયું"), - ("Export server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક એક્સપોર્ટ થયું"), - ("Invalid server configuration", "અમાન્ય સર્વર કોન્ફિગરેશન"), - ("Clipboard is empty", "ક્લિપબોર્ડ ખાલી છે"), - ("Stop service", "સેવા બંધ કરો"), - ("Change ID", "ID બદલો"), - ("Your new ID", "તમારું નવું ID"), - ("length %min% to %max%", "લંબાઈ %min% થી %max% સુધી"), - ("starts with a letter", "અક્ષરથી શરૂ થાય છે"), - ("allowed characters", "માન્ય અક્ષરો"), - ("id_change_tip", "ID બદલ્યા પછી વર્તમાન કનેક્શન તૂટી જશે."), - ("Website", "વેબસાઇટ"), - ("About", "વિશે"), - ("Slogan_tip", "વધુ સારા અનુભવ માટે બનાવેલ રિમોટ ડેસ્કટોપ સોફ્ટવેર"), - ("Privacy Statement", "ગોપનીયતા નિવેદન"), - ("Mute", "મ્યૂટ કરો"), - ("Build Date", "બિલ્ડ તારીખ"), - ("Version", "સંસ્કરણ (Version)"), - ("Home", "હોમ"), - ("Audio Input", "ઓડિયો ઇનપુટ"), - ("Enhancements", "વધારાની સુવિધાઓ"), - ("Hardware Codec", "હાર્ડવેર કોડેક"), - ("Adaptive bitrate", "એડેપ્ટિવ બિટરેટ"), - ("ID Server", "ID સર્વર"), - ("Relay Server", "રિલે સર્વર"), - ("API Server", "API સર્વર"), - ("invalid_http", "અમાન્ય HTTP લિંક"), - ("Invalid IP", "અમાન્ય IP"), - ("Invalid format", "અમાન્ય ફોર્મેટ"), - ("server_not_support", "સર્વર દ્વારા સમર્થિત નથી"), - ("Not available", "ઉપલબ્ધ નથી"), - ("Too frequent", "ખૂબ વારંવાર"), - ("Cancel", "રદ કરો"), - ("Skip", "રહેવા દો (Skip)"), - ("Close", "બંધ કરો"), - ("Retry", "ફરી પ્રયાસ કરો"), - ("OK", "બરાબર"), - ("Password Required", "પાસવર્ડ જરૂરી છે"), - ("Please enter your password", "કૃપા કરીને તમારો પાસવર્ડ દાખલ કરો"), - ("Remember password", "પાસવર્ડ યાદ રાખો"), - ("Wrong Password", "ખોટો પાસવર્ડ"), - ("Do you want to enter again?", "શું તમે ફરીથી દાખલ કરવા માંગો છો?"), - ("Connection Error", "કનેક્શન ભૂલ"), - ("Error", "ભૂલ"), - ("Reset by the peer", "સામેના છેડેથી રિસેટ કરવામાં આવ્યું"), - ("Connecting...", "જોડાઈ રહ્યું છે..."), - ("Connection in progress. Please wait.", "કનેક્શન ચાલુ છે. કૃપા કરીને રાહ જુઓ."), - ("Please try 1 minute later", "કૃપા કરીને 1 મિનિટ પછી ફરી પ્રયાસ કરો"), - ("Login Error", "લોગિન ભૂલ"), - ("Successful", "સફળ"), - ("Connected, waiting for image...", "જોડાયેલ, ઇમેજની રાહ જોવાય છે..."), - ("Name", "નામ"), - ("Type", "પ્રકાર"), - ("Modified", "સુધારેલ"), - ("Size", "કદ (Size)"), - ("Show Hidden Files", "છુપાયેલી ફાઇલો બતાવો"), - ("Receive", "મેળવો"), - ("Send", "મોકલો"), - ("Refresh File", "ફાઇલ રિફ્રેશ કરો"), - ("Local", "લોકલ"), - ("Remote", "રિમોટ"), - ("Remote Computer", "રિમોટ કોમ્પ્યુટર"), - ("Local Computer", "લોકલ કોમ્પ્યુટર"), - ("Confirm Delete", "કાઢી નાખવાની પુષ્ટિ કરો"), - ("Delete", "કાઢી નાખો"), - ("Properties", "ગુણધર્મો (Properties)"), - ("Multi Select", "બહુ-પસંદગી"), - ("Select All", "બધું પસંદ કરો"), - ("Unselect All", "બધું નાપસંદ કરો"), - ("Empty Directory", "ખાલી ડિરેક્ટરી"), - ("Not an empty directory", "ડિરેક્ટરી ખાલી નથી"), - ("Are you sure you want to delete this file?", "શું તમે ખરેખર આ ફાઇલ કાઢી નાખવા માંગો છો?"), - ("Are you sure you want to delete this empty directory?", "શું તમે ખરેખર આ ખાલી ડિરેક્ટરી કાઢી નાખવા માંગો છો?"), - ("Are you sure you want to delete the file of this directory?", "શું તમે ખરેખર આ ડિરેક્ટરીની ફાઇલ કાઢી નાખવા માંગો છો?"), - ("Do this for all conflicts", "તમામ વિવાદો માટે આ કરો"), - ("This is irreversible!", "આ બદલી શકાશે નહીં!"), - ("Deleting", "કાઢી નાખવામાં આવી રહ્યું છે"), - ("files", "ફાઇલો"), - ("Waiting", "રાહ જુઓ"), - ("Finished", "પૂરું થયું"), - ("Speed", "ગતિ"), - ("Custom Image Quality", "કસ્ટમ ઇમેજ ગુણવત્તા"), - ("Privacy mode", "પ્રાઇવસી મોડ"), - ("Block user input", "યુઝર ઇનપુટ બ્લોક કરો"), - ("Unblock user input", "યુઝર ઇનપુટ અનબ્લોક કરો"), - ("Adjust Window", "વિન્ડો એડજસ્ટ કરો"), - ("Original", "મૂળ (Original)"), - ("Shrink", "સંકોચો (Shrink)"), - ("Stretch", "ખેંચો (Stretch)"), - ("Scrollbar", "સ્ક્રોલબાર"), - ("ScrollAuto", "ઓટો સ્ક્રોલ"), - ("Good image quality", "સારી ઇમેજ ગુણવત્તા"), - ("Balanced", "સંતુલિત"), - ("Optimize reaction time", "પ્રતિક્રિયા સમય શ્રેષ્ઠ બનાવો"), - ("Custom", "કસ્ટમ"), - ("Show remote cursor", "રિમોટ કર્સર બતાવો"), - ("Show quality monitor", "ક્વોલિટી મોનિટર બતાવો"), - ("Disable clipboard", "ક્લિપબોર્ડ અક્ષમ કરો"), - ("Lock after session end", "સત્ર સમાપ્ત થયા પછી લોક કરો"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del દાખલ કરો"), - ("Insert Lock", "લોક દાખલ કરો"), - ("Refresh", "રિફ્રેશ કરો"), - ("ID does not exist", "ID અસ્તિત્વમાં નથી"), - ("Failed to connect to rendezvous server", "Rendezvous સર્વર સાથે જોડવામાં નિષ્ફળ"), - ("Please try later", "કૃપા કરીને પછી પ્રયાસ કરો"), - ("Remote desktop is offline", "રિમોટ ડેસ્કટોપ ઓફલાઇન છે"), - ("Key mismatch", "કી મેળ ખાતી નથી"), - ("Timeout", "સમય સમાપ્ત"), - ("Failed to connect to relay server", "રિલે સર્વર સાથે જોડવામાં નિષ્ફળ"), - ("Failed to connect via rendezvous server", "Rendezvous સર્વર દ્વારા જોડવામાં નિષ્ફળ"), - ("Failed to connect via relay server", "રિલે સર્વર દ્વારા જોડવામાં નિષ્ફળ"), - ("Failed to make direct connection to remote desktop", "રિમોટ ડેસ્કટોપ સાથે સીધું જોડાણ કરવામાં નિષ્ફળ"), - ("Set Password", "પાસવર્ડ સેટ કરો"), - ("OS Password", "OS પાસવર્ડ"), - ("install_tip", "શ્રેષ્ઠ પ્રદર્શન માટે, કૃપા કરીને ઇન્સ્ટોલ કરો."), - ("Click to upgrade", "અપગ્રેડ કરવા માટે ક્લિક કરો"), - ("Configure", "કોન્ફિગર કરો"), - ("config_acc", "એક્સેસિબિલિટી કોન્ફિગર કરો"), - ("config_screen", "સ્ક્રીન કોન્ફિગર કરો"), - ("Installing ...", "ઇન્સ્ટોલ થઈ રહ્યું છે..."), - ("Install", "ઇન્સ્ટોલ કરો"), - ("Installation", "ઇન્સ્ટોલેશન"), - ("Installation Path", "ઇન્સ્ટોલેશન પાથ"), - ("Create start menu shortcuts", "સ્ટાર્ટ મેનૂ શોર્ટકટ બનાવો"), - ("Create desktop icon", "ડેસ્કટોપ આઇકોન બનાવો"), - ("agreement_tip", "ઇન્સ્ટોલ કરીને તમે લાયસન્સ કરાર સ્વીકારો છો."), - ("Accept and Install", "સ્વીકારો અને ઇન્સ્ટોલ કરો"), - ("End-user license agreement", "અંતિમ વપરાશકર્તા લાયસન્સ કરાર"), - ("Generating ...", "જનરેટ થઈ રહ્યું છે..."), - ("Your installation is lower version.", "તમારું ઇન્સ્ટોલેશન જૂનું સંસ્કરણ છે."), - ("not_close_tcp_tip", "ટનલનો ઉપયોગ કરતી વખતે આ વિન્ડો બંધ કરશો નહીં."), - ("Listening ...", "સાંભળી રહ્યું છે..."), - ("Remote Host", "રિમોટ હોસ્ટ"), - ("Remote Port", "રિમોટ પોર્ટ"), - ("Action", "ક્રિયા"), - ("Add", "ઉમેરો"), - ("Local Port", "લોકલ પોર્ટ"), - ("Local Address", "લોકલ સરનામું"), - ("Change Local Port", "લોકલ પોર્ટ બદલો"), - ("setup_server_tip", "ઝડપી કનેક્શન માટે તમારું પોતાનું સર્વર સેટ કરો"), - ("Too short, at least 6 characters.", "ખૂબ ટૂંકું, ઓછામાં ઓછા 6 અક્ષરો હોવા જોઈએ."), - ("The confirmation is not identical.", "પુષ્ટિકરણ સરખું નથી."), - ("Permissions", "પરવાનગીઓ"), - ("Accept", "સ્વીકારો"), - ("Dismiss", "ખારીજ કરો"), - ("Disconnect", "ડિસ્કનેક્ટ કરો"), - ("Enable file copy and paste", "ફાઇલ કોપી અને પેસ્ટ સક્ષમ કરો"), - ("Connected", "જોડાયેલ"), - ("Direct and encrypted connection", "સીધું અને એન્ક્રિપ્ટેડ કનેક્શન"), - ("Relayed and encrypted connection", "રિલે અને એન્ક્રિપ્ટેડ કનેક્શન"), - ("Direct and unencrypted connection", "સીધું અને અનએન્ક્રિપ્ટેડ કનેક્શન"), - ("Relayed and unencrypted connection", "રિલે અને અનએન્ક્રિપ્ટેડ કનેક્શન"), - ("Enter Remote ID", "રિમોટ ID દાખલ કરો"), - ("Enter your password", "તમારો પાસવર્ડ દાખલ કરો"), - ("Logging in...", "લોગિન થઈ રહ્યું છે..."), - ("Enable RDP session sharing", "RDP સત્ર શેરિંગ સક્ષમ કરો"), - ("Auto Login", "ઓટો લોગિન"), - ("Enable direct IP access", "સીધું IP એક્સેસ સક્ષમ કરો"), - ("Rename", "નામ બદલો"), - ("Space", "જગ્યા (Space)"), - ("Create desktop shortcut", "ડેસ્કટોપ શોર્ટકટ બનાવો"), - ("Change Path", "પાથ બદલો"), - ("Create Folder", "ફોલ્ડર બનાવો"), - ("Please enter the folder name", "કૃપા કરીને ફોલ્ડરનું નામ દાખલ કરો"), - ("Fix it", "તેને ઠીક કરો"), - ("Warning", "ચેતવણી"), - ("Login screen using Wayland is not supported", "Wayland ઉપયોગ કરતી લોગિન સ્ક્રીન સમર્થિત નથી"), - ("Reboot required", "રિબૂટ જરૂરી છે"), - ("Unsupported display server", "અસમર્થિત ડિસ્પ્લે સર્વર"), - ("x11 expected", "x11 અપેક્ષિત છે"), - ("Port", "પોર્ટ"), - ("Settings", "સેટિંગ્સ"), - ("Username", "વપરાશકર્તા નામ"), - ("Invalid port", "અમાન્ય પોર્ટ"), - ("Closed manually by the peer", "સામેથી મેન્યુઅલી બંધ કરવામાં આવ્યું"), - ("Enable remote configuration modification", "રિમોટ કોન્ફિગરેશન ફેરફાર સક્ષમ કરો"), - ("Run without install", "ઇન્સ્ટોલ કર્યા વગર ચલાવો"), - ("Connect via relay", "રિલે દ્વારા કનેક્ટ કરો"), - ("Always connect via relay", "હંમેશા રિલે દ્વારા કનેક્ટ કરો"), - ("whitelist_tip", "માત્ર વ્હાઇટલિસ્ટ કરેલ IP જ મને એક્સેસ કરી શકે છે"), - ("Login", "લોગિન"), - ("Verify", "ચકાસો"), - ("Remember me", "મને યાદ રાખો"), - ("Trust this device", "આ ઉપકરણ પર વિશ્વાસ કરો"), - ("Verification code", "વેરિફિકેશન કોડ"), - ("verification_tip", "વેરિફિકેશન કોડ તમારા ઇમેઇલ પર મોકલવામાં આવ્યો છે"), - ("Logout", "લોગઆઉટ"), - ("Tags", "ટેગ્સ"), - ("Search ID", "ID શોધો"), - ("whitelist_sep", "અલ્પવિરામ, અર્ધવિરામ અથવા સ્પેસ દ્વારા અલગ કરો"), - ("Add ID", "ID ઉમેરો"), - ("Add Tag", "ટેગ ઉમેરો"), - ("Unselect all tags", "તમામ ટેગ નાપસંદ કરો"), - ("Network error", "નેટવર્ક ભૂલ"), - ("Username missed", "વપરાશકર્તા નામ બાકી છે"), - ("Password missed", "પાસવર્ડ બાકી છે"), - ("Wrong credentials", "ખોટી વિગતો"), - ("The verification code is incorrect or has expired", "વેરિફિકેશન કોડ ખોટો છે અથવા તેની મર્યાદા પૂરી થઈ ગઈ છે"), - ("Edit Tag", "ટેગ સુધારો"), - ("Forget Password", "પાસવર્ડ ભૂલી ગયા"), - ("Favorites", "પસંદગીના"), - ("Add to Favorites", "પસંદગીમાં ઉમેરો"), - ("Remove from Favorites", "પસંદગીમાંથી દૂર કરો"), - ("Empty", "ખાલી"), - ("Invalid folder name", "અમાન્ય ફોલ્ડર નામ"), - ("Socks5 Proxy", "Socks5 પ્રોક્સી"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) પ્રોક્સી"), - ("Discovered", "શોધાયેલ"), - ("install_daemon_tip", "બૂટ વખતે શરૂ કરવા માટે સેવા ઇન્સ્ટોલ કરો"), - ("Remote ID", "રિમોટ ID"), - ("Paste", "પેસ્ટ કરો"), - ("Paste here?", "અહીં પેસ્ટ કરવું છે?"), - ("Are you sure to close the connection?", "શું તમે ખરેખર કનેક્શન બંધ કરવા માંગો છો?"), - ("Download new version", "નવું સંસ્કરણ ડાઉનલોડ કરો"), - ("Touch mode", "ટચ મોડ"), - ("Mouse mode", "માઉસ મોડ"), - ("One-Finger Tap", "એક આંગળીથી ટેપ"), - ("Left Mouse", "ડાબું માઉસ બટન"), - ("One-Long Tap", "એક લાંબો ટેપ"), - ("Two-Finger Tap", "બે આંગળીથી ટેપ"), - ("Right Mouse", "જમણું માઉસ બટન"), - ("One-Finger Move", "એક આંગળીથી હલનચલન"), - ("Double Tap & Move", "ડબલ ટેપ અને હલનચલન"), - ("Mouse Drag", "માઉસ ડ્રેગ"), - ("Three-Finger vertically", "ત્રણ આંગળી ઊભી રીતે"), - ("Mouse Wheel", "માઉસ વ્હીલ"), - ("Two-Finger Move", "બે આંગળીથી હલનચલન"), - ("Canvas Move", "કેનવાસ ખસેડો"), - ("Pinch to Zoom", "ઝૂમ કરવા માટે પિંચ કરો"), - ("Canvas Zoom", "કેનવાસ ઝૂમ"), - ("Reset canvas", "કેનવાસ રિસેટ કરો"), - ("No permission of file transfer", "ફાઇલ ટ્રાન્સફરની પરવાનગી નથી"), - ("Note", "નોંધ"), - ("Connection", "કનેક્શન"), - ("Share screen", "સ્ક્રીન શેર કરો"), - ("Chat", "ચેટ"), - ("Total", "કુલ"), - ("items", "વસ્તુઓ"), - ("Selected", "પસંદ કરેલ"), - ("Screen Capture", "સ્ક્રીન કેપ્ચર"), - ("Input Control", "ઇનપુટ નિયંત્રણ"), - ("Audio Capture", "ઓડિયો કેપ્ચર"), - ("Do you accept?", "શું તમે સ્વીકારો છો?"), - ("Open System Setting", "સિસ્ટમ સેટિંગ ખોલો"), - ("How to get Android input permission?", "Android ઇનપુટ પરવાનગી કેવી રીતે મેળવવી?"), - ("android_input_permission_tip1", "ઇનપુટ પરવાનગી મેળવવા માટે એક્સેસિબિલિટી સેવા સક્ષમ કરો."), - ("android_input_permission_tip2", "કૃપા કરીને સેટિંગ્સમાં RustDesk શોધો અને તેને ચાલુ કરો."), - ("android_new_connection_tip", "નવો કંટ્રોલ વિનંતી પ્રાપ્ત થઈ છે."), - ("android_service_will_start_tip", "સ્ક્રીન કેપ્ચર ચાલુ કરવાથી સેવા આપમેળે શરૂ થશે."), - ("android_stop_service_tip", "સેવા બંધ કરવાથી તમામ કનેક્શન બંધ થઈ જશે."), - ("android_version_audio_tip", "ઓડિયો કેપ્ચર માત્ર Android 10 કે તેથી ઉપરના વર્ઝનમાં ઉપલબ્ધ છે."), - ("android_start_service_tip", "સ્ક્રીન શેરિંગ સેવા શરૂ કરવા ક્લિક કરો."), - ("android_permission_may_not_change_tip", "પરવાનગીઓ પછીથી બદલી શકાશે નહીં, કૃપા કરીને કાળજીપૂર્વક પસંદ કરો."), - ("Account", "ખાતું"), - ("Overwrite", "ઓવરરાઇટ કરો"), - ("This file exists, skip or overwrite this file?", "આ ફાઇલ અસ્તિત્વમાં છે, રહેવા દેવી છે કે ઓવરરાઇટ કરવી છે?"), - ("Quit", "બહાર નીકળો"), - ("Help", "મદદ"), - ("Failed", "નિષ્ફળ"), - ("Succeeded", "સફળ"), - ("Someone turns on privacy mode, exit", "કોઈએ પ્રાઇવસી મોડ ચાલુ કર્યો છે, બહાર નીકળો"), - ("Unsupported", "અસમર્થિત"), - ("Peer denied", "સામેથી નકારવામાં આવ્યું"), - ("Please install plugins", "કૃપા કરીને પ્લગઇન્સ ઇન્સ્ટોલ કરો"), - ("Peer exit", "સામેથી કોઈ બહાર નીકળી ગયું"), - ("Failed to turn off", "બંધ કરવામાં નિષ્ફળ"), - ("Turned off", "બંધ કરવામાં આવ્યું"), - ("Language", "ભાષા"), - ("Keep RustDesk background service", "RustDesk બેકગ્રાઉન્ડ સેવા ચાલુ રાખો"), - ("Ignore Battery Optimizations", "બેટરી ઓપ્ટિમાઇઝેશન અવગણો"), - ("android_open_battery_optimizations_tip", "ડિસ્કનેક્શન ટાળવા માટે બેટરી ઓપ્ટિમાઇઝેશન સેટિંગ ખોલો"), - ("Start on boot", "બૂટ પર શરૂ કરો"), - ("Start the screen sharing service on boot, requires special permissions", "બૂટ પર સ્ક્રીન શેરિંગ શરૂ કરો, ખાસ પરવાનગીની જરૂર છે"), - ("Connection not allowed", "કનેક્શનની પરવાનગી નથી"), - ("Legacy mode", "લેગસી મોડ"), - ("Map mode", "મેપ મોડ"), - ("Translate mode", "અનુવાદ મોડ"), - ("Use permanent password", "કાયમી પાસવર્ડનો ઉપયોગ કરો"), - ("Use both passwords", "બંને પાસવર્ડનો ઉપયોગ કરો"), - ("Set permanent password", "કાયમી પાસવર્ડ સેટ કરો"), - ("Enable remote restart", "રિમોટ રિસ્ટાર્ટ સક્ષમ કરો"), - ("Restart remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ કરો"), - ("Are you sure you want to restart", "શું તમે ખરેખર રિસ્ટાર્ટ કરવા માંગો છો?"), - ("Restarting remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે"), - ("remote_restarting_tip", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે, કૃપા કરીને રાહ જુઓ..."), - ("Copied", "કોપી થઈ ગયું"), - ("Exit Fullscreen", "ફુલસ્ક્રીનમાંથી બહાર નીકળો"), - ("Fullscreen", "ફુલસ્ક્રીન"), - ("Mobile Actions", "મોબાઇલ ક્રિયાઓ"), - ("Select Monitor", "મોનિટર પસંદ કરો"), - ("Control Actions", "નિયંત્રણ ક્રિયાઓ"), - ("Display Settings", "ડિસ્પ્લે સેટિંગ્સ"), - ("Ratio", "રેશિયો (Ratio)"), - ("Image Quality", "ઇમેજ ગુણવત્તા"), - ("Scroll Style", "સ્ક્રોલ શૈલી"), - ("Show Toolbar", "ટૂલબાર બતાવો"), - ("Hide Toolbar", "ટૂલબાર છુપાવો"), - ("Direct Connection", "સીધું કનેક્શન"), - ("Relay Connection", "રિલે કનેક્શન"), - ("Secure Connection", "સુરક્ષિત કનેક્શન"), - ("Insecure Connection", "અસુરક્ષિત કનેક્શન"), - ("Scale original", "મૂળ સ્કેલ"), - ("Scale adaptive", "એડેપ્ટિવ સ્કેલ"), - ("General", "સામાન્ય"), - ("Security", "સુરક્ષા"), - ("Theme", "થીમ"), - ("Dark Theme", "ડાર્ક થીમ"), - ("Light Theme", "લાઇટ થીમ"), - ("Dark", "ડાર્ક"), - ("Light", "લાઇટ"), - ("Follow System", "સિસ્ટમ મુજબ"), - ("Enable hardware codec", "હાર્ડવેર કોડેક સક્ષમ કરો"), - ("Unlock Security Settings", "સુરક્ષા સેટિંગ્સ અનલોક કરો"), - ("Enable audio", "ઓડિયો સક્ષમ કરો"), - ("Unlock Network Settings", "નેટવર્ક સેટિંગ્સ અનલોક કરો"), - ("Server", "સર્વર"), - ("Direct IP Access", "સીધું IP એક્સેસ"), - ("Proxy", "પ્રોક્સી"), - ("Apply", "લાગુ કરો"), - ("Disconnect all devices?", "તમામ ઉપકરણો ડિસ્કનેક્ટ કરવા છે?"), - ("Clear", "સાફ કરો"), - ("Audio Input Device", "ઓડિયો ઇનપુટ ઉપકરણ"), - ("Use IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગનો ઉપયોગ કરો"), - ("Network", "નેટવર્ક"), - ("Pin Toolbar", "ટૂલબાર પિન કરો"), - ("Unpin Toolbar", "ટૂલબાર અનપિન કરો"), - ("Recording", "રેકોર્ડિંગ"), - ("Directory", "ડિરેક્ટરી"), - ("Automatically record incoming sessions", "આવતા સત્રો આપમેળે રેકોર્ડ કરો"), - ("Automatically record outgoing sessions", "જતા સત્રો આપમેળે રેકોર્ડ કરો"), - ("Change", "બદલો"), - ("Start session recording", "સત્ર રેકોર્ડિંગ શરૂ કરો"), - ("Stop session recording", "સત્ર રેકોર્ડિંગ બંધ કરો"), - ("Enable recording session", "સત્ર રેકોર્ડિંગ સક્ષમ કરો"), - ("Enable LAN discovery", "LAN ડિસ્કવરી સક્ષમ કરો"), - ("Deny LAN discovery", "LAN ડિસ્કવરી નકારો"), - ("Write a message", "સંદેશ લખો"), - ("Prompt", "પ્રોમ્પ્ટ"), - ("Please wait for confirmation of UAC...", "કૃપા કરીને UAC પુષ્ટિની રાહ જુઓ..."), - ("elevated_foreground_window_tip", "રિમોટની વર્તમાન વિન્ડોને વધારે પરવાનગીની જરૂર છે."), - ("Disconnected", "ડિસ્કનેક્ટ થઈ ગયું"), - ("Other", "અન્ય"), - ("Confirm before closing multiple tabs", "બહુવિધ ટેબ્સ બંધ કરતા પહેલા પુષ્ટિ કરો"), - ("Keyboard Settings", "કીબોર્ડ સેટિંગ્સ"), - ("Full Access", "પૂર્ણ એક્સેસ"), - ("Screen Share", "સ્ક્રીન શેર"), - ("ubuntu-21-04-required", "Ubuntu 21.04 કે તેથી ઉપર જરૂરી છે"), - ("wayland-requires-higher-linux-version", "Wayland માટે ઉચ્ચ Linux વર્ઝન જરૂરી છે"), - ("xdp-portal-unavailable", "XDP પોર્ટલ અનુપલબ્ધ છે"), - ("JumpLink", "JumpLink"), - ("Please Select the screen to be shared(Operate on the peer side).", "કૃપા કરીને શેર કરવાની સ્ક્રીન પસંદ કરો (સામેના છેડે કાર્ય કરો)."), - ("Show RustDesk", "RustDesk બતાવો"), - ("This PC", "આ PC"), - ("or", "અથવા"), - ("Elevate", "એલિવેટ કરો"), - ("Zoom cursor", "ઝૂમ કર્સર"), - ("Accept sessions via password", "પાસવર્ડ દ્વારા સત્રો સ્વીકારો"), - ("Accept sessions via click", "ક્લિક દ્વારા સત્રો સ્વીકારો"), - ("Accept sessions via both", "બંને દ્વારા સત્રો સ્વીકારો"), - ("Please wait for the remote side to accept your session request...", "કૃપા કરીને સામેનો છેડો વિનંતી સ્વીકારે તેની રાહ જુઓ..."), - ("One-time Password", "વન-ટાઇમ પાસવર્ડ (OTP)"), - ("Use one-time password", "વન-ટાઇમ પાસવર્ડનો ઉપયોગ કરો"), - ("One-time password length", "OTP ની લંબાઈ"), - ("Request access to your device", "તમારા ઉપકરણના એક્સેસ માટે વિનંતી"), - ("Hide connection management window", "કનેક્શન મેનેજમેન્ટ વિન્ડો છુપાવો"), - ("hide_cm_tip", "જો પાસવર્ડ દ્વારા કનેક્શન હોય તો જ છુપાવો"), - ("wayland_experiment_tip", "Wayland સપોર્ટ હજુ પ્રાયોગિક ધોરણે છે"), - ("Right click to select tabs", "ટેબ્સ પસંદ કરવા રાઇટ ક્લિક કરો"), - ("Skipped", "રહેવા દીધું (Skipped)"), - ("Add to address book", "એડ્રેસ બુકમાં ઉમેરો"), - ("Group", "ગ્રુપ"), - ("Search", "શોધો"), - ("Closed manually by web console", "વેબ કન્સોલ દ્વારા મેન્યુઅલી બંધ કરવામાં આવ્યું"), - ("Local keyboard type", "લોકલ કીબોર્ડ પ્રકાર"), - ("Select local keyboard type", "લોકલ કીબોર્ડ પ્રકાર પસંદ કરો"), - ("software_render_tip", "જો સ્ક્રીન કાળી દેખાય, તો આ અજમાવો"), - ("Always use software rendering", "હંમેશા સોફ્ટવેર રેન્ડરિંગનો ઉપયોગ કરો"), - ("config_input", "ઇનપુટ કોન્ફિગર કરો"), - ("config_microphone", "માઇક્રોફોન કોન્ફિગર કરો"), - ("request_elevation_tip", "સામેથી ઉચ્ચ પરવાનગી (Elevation) માટે વિનંતી કરો"), - ("Wait", "રાહ જુઓ"), - ("Elevation Error", "એલિવેશન ભૂલ"), - ("Ask the remote user for authentication", "સામેના યુઝરને ઓથેન્ટિકેશન માટે પૂછો"), - ("Choose this if the remote account is administrator", "જો સામેનું ખાતું એડમિનિસ્ટ્રેટર હોય તો આ પસંદ કરો"), - ("Transmit the username and password of administrator", "એડમિનિસ્ટ્રેટરનું નામ અને પાસવર્ડ મોકલો"), - ("still_click_uac_tip", "રિમોટ યુઝરે હજુ પણ UAC વિન્ડોમાં 'હા' ક્લિક કરવું પડશે."), - ("Request Elevation", "એલિવેશન માટે વિનંતી કરો"), - ("wait_accept_uac_tip", "કૃપા કરીને સામેનો યુઝર UAC સ્વીકારે તેની રાહ જુઓ."), - ("Elevate successfully", "સફળતાપૂર્વક એલિવેટ થયું"), - ("uppercase", "મોટા અક્ષરો (Uppercase)"), - ("lowercase", "નાના અક્ષરો (Lowercase)"), - ("digit", "અંક (Digit)"), - ("special character", "ખાસ અક્ષર"), - ("length>=8", "લંબાઈ >= 8"), - ("Weak", "નબળું"), - ("Medium", "મધ્યમ"), - ("Strong", "મજબૂત"), - ("Switch Sides", "બાજુઓ બદલો"), - ("Please confirm if you want to share your desktop?", "શું તમે તમારું ડેસ્કટોપ શેર કરવા માંગો છો?"), - ("Display", "ડિસ્પ્લે"), - ("Default View Style", "ડિફોલ્ટ વ્યુ શૈલી"), - ("Default Scroll Style", "ડિફોલ્ટ સ્ક્રોલ શૈલી"), - ("Default Image Quality", "ડિફોલ્ટ ઇમેજ ગુણવત્તા"), - ("Default Codec", "ડિફોલ્ટ કોડેક"), - ("Bitrate", "બિટરેટ"), - ("FPS", "FPS"), - ("Auto", "ઓટો"), - ("Other Default Options", "અન્ય ડિફોલ્ટ વિકલ્પો"), - ("Voice call", "વોઇસ કોલ"), - ("Text chat", "ટેક્સ્ટ ચેટ"), - ("Stop voice call", "વોઇસ કોલ બંધ કરો"), - ("relay_hint_tip", "સીધું કનેક્શન શક્ય નથી; તમે રિલે દ્વારા પ્રયાસ કરી શકો છો."), - ("Reconnect", "ફરી કનેક્ટ કરો"), - ("Codec", "કોડેક"), - ("Resolution", "રિઝોલ્યુશન"), - ("No transfers in progress", "કોઈ ટ્રાન્સફર ચાલુ નથી"), - ("Set one-time password length", "OTP લંબાઈ સેટ કરો"), - ("RDP Settings", "RDP સેટિંગ્સ"), - ("Sort by", "ક્રમબદ્ધ કરો"), - ("New Connection", "નવું કનેક્શન"), - ("Restore", "રીસ્ટોર"), - ("Minimize", "મિનિમાઇઝ"), - ("Maximize", "મેક્સિમાઇઝ"), - ("Your Device", "તમારું ઉપકરણ"), - ("empty_recent_tip", "તાજેતરના સત્રો અહીં દેખાશે."), - ("empty_favorite_tip", "પસંદગીના ઉપકરણો અહીં દેખાશે."), - ("empty_lan_tip", "નેટવર્ક પરના ઉપકરણો અહીં દેખાશે."), - ("empty_address_book_tip", "તમારી એડ્રેસ બુક ખાલી છે."), - ("Empty Username", "ખાલી યુઝરનેમ"), - ("Empty Password", "ખાલી પાસવર્ડ"), - ("Me", "હું"), - ("identical_file_tip", "આ ફાઇલ પહેલેથી જ અસ્તિત્વમાં છે."), - ("show_monitors_tip", "ટૂલબારમાં મોનિટર બતાવો"), - ("View Mode", "વ્યુ મોડ"), - ("login_linux_tip", "રિમોટ Linux સત્ર માટે તમારે લોગિન કરવું પડશે"), - ("verify_rustdesk_password_tip", "RustDesk પાસવર્ડ ચકાસો"), - ("remember_account_tip", "આ ખાતું યાદ રાખો"), - ("os_account_desk_tip", "એક્સેસ માટે OS ખાતાનો ઉપયોગ કરો"), - ("OS Account", "OS ખાતું"), - ("another_user_login_title_tip", "બીજો યુઝર પહેલેથી લોગિન છે"), - ("another_user_login_text_tip", "ડિસ્કનેક્ટ કરો અને ફરી પ્રયાસ કરો"), - ("xorg_not_found_title_tip", "Xorg મળ્યું નથી"), - ("xorg_not_found_text_tip", "કૃપા કરીને Xorg ઇન્સ્ટોલ કરો"), - ("no_desktop_title_tip", "કોઈ ડેસ્કટોપ ઉપલબ્ધ નથી"), - ("no_desktop_text_tip", "કૃપા કરીને Linux ડેસ્કટોપ ઇન્સ્ટોલ કરો"), - ("No need to elevate", "એલિવેટ કરવાની જરૂર નથી"), - ("System Sound", "સિસ્ટમ સાઉન્ડ"), - ("Default", "ડિફોલ્ટ"), - ("New RDP", "નવું RDP"), - ("Fingerprint", "ફિંગરપ્રિન્ટ"), - ("Copy Fingerprint", "ફિંગરપ્રિન્ટ કોપી કરો"), - ("no fingerprints", "કોઈ ફિંગરપ્રિન્ટ નથી"), - ("Select a peer", "એક પીઅર પસંદ કરો"), - ("Select peers", "પીઅર્સ પસંદ કરો"), - ("Plugins", "પ્લગઇન્સ"), - ("Uninstall", "અનઇન્સ્ટોલ કરો"), - ("Update", "અપડેટ કરો"), - ("Enable", "સક્ષમ કરો"), - ("Disable", "અક્ષમ કરો"), - ("Options", "વિકલ્પો"), - ("resolution_original_tip", "મૂળ રિઝોલ્યુશન"), - ("resolution_fit_local_tip", "સ્ક્રીન મુજબ ફીટ કરો"), - ("resolution_custom_tip", "કસ્ટમ રિઝોલ્યુશન"), - ("Collapse toolbar", "ટૂલબાર નાનું કરો"), - ("Accept and Elevate", "સ્વીકારો અને એલિવેટ કરો"), - ("accept_and_elevate_btn_tooltip", "કનેક્શન સ્વીકારો અને UAC પરવાનગીઓ મેળવો."), - ("clipboard_wait_response_timeout_tip", "ક્લિપબોર્ડ પ્રતિક્રિયા માટે સમય સમાપ્ત થયો."), - ("Incoming connection", "આવતું કનેક્શન"), - ("Outgoing connection", "જતું કનેક્શન"), - ("Exit", "બહાર નીકળો"), - ("Open", "ખોલો"), - ("logout_tip", "શું તમે ખરેખર લોગઆઉટ કરવા માંગો છો?"), - ("Service", "સેવા"), - ("Start", "શરૂ કરો"), - ("Stop", "બંધ કરો"), - ("exceed_max_devices", "તમે ઉપકરણોની મહત્તમ મર્યાદા વટાવી દીધી છે."), - ("Sync with recent sessions", "તાજેતરના સત્રો સાથે સિંક કરો"), - ("Sort tags", "ટેગ્સ ક્રમબદ્ધ કરો"), - ("Open connection in new tab", "નવી ટેબમાં કનેક્શન ખોલો"), - ("Move tab to new window", "ટેબને નવી વિન્ડોમાં ખસેડો"), - ("Can not be empty", "ખાલી ન હોઈ શકે"), - ("Already exists", "પહેલેથી અસ્તિત્વમાં છે"), - ("Change Password", "પાસવર્ડ બદલો"), - ("Refresh Password", "પાસવર્ડ રિફ્રેશ કરો"), - ("ID", "ID"), - ("Grid View", "ગ્રીડ વ્યુ"), - ("List View", "લિસ્ટ વ્યુ"), - ("Select", "પસંદ કરો"), - ("Toggle Tags", "ટેગ્સ ચાલુ/બંધ કરો"), - ("pull_ab_failed_tip", "એડ્રેસ બુક અપડેટ કરવામાં નિષ્ફળ."), - ("push_ab_failed_tip", "એડ્રેસ બુક સિંક કરવામાં નિષ્ફળ."), - ("synced_peer_readded_tip", "તાજેતરના સત્રોના ઉપકરણો એડ્રેસ બુકમાં સિંક થયા."), - ("Change Color", "રંગ બદલો"), - ("Primary Color", "પ્રાથમિક રંગ"), - ("HSV Color", "HSV રંગ"), - ("Installation Successful!", "ઇન્સ્ટોલેશન સફળ!"), - ("Installation failed!", "ઇન્સ્ટોલેશન નિષ્ફળ!"), - ("Reverse mouse wheel", "માઉસ વ્હીલ ઊલટું કરો"), - ("{} sessions", "{} સત્રો"), - ("scam_title", "છેતરપિંડીની ચેતવણી!"), - ("scam_text1", "જો તમે અજાણી વ્યક્તિ સાથે વાત કરી રહ્યા હો અને તેણે RustDesk વાપરવા કહ્યું હોય, તો તરત ડિસ્કનેક્ટ કરો."), - ("scam_text2", "આ એક છેતરપિંડી હોઈ શકે છે. કોઈને પાસવર્ડ આપશો નહીં."), - ("Don't show again", "ફરીથી ના બતાવશો"), - ("I Agree", "હું સહમત છું"), - ("Decline", "અસ્વીકાર"), - ("Timeout in minutes", "મિનિટોમાં ટાઇમઆઉટ"), - ("auto_disconnect_option_tip", "નિષ્ક્રિયતા પર આપમેળે ડિસ્કનેક્ટ કરો"), - ("Connection failed due to inactivity", "નિષ્ક્રિયતાને કારણે કનેક્શન નિષ્ફળ"), - ("Check for software update on startup", "શરૂઆતમાં અપડેટ તપાસો"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "સર્વર પ્રો ને {} માં અપગ્રેડ કરો"), - ("pull_group_failed_tip", "ગ્રુપ ખેંચવામાં (Pull) નિષ્ફળ"), - ("Filter by intersection", "ઇન્ટરસેક્શન દ્વારા ફિલ્ટર કરો"), - ("Remove wallpaper during incoming sessions", "કનેક્શન દરમિયાન વોલપેપર હટાવો"), - ("Test", "ટેસ્ટ"), - ("display_is_plugged_out_msg", "ડિસ્પ્લે કાઢી નાખવામાં આવ્યું છે."), - ("No displays", "કોઈ ડિસ્પ્લે નથી"), - ("Open in new window", "નવી વિન્ડોમાં ખોલો"), - ("Show displays as individual windows", "દરેક ડિસ્પ્લે અલગ વિન્ડોમાં બતાવો"), - ("Use all my displays for the remote session", "તમામ ડિસ્પ્લેનો ઉપયોગ કરો"), - ("selinux_tip", "SELinux ઉપકરણ પર સક્ષમ છે."), - ("Change view", "વ્યુ બદલો"), - ("Big tiles", "મોટી ટાઇલ્સ"), - ("Small tiles", "નાની ટાઇલ્સ"), - ("List", "લિસ્ટ"), - ("Virtual display", "વર્ચ્યુઅલ ડિસ્પ્લે"), - ("Plug out all", "બધું કાઢી નાખો (Plug out)"), - ("True color (4:4:4)", "ટ્રુ કલર (4:4:4)"), - ("Enable blocking user input", "યુઝર ઇનપુટ બ્લોકિંગ સક્ષમ કરો"), - ("id_input_tip", "તમે ID, Alias અથવા IP એડ્રેસ દાખલ કરી શકો છો."), - ("privacy_mode_impl_mag_tip", "મેગ્નિફાયર પ્રાઇવસી મોડ"), - ("privacy_mode_impl_virtual_display_tip", "વર્ચ્યુઅલ ડિસ્પ્લે પ્રાઇવસી મોડ"), - ("Enter privacy mode", "પ્રાઇવસી મોડમાં પ્રવેશ કરો"), - ("Exit privacy mode", "પ્રાઇવસી મોડમાંથી બહાર નીકળો"), - ("idd_not_support_under_win10_2004_tip", "વર્ચ્યુઅલ ડિસ્પ્લે Windows 10 (2004) કે તેથી ઉપર જ સક્ષમ છે."), - ("input_source_1_tip", "ઇનપુટ સ્ત્રોત ૧"), - ("input_source_2_tip", "ઇનપુટ સ્ત્રોત ૨"), - ("Swap control-command key", "Control અને Command કી બદલો"), - ("swap-left-right-mouse", "ડાબું અને જમણું માઉસ બટન બદલો"), - ("2FA code", "2FA કોડ"), - ("More", "વધારે"), - ("enable-2fa-title", "2FA સક્ષમ કરો"), - ("enable-2fa-desc", "તમારું ઓથેન્ટિકેટર એપ સેટ કરો."), - ("wrong-2fa-code", "ખોટો 2FA કોડ."), - ("enter-2fa-title", "2FA કોડ દાખલ કરો"), - ("Email verification code must be 6 characters.", "ઇમેઇલ કોડ 6 અક્ષરનો હોવો જોઈએ."), - ("2FA code must be 6 digits.", "2FA કોડ 6 અંકનો હોવો જોઈએ."), - ("Multiple Windows sessions found", "બહુવિધ Windows સત્રો મળ્યા"), - ("Please select the session you want to connect to", "કૃપા કરીને જે સત્ર સાથે જોડાવું હોય તે પસંદ કરો"), - ("powered_by_me", "મારા દ્વારા સંચાલિત"), - ("outgoing_only_desk_tip", "આ માત્ર આઉટગોઇંગ મોડ છે"), - ("preset_password_warning", "સુરક્ષા માટે પાસવર્ડ બદલો."), - ("Security Alert", "સુરક્ષા ચેતવણી"), - ("My address book", "મારી એડ્રેસ બુક"), - ("Personal", "વ્યક્તિગત"), - ("Owner", "માલિક"), - ("Set shared password", "શેર કરેલ પાસવર્ડ સેટ કરો"), - ("Exist in", "માં અસ્તિત્વ ધરાવે છે"), - ("Read-only", "માત્ર વાંચવા માટે"), - ("Read/Write", "વાંચવા/લખવા માટે"), - ("Full Control", "પૂર્ણ નિયંત્રણ"), - ("share_warning_tip", "તમે તમારો એક્સેસ શેર કરી રહ્યા છો."), - ("Everyone", "દરેક વ્યક્તિ"), - ("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", "રિમોટ વિન્ડો ફોકસને અનુસરો"), - ("default_proxy_tip", "ડિફોલ્ટ પ્રોક્સી સેટિંગ"), - ("no_audio_input_device_tip", "કોઈ ઓડિયો ઇનપુટ મળ્યું નથી."), - ("Incoming", "આવતું"), - ("Outgoing", "જતું"), - ("Clear Wayland screen selection", "Wayland સ્ક્રીન સિલેક્શન સાફ કરો"), - ("clear_Wayland_screen_selection_tip", "સ્ક્રીન સિલેક્શન રીસેટ કરો."), - ("confirm_clear_Wayland_screen_selection_tip", "શું તમે સિલેક્શન સાફ કરવા માંગો છો?"), - ("android_new_voice_call_tip", "નવો વોઇસ કોલ વિનંતી"), - ("texture_render_tip", "ટેક્સચર રેન્ડરિંગ વાપરો"), - ("Use texture rendering", "ટેક્સચર રેન્ડરિંગનો ઉપયોગ કરો"), - ("Floating window", "ફ્લોટિંગ વિન્ડો"), - ("floating_window_tip", "બેકગ્રાઉન્ડમાં હોય ત્યારે RustDesk બતાવો"), - ("Keep screen on", "સ્ક્રીન ચાલુ રાખો"), - ("Never", "ક્યારેય નહીં"), - ("During controlled", "નિયંત્રણ દરમિયાન"), - ("During service is on", "જ્યારે સેવા ચાલુ હોય ત્યારે"), - ("Capture screen using DirectX", "DirectX દ્વારા સ્ક્રીન કેપ્ચર કરો"), - ("Back", "પાછળ"), - ("Apps", "એપ્સ"), - ("Volume up", "અવાજ વધારો"), - ("Volume down", "અવાજ ઘટાડો"), - ("Power", "પાવર"), - ("Telegram bot", "Telegram બોટ"), - ("enable-bot-tip", "સૂચનાઓ માટે બોટ સક્ષમ કરો"), - ("enable-bot-desc", "સૂચનાઓ માટે ટેલિગ્રામ બોટ સેટ કરો."), - ("cancel-2fa-confirm-tip", "શું તમે 2FA રદ કરવા માંગો છો?"), - ("cancel-bot-confirm-tip", "શું તમે બોટ રદ કરવા માંગો છો?"), - ("About RustDesk", "RustDesk વિશે"), - ("Send clipboard keystrokes", "ક્લિપબોર્ડ કી-સ્ટ્રોક્સ મોકલો"), - ("network_error_tip", "નેટવર્ક ભૂલ, ફરી પ્રયાસ કરો."), - ("Unlock with PIN", "PIN થી અનલોક કરો"), - ("Requires at least {} characters", "ઓછામાં ઓછા {} અક્ષર જરૂરી"), - ("Wrong PIN", "ખોટો PIN"), - ("Set PIN", "PIN સેટ કરો"), - ("Enable trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સક્ષમ કરો"), - ("Manage trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સંચાલિત કરો"), - ("Platform", "પ્લેટફોર્મ"), - ("Days remaining", "બાકી દિવસો"), - ("enable-trusted-devices-tip", "માત્ર વિશ્વાસપાત્ર ઉપકરણો જ પાસવર્ડ વગર જોડાઈ શકે"), - ("Parent directory", "પેરન્ટ ડિરેક્ટરી"), - ("Resume", "ફરી શરૂ કરો"), - ("Invalid file name", "અમાન્ય ફાઇલ નામ"), - ("one-way-file-transfer-tip", "માત્ર એકતરફી ફાઇલ ટ્રાન્સફરની મંજૂરી છે"), - ("Authentication Required", "ઓથેન્ટિકેશન જરૂરી"), - ("Authenticate", "ઓથેન્ટિકેટ કરો"), - ("web_id_input_tip", "રિમોટ ID દાખલ કરો"), - ("Download", "ડાઉનલોડ"), - ("Upload folder", "ફોલ્ડર અપલોડ કરો"), - ("Upload files", "ફાઇલો અપલોડ કરો"), - ("Clipboard is synchronized", "ક્લિપબોર્ડ સિંક થયેલ છે"), - ("Update client clipboard", "ક્લાયન્ટ ક્લિપબોર્ડ અપડેટ કરો"), - ("Untagged", "ટેગ વગરનું"), - ("new-version-of-{}-tip", "{} નું નવું વર્ઝન ઉપલબ્ધ છે"), - ("Accessible devices", "એક્સેસિબલ ઉપકરણો"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), - ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), - ("Use D3D rendering", ""), - ("Printer", "પ્રિન્ટર"), - ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), - ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), - ("printer-{}-not-installed-tip", "પ્રિન્ટર {} ઇન્સ્ટોલ નથી."), - ("printer-{}-ready-tip", "પ્રિન્ટર {} તૈયાર છે."), - ("Install {} Printer", "{} પ્રિન્ટર ઇન્સ્ટોલ કરો"), - ("Outgoing Print Jobs", "જતા પ્રિન્ટ કાર્યો"), - ("Incoming Print Jobs", "આવતા પ્રિન્ટ કાર્યો"), - ("Incoming Print Job", "આવતું પ્રિન્ટ કાર્ય"), - ("use-the-default-printer-tip", "ડિફોલ્ટ પ્રિન્ટર વાપરો"), - ("use-the-selected-printer-tip", "પસંદ કરેલ પ્રિન્ટર વાપરો"), - ("auto-print-tip", "આપમેળે પ્રિન્ટ કરો"), - ("print-incoming-job-confirm-tip", "પ્રિન્ટ કરતા પહેલા પુષ્ટિ કરો"), - ("remote-printing-disallowed-tile-tip", "રિમોટ પ્રિન્ટિંગની મંજૂરી નથી"), - ("remote-printing-disallowed-text-tip", "સેટિંગ્સમાં રિમોટ પ્રિન્ટિંગ સક્ષમ કરો."), - ("save-settings-tip", "સેટિંગ્સ સાચવો"), - ("dont-show-again-tip", "ફરીથી ના બતાવશો"), - ("Take screenshot", "સ્ક્રીનશોટ લો"), - ("Taking screenshot", "સ્ક્રીનશોટ લેવાઈ રહ્યો છે"), - ("screenshot-merged-screen-not-supported-tip", "મર્જ કરેલ સ્ક્રીનશોટ સપોર્ટેડ નથી."), - ("screenshot-action-tip", "સ્ક્રીનશોટ પછીની ક્રિયા"), - ("Save as", "તરીકે સાચવો"), - ("Copy to clipboard", "ક્લિપબોર્ડમાં કોપી કરો"), - ("Enable remote printer", "રિમોટ પ્રિન્ટર સક્ષમ કરો"), - ("Downloading {}", "{} ડાઉનલોડ થઈ રહ્યું છે"), - ("{} Update", "{} અપડેટ"), - ("{}-to-update-tip", "અપડેટ કરવા માટે {}"), - ("download-new-version-failed-tip", "નવું વર્ઝન ડાઉનલોડ કરવામાં નિષ્ફળ."), - ("Auto update", "ઓટો અપડેટ"), - ("update-failed-check-msi-tip", "અપડેટ નિષ્ફળ, MSI ફાઇલ તપાસો."), - ("websocket_tip", "જો પોર્ટ બ્લોક હોય તો WebSocket વાપરો."), - ("Use WebSocket", "WebSocket નો ઉપયોગ કરો"), - ("Trackpad speed", "ટ્રેકપેડ સ્પીડ"), - ("Default trackpad speed", "ડિફોલ્ટ ટ્રેકપેડ સ્પીડ"), - ("Numeric one-time password", "ન્યુમેરિક OTP"), - ("Enable IPv6 P2P connection", "IPv6 P2P કનેક્શન સક્ષમ કરો"), - ("Enable UDP hole punching", "UDP હોલ પંચિંગ સક્ષમ કરો"), - ("View camera", "કેમેરા જુઓ"), - ("Enable camera", "કેમેરા સક્ષમ કરો"), - ("No cameras", "કોઈ કેમેરા મળ્યો નથી"), - ("view_camera_unsupported_tip", "રિમોટ કેમેરા સપોર્ટેડ નથી."), - ("Terminal", "ટર્મિનલ"), - ("Enable terminal", "ટર્મિનલ સક્ષમ કરો"), - ("New tab", "નવી ટેબ"), - ("Keep terminal sessions on disconnect", "ડિસ્કનેક્ટ વખતે ટર્મિનલ ચાલુ રાખો"), - ("Terminal (Run as administrator)", "ટર્મિનલ (એડમિનિસ્ટ્રેટર તરીકે)"), - ("terminal-admin-login-tip", "એડમિન લોગિન જરૂરી છે."), - ("Failed to get user token.", "યુઝર ટોકન મેળવવામાં નિષ્ફળ."), - ("Incorrect username or password.", "ખોટું યુઝરનેમ કે પાસવર્ડ."), - ("The user is not an administrator.", "યુઝર એડમિનિસ્ટ્રેટર નથી."), - ("Failed to check if the user is an administrator.", "યુઝર એડમિન છે કે નહીં તે ચકાસવામાં નિષ્ફળ."), - ("Supported only in the installed version.", "માત્ર ઇન્સ્ટોલ કરેલ વર્ઝનમાં ઉપલબ્ધ."), - ("elevation_username_tip", "એડમિનિસ્ટ્રેટર નામ દાખલ કરો"), - ("Preparing for installation ...", "ઇન્સ્ટોલેશનની તૈયારી..."), - ("Show my cursor", "મારું કર્સર બતાવો"), - ("Scale custom", "કસ્ટમ સ્કેલ"), - ("Custom scale slider", "કસ્ટમ સ્કેલ સ્લાઇડર"), - ("Decrease", "ઘટાડો"), - ("Increase", "વધારો"), - ("Show virtual mouse", "વર્ચ્યુઅલ માઉસ બતાવો"), - ("Virtual mouse size", "વર્ચ્યુઅલ માઉસ કદ"), - ("Small", "નાનું"), - ("Large", "મોટું"), - ("Show virtual joystick", "વર્ચ્યુઅલ જોયસ્ટિક બતાવો"), - ("Edit note", "નોંધ સુધારો"), - ("Alias", "Alias (ઉપનામ)"), - ("ScrollEdge", "સ્ક્રોલ એજ"), - ("Allow insecure TLS fallback", "અસુરક્ષિત TLS ફોલબેકની મંજૂરી આપો"), - ("allow-insecure-tls-fallback-tip", "જૂના સર્વર માટે વાપરો."), - ("Disable UDP", "UDP અક્ષમ કરો"), - ("disable-udp-tip", "કનેક્શન સમસ્યાઓ માટે UDP બંધ કરો."), - ("server-oss-not-support-tip", "OSS સર્વર આને સપોર્ટ કરતું નથી."), - ("input note here", "અહીં નોંધ લખો"), - ("note-at-conn-end-tip", "કનેક્શનના અંતે નોંધ બતાવો"), - ("Show terminal extra keys", "ટર્મિનલની વધારાની કી બતાવો"), - ("Relative mouse mode", "રીલેટિવ માઉસ મોડ"), - ("rel-mouse-not-supported-peer-tip", "સામેથી સપોર્ટેડ નથી."), - ("rel-mouse-not-ready-tip", "તૈયાર નથી."), - ("rel-mouse-lock-failed-tip", "માઉસ લોક નિષ્ફળ."), - ("rel-mouse-exit-{}-tip", "બહાર નીકળવા {} દબાવો"), - ("rel-mouse-permission-lost-tip", "પરવાનગી ગુમાવી દીધી."), - ("Changelog", "Changelog (ફેરફારો)"), - ("keep-awake-during-outgoing-sessions-label", "આઉટગોઇંગ સત્ર વખતે જાગૃત રાખો"), - ("keep-awake-during-incoming-sessions-label", "ઇનકમિંગ સત્ર વખતે જાગૃત રાખો"), - ("Continue with {}", "{} સાથે આગળ વધો"), - ("Display Name", "ડિસ્પ્લે નામ"), - ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), - ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), - ].iter().cloned().collect(); -} diff --git a/src/lang/he.rs b/src/lang/he.rs index 44b940784..00999708f 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "הגדרות מקלדת"), ("Full Access", "גישה מלאה"), ("Screen Share", "שיתוף מסך"), - ("ubuntu-21-04-required", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"), - ("wayland-requires-higher-linux-version", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"), ("JumpLink", "קישור מהיר"), ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), ("Show RustDesk", "הצג את RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "המשך עם {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs deleted file mode 100644 index 904d43118..000000000 --- a/src/lang/hi.rs +++ /dev/null @@ -1,749 +0,0 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "स्थिति"), - ("Your Desktop", "आपका डेस्कटॉप"), - ("desk_tip", "आपका डेस्कटॉप इस आईडी और पासवर्ड से एक्सेस किया जा सकता है।"), - ("Password", "पासवर्ड"), - ("Ready", "तैयार"), - ("Established", "स्थापित"), - ("connecting_status", "नेटवर्क से जुड़ रहा है..."), - ("Enable service", "सेवा सक्षम करें"), - ("Start service", "सेवा शुरू करें"), - ("Service is running", "सेवा चल रही है"), - ("Service is not running", "सेवा नहीं चल रही है"), - ("not_ready_status", "तैयार नहीं। कृपया अपना कनेक्शन जांचें"), - ("Control Remote Desktop", "रिमोट डेस्कटॉप नियंत्रित करें"), - ("Transfer file", "फ़ाइल स्थानांतरण"), - ("Connect", "जुड़ें"), - ("Recent sessions", "हाल के सत्र"), - ("Address book", "पता पुस्तिका"), - ("Confirmation", "पुष्टि"), - ("TCP tunneling", "TCP टनलिंग"), - ("Remove", "हटाएं"), - ("Refresh random password", "यादृच्छिक (Random) पासवर्ड बदलें"), - ("Set your own password", "अपना पासवर्ड सेट करें"), - ("Enable keyboard/mouse", "कीबोर्ड/माउस सक्षम करें"), - ("Enable clipboard", "क्लिपबोर्ड सक्षम करें"), - ("Enable file transfer", "फ़ाइल स्थानांतरण सक्षम करें"), - ("Enable TCP tunneling", "TCP टनलिंग सक्षम करें"), - ("IP Whitelisting", "IP श्वेतसूची (Whitelisting)"), - ("ID/Relay Server", "ID/रिले सर्वर"), - ("Import server config", "सर्वर कॉन्फ़िगरेशन इम्पोर्ट करें"), - ("Export Server Config", "सर्वर कॉन्फ़िगरेशन एक्सपोर्ट करें"), - ("Import server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक इम्पोर्ट किया गया"), - ("Export server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक एक्सपोर्ट किया गया"), - ("Invalid server configuration", "अमान्य सर्वर कॉन्फ़िगरेशन"), - ("Clipboard is empty", "क्लिपबोर्ड खाली है"), - ("Stop service", "सेवा रोकें"), - ("Change ID", "ID बदलें"), - ("Your new ID", "आपकी नई ID"), - ("length %min% to %max%", "लंबाई %min% से %max% तक"), - ("starts with a letter", "एक अक्षर से शुरू होता है"), - ("allowed characters", "अनुमत अक्षर"), - ("id_change_tip", "ID बदलने के बाद वर्तमान कनेक्शन टूट जाएगा।"), - ("Website", "वेबसाइट"), - ("About", "के बारे में"), - ("Slogan_tip", "बेहतर अनुभव के लिए बनाया गया रिमोट डेस्कटॉप सॉफ़्टवेयर"), - ("Privacy Statement", "गोपनीयता कथन"), - ("Mute", "म्यूट करें"), - ("Build Date", "निर्माण तिथि"), - ("Version", "संस्करण"), - ("Home", "होम"), - ("Audio Input", "ऑडियो इनपुट"), - ("Enhancements", "वृद्धि (Enhancements)"), - ("Hardware Codec", "हार्डवेयर कोडेक"), - ("Adaptive bitrate", "अनुकूली (Adaptive) बिटरेट"), - ("ID Server", "ID सर्वर"), - ("Relay Server", "रिले सर्वर"), - ("API Server", "API सर्वर"), - ("invalid_http", "अमान्य HTTP लिंक"), - ("Invalid IP", "अमान्य IP"), - ("Invalid format", "अमान्य प्रारूप"), - ("server_not_support", "सर्वर द्वारा समर्थित नहीं"), - ("Not available", "उपलब्ध नहीं"), - ("Too frequent", "बहुत बार-बार"), - ("Cancel", "रद्द करें"), - ("Skip", "छोड़ें"), - ("Close", "बंद करें"), - ("Retry", "पुनः प्रयास करें"), - ("OK", "ठीक है"), - ("Password Required", "पासवर्ड आवश्यक है"), - ("Please enter your password", "कृपया अपना पासवर्ड दर्ज करें"), - ("Remember password", "पासवर्ड याद रखें"), - ("Wrong Password", "गलत पासवर्ड"), - ("Do you want to enter again?", "क्या आप दोबारा दर्ज करना चाहते हैं?"), - ("Connection Error", "कनेक्शन त्रुटि"), - ("Error", "त्रुटि"), - ("Reset by the peer", "दूसरे सिस्टम द्वारा रिसेट किया गया"), - ("Connecting...", "जुड़ रहा है..."), - ("Connection in progress. Please wait.", "कनेक्शन जारी है। कृपया प्रतीक्षा करें।"), - ("Please try 1 minute later", "कृपया 1 मिनट बाद पुनः प्रयास करें"), - ("Login Error", "लॉगिन त्रुटि"), - ("Successful", "सफल"), - ("Connected, waiting for image...", "जुड़ गया, इमेज की प्रतीक्षा कर रहा है..."), - ("Name", "नाम"), - ("Type", "प्रकार"), - ("Modified", "संशोधित"), - ("Size", "आकार"), - ("Show Hidden Files", "छिपी हुई फाइलें दिखाएं"), - ("Receive", "प्राप्त करें"), - ("Send", "भेजें"), - ("Refresh File", "फ़ाइल रिफ्रेश करें"), - ("Local", "स्थानीय (Local)"), - ("Remote", "रिमोट"), - ("Remote Computer", "रिमोट कंप्यूटर"), - ("Local Computer", "स्थानीय कंप्यूटर"), - ("Confirm Delete", "हटाने की पुष्टि करें"), - ("Delete", "हटाएं"), - ("Properties", "गुण (Properties)"), - ("Multi Select", "बहु-चयन"), - ("Select All", "सभी चुनें"), - ("Unselect All", "सभी अचयनित करें"), - ("Empty Directory", "खाली निर्देशिका"), - ("Not an empty directory", "निर्देशिका खाली नहीं है"), - ("Are you sure you want to delete this file?", "क्या आप वाकई इस फ़ाइल को हटाना चाहते हैं?"), - ("Are you sure you want to delete this empty directory?", "क्या आप वाकई इस खाली निर्देशिका को हटाना चाहते हैं?"), - ("Are you sure you want to delete the file of this directory?", "क्या आप वाकई इस निर्देशिका की फ़ाइल को हटाना चाहते हैं?"), - ("Do this for all conflicts", "सभी विवादों के लिए यह करें"), - ("This is irreversible!", "इसे वापस नहीं लिया जा सकता!"), - ("Deleting", "हटाया जा रहा है"), - ("files", "फाइलें"), - ("Waiting", "प्रतीक्षा कर रहा है"), - ("Finished", "पूरा हुआ"), - ("Speed", "गति"), - ("Custom Image Quality", "कस्टम इमेज गुणवत्ता"), - ("Privacy mode", "गोपनीयता मोड"), - ("Block user input", "उपयोगकर्ता इनपुट ब्लॉक करें"), - ("Unblock user input", "उपयोगकर्ता इनपुट अनब्लॉक करें"), - ("Adjust Window", "विंडो समायोजित करें"), - ("Original", "मूल (Original)"), - ("Shrink", "सिकुड़ें"), - ("Stretch", "खिंचाव (Stretch)"), - ("Scrollbar", "स्क्रोलबार"), - ("ScrollAuto", "ऑटो स्क्रॉल"), - ("Good image quality", "अच्छी इमेज गुणवत्ता"), - ("Balanced", "संतुलित"), - ("Optimize reaction time", "प्रतिक्रिया समय अनुकूलित करें"), - ("Custom", "कस्टम"), - ("Show remote cursor", "रिमोट कर्सर दिखाएं"), - ("Show quality monitor", "गुणवत्ता मॉनिटर दिखाएं"), - ("Disable clipboard", "क्लिपबोर्ड अक्षम करें"), - ("Lock after session end", "सत्र समाप्त होने के बाद लॉक करें"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del डालें"), - ("Insert Lock", "लॉक डालें"), - ("Refresh", "रिफ्रेश करें"), - ("ID does not exist", "ID मौजूद नहीं है"), - ("Failed to connect to rendezvous server", "Rendezvous सर्वर से जुड़ने में विफल"), - ("Please try later", "कृपया बाद में प्रयास करें"), - ("Remote desktop is offline", "रिमोट डेस्कटॉप ऑफ़लाइन है"), - ("Key mismatch", "कुंजी बेमेल (Key mismatch)"), - ("Timeout", "समय समाप्त"), - ("Failed to connect to relay server", "रिले सर्वर से जुड़ने में विफल"), - ("Failed to connect via rendezvous server", "Rendezvous सर्वर के माध्यम से जुड़ने में विफल"), - ("Failed to connect via relay server", "रिले सर्वर के माध्यम से जुड़ने में विफल"), - ("Failed to make direct connection to remote desktop", "रिमोट डेस्कटॉप से सीधा कनेक्शन बनाने में विफल"), - ("Set Password", "पासवर्ड सेट करें"), - ("OS Password", "OS पासवर्ड"), - ("install_tip", "सर्वोत्तम प्रदर्शन के लिए, इसे इंस्टॉल करें।"), - ("Click to upgrade", "अपग्रेड करने के लिए क्लिक करें"), - ("Configure", "कॉन्फ़िगर करें"), - ("config_acc", "एक्सेसिबिलिटी कॉन्फ़िगर करें"), - ("config_screen", "स्क्रीन कॉन्फ़िगर करें"), - ("Installing ...", "इंस्टॉल हो रहा है..."), - ("Install", "इंस्टॉल करें"), - ("Installation", "इंस्टॉलेशन"), - ("Installation Path", "इंस्टॉलेशन पाथ"), - ("Create start menu shortcuts", "स्टार्ट मेनू शॉर्टकट बनाएं"), - ("Create desktop icon", "डेस्कटॉप आइकन बनाएं"), - ("agreement_tip", "इंस्टॉल करके आप लाइसेंस समझौते को स्वीकार करते हैं।"), - ("Accept and Install", "स्वीकार करें और इंस्टॉल करें"), - ("End-user license agreement", "अंतिम उपयोगकर्ता लाइसेंस समझौता"), - ("Generating ...", "बनाया जा रहा है..."), - ("Your installation is lower version.", "आपका वर्तमान इंस्टॉलेशन पुराना संस्करण है।"), - ("not_close_tcp_tip", "टनल का उपयोग करते समय इस विंडो को बंद न करें।"), - ("Listening ...", "सुन रहा है (Listening)..."), - ("Remote Host", "रिमोट होस्ट"), - ("Remote Port", "रिमोट पोर्ट"), - ("Action", "कार्य"), - ("Add", "जोड़ें"), - ("Local Port", "स्थानीय पोर्ट"), - ("Local Address", "स्थानीय पता"), - ("Change Local Port", "स्थानीय पोर्ट बदलें"), - ("setup_server_tip", "तेज़ कनेक्शन के लिए अपना खुद का सर्वर सेटअप करें"), - ("Too short, at least 6 characters.", "बहुत छोटा, कम से कम 6 अक्षर होने चाहिए।"), - ("The confirmation is not identical.", "पुष्टि समान नहीं है।"), - ("Permissions", "अनुमतियाँ"), - ("Accept", "स्वीकार करें"), - ("Dismiss", "खारिज करें"), - ("Disconnect", "डिस्कनेक्ट करें"), - ("Enable file copy and paste", "फ़ाइल कॉपी और पेस्ट सक्षम करें"), - ("Connected", "जुड़ गया"), - ("Direct and encrypted connection", "सीधा और एन्क्रिप्टेड कनेक्शन"), - ("Relayed and encrypted connection", "रिले और एन्क्रिप्टेड कनेक्शन"), - ("Direct and unencrypted connection", "सीधा और अनएन्क्रिप्टेड कनेक्शन"), - ("Relayed and unencrypted connection", "रिले और अनएन्क्रिप्टेड कनेक्शन"), - ("Enter Remote ID", "रिमोट ID दर्ज करें"), - ("Enter your password", "अपना पासवर्ड दर्ज करें"), - ("Logging in...", "लॉग इन हो रहा है..."), - ("Enable RDP session sharing", "RDP सत्र साझाकरण सक्षम करें"), - ("Auto Login", "ऑटो लॉगिन"), - ("Enable direct IP access", "सीधी IP पहुंच सक्षम करें"), - ("Rename", "नाम बदलें"), - ("Space", "स्थान (Space)"), - ("Create desktop shortcut", "डेस्कटॉप शॉर्टकट बनाएं"), - ("Change Path", "पाथ बदलें"), - ("Create Folder", "फ़ोल्डर बनाएं"), - ("Please enter the folder name", "कृपया फ़ोल्डर का नाम दर्ज करें"), - ("Fix it", "इसे ठीक करें"), - ("Warning", "चेतावनी"), - ("Login screen using Wayland is not supported", "Wayland का उपयोग करने वाली लॉगिन स्क्रीन समर्थित नहीं है"), - ("Reboot required", "रीबूट आवश्यक है"), - ("Unsupported display server", "असमर्थित डिस्प्ले सर्वर"), - ("x11 expected", "x11 अपेक्षित है"), - ("Port", "पोर्ट"), - ("Settings", "सेटिंग्स"), - ("Username", "उपयोगकर्ता नाम"), - ("Invalid port", "अमान्य पोर्ट"), - ("Closed manually by the peer", "दूसरे सिस्टम द्वारा मैन्युअल रूप से बंद किया गया"), - ("Enable remote configuration modification", "रिमोट कॉन्फ़िगरेशन संशोधन सक्षम करें"), - ("Run without install", "बिना इंस्टॉल किए चलाएं"), - ("Connect via relay", "रिले के माध्यम से जुड़ें"), - ("Always connect via relay", "हमेशा रिले के माध्यम से जुड़ें"), - ("whitelist_tip", "केवल श्वेतसूचीबद्ध IP ही मुझ तक पहुंच सकते हैं"), - ("Login", "लॉगिन"), - ("Verify", "सत्यापित करें"), - ("Remember me", "मुझे याद रखें"), - ("Trust this device", "इस डिवाइस पर भरोसा करें"), - ("Verification code", "सत्यापन कोड"), - ("verification_tip", "एक सत्यापन कोड आपके ईमेल पर भेजा गया है"), - ("Logout", "लॉगआउट"), - ("Tags", "टैग"), - ("Search ID", "ID खोजें"), - ("whitelist_sep", "अल्पविराम, अर्धविराम या रिक्त स्थान द्वारा अलग किया गया"), - ("Add ID", "ID जोड़ें"), - ("Add Tag", "टैग जोड़ें"), - ("Unselect all tags", "सभी टैग अचयनित करें"), - ("Network error", "नेटवर्क त्रुटि"), - ("Username missed", "उपयोगकर्ता नाम छूट गया"), - ("Password missed", "पासवर्ड छूट गया"), - ("Wrong credentials", "गलत क्रेडेंशियल"), - ("The verification code is incorrect or has expired", "सत्यापन कोड गलत है या समाप्त हो गया है"), - ("Edit Tag", "टैग संपादित करें"), - ("Forget Password", "पासवर्ड भूल गए"), - ("Favorites", "पसंदीदा"), - ("Add to Favorites", "पसंदीदा में जोड़ें"), - ("Remove from Favorites", "पसंदीदा से हटाएं"), - ("Empty", "खाली"), - ("Invalid folder name", "अमान्य फ़ोल्डर नाम"), - ("Socks5 Proxy", "Socks5 प्रॉक्सी"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) प्रॉक्सी"), - ("Discovered", "खोजा गया"), - ("install_daemon_tip", "बूट पर शुरू करने के लिए सेवा इंस्टॉल करें"), - ("Remote ID", "रिमोट ID"), - ("Paste", "पेस्ट करें"), - ("Paste here?", "यहाँ पेस्ट करें?"), - ("Are you sure to close the connection?", "क्या आप वाकई कनेक्शन बंद करना चाहते हैं?"), - ("Download new version", "नया संस्करण डाउनलोड करें"), - ("Touch mode", "टच मोड"), - ("Mouse mode", "माउस मोड"), - ("One-Finger Tap", "एक उंगली से टैप"), - ("Left Mouse", "बायां माउस"), - ("One-Long Tap", "एक लंबा टैप"), - ("Two-Finger Tap", "दो उंगलियों से टैप"), - ("Right Mouse", "दायां माउस"), - ("One-Finger Move", "एक उंगली से हिलाएं"), - ("Double Tap & Move", "डबल टैप और हिलाएं"), - ("Mouse Drag", "माउस ड्रैग"), - ("Three-Finger vertically", "तीन उंगलियां लंबवत"), - ("Mouse Wheel", "माउस व्हील"), - ("Two-Finger Move", "दो उंगलियों से हिलाएं"), - ("Canvas Move", "कैनवास मूव"), - ("Pinch to Zoom", "ज़ूम करने के लिए पिंच करें"), - ("Canvas Zoom", "कैनवास ज़ूम"), - ("Reset canvas", "कैनवास रिसेट करें"), - ("No permission of file transfer", "फ़ाइल स्थानांतरण की अनुमति नहीं है"), - ("Note", "नोट"), - ("Connection", "कनेक्शन"), - ("Share screen", "स्क्रीन शेयर करें"), - ("Chat", "चैट"), - ("Total", "कुल"), - ("items", "आइटम"), - ("Selected", "चयनित"), - ("Screen Capture", "स्क्रीन कैप्चर"), - ("Input Control", "इनपुट नियंत्रण"), - ("Audio Capture", "ऑडियो कैप्चर"), - ("Do you accept?", "क्या आप स्वीकार करते हैं?"), - ("Open System Setting", "सिस्टम सेटिंग खोलें"), - ("How to get Android input permission?", "Android इनपुट अनुमति कैसे प्राप्त करें?"), - ("android_input_permission_tip1", "इनपुट अनुमति प्राप्त करने के लिए एक्सेसिबिलिटी सेवा सक्षम करें।"), - ("android_input_permission_tip2", "कृपया सिस्टम सेटिंग में RustDesk खोजें और इसे चालू करें।"), - ("android_new_connection_tip", "एक नया नियंत्रण अनुरोध प्राप्त हुआ है।"), - ("android_service_will_start_tip", "स्क्रीन कैप्चर चालू करने से सेवा अपने आप शुरू हो जाएगी।"), - ("android_stop_service_tip", "सेवा बंद करने से सभी कनेक्शन टूट जाएंगे।"), - ("android_version_audio_tip", "ऑडियो कैप्चर केवल Android 10 या उच्चतर पर समर्थित है।"), - ("android_start_service_tip", "स्क्रीन शेयरिंग सेवा शुरू करने के लिए क्लिक करें।"), - ("android_permission_may_not_change_tip", "अनुमतियाँ बाद में नहीं बदली जा सकती हैं, कृपया ध्यान से चुनें।"), - ("Account", "खाता"), - ("Overwrite", "ओवरराइट (Overwrite) करें"), - ("This file exists, skip or overwrite this file?", "यह फ़ाइल मौजूद है, छोड़ें या ओवरराइट करें?"), - ("Quit", "बाहर निकलें"), - ("Help", "सहायता"), - ("Failed", "विफल"), - ("Succeeded", "सफल"), - ("Someone turns on privacy mode, exit", "किसी ने गोपनीयता मोड चालू किया है, बाहर निकल रहे हैं"), - ("Unsupported", "असमर्थित"), - ("Peer denied", "दूसरे सिस्टम ने मना कर दिया"), - ("Please install plugins", "कृपया प्लगइन्स इंस्टॉल करें"), - ("Peer exit", "दूसरा सिस्टम बाहर निकल गया"), - ("Failed to turn off", "बंद करने में विफल"), - ("Turned off", "बंद कर दिया गया"), - ("Language", "भाषा"), - ("Keep RustDesk background service", "RustDesk बैकग्राउंड सेवा चालू रखें"), - ("Ignore Battery Optimizations", "बैटरी ऑप्टिमाइजेशन को अनदेखा करें"), - ("android_open_battery_optimizations_tip", "डिस्कनेक्शन से बचने के लिए बैटरी ऑप्टिमाइजेशन सेटिंग खोलें"), - ("Start on boot", "बूट पर शुरू करें"), - ("Start the screen sharing service on boot, requires special permissions", "बूट पर स्क्रीन शेयरिंग सेवा शुरू करें, विशेष अनुमतियों की आवश्यकता है"), - ("Connection not allowed", "कनेक्शन की अनुमति नहीं है"), - ("Legacy mode", "लेगेसी (Legacy) मोड"), - ("Map mode", "मैप मोड"), - ("Translate mode", "अनुवाद मोड"), - ("Use permanent password", "स्थायी पासवर्ड का उपयोग करें"), - ("Use both passwords", "दोनों पासवर्ड का उपयोग करें"), - ("Set permanent password", "स्थायी पासवर्ड सेट करें"), - ("Enable remote restart", "रिमोट रीस्टार्ट सक्षम करें"), - ("Restart remote device", "रिमोट डिवाइस रीस्टार्ट करें"), - ("Are you sure you want to restart", "क्या आप वाकई रीस्टार्ट करना चाहते हैं?"), - ("Restarting remote device", "रिमोट डिवाइस रीस्टार्ट हो रहा है"), - ("remote_restarting_tip", "रिमोट डिवाइस रीस्टार्ट हो रहा है, कृपया प्रतीक्षा करें..."), - ("Copied", "कॉपी किया गया"), - ("Exit Fullscreen", "फुलस्क्रीन से बाहर निकलें"), - ("Fullscreen", "फुलस्क्रीन"), - ("Mobile Actions", "मोबाइल क्रियाएं"), - ("Select Monitor", "मॉनिटर चुनें"), - ("Control Actions", "नियंत्रण क्रियाएं"), - ("Display Settings", "डिस्प्ले सेटिंग्स"), - ("Ratio", "अनुपात (Ratio)"), - ("Image Quality", "इमेज गुणवत्ता"), - ("Scroll Style", "स्क्रॉल शैली"), - ("Show Toolbar", "टूलबार दिखाएं"), - ("Hide Toolbar", "टूलबार छुपाएं"), - ("Direct Connection", "सीधा कनेक्शन"), - ("Relay Connection", "रिले कनेक्शन"), - ("Secure Connection", "सुरक्षित कनेक्शन"), - ("Insecure Connection", "असुरक्षित कनेक्शन"), - ("Scale original", "मूल पैमाना"), - ("Scale adaptive", "अनुकूली पैमाना"), - ("General", "सामान्य"), - ("Security", "सुरक्षा"), - ("Theme", "थीम"), - ("Dark Theme", "डार्क थीम"), - ("Light Theme", "लाइट थीम"), - ("Dark", "डार्क"), - ("Light", "लाइट"), - ("Follow System", "सिस्टम का पालन करें"), - ("Enable hardware codec", "हार्डवेयर कोडेक सक्षम करें"), - ("Unlock Security Settings", "सुरक्षा सेटिंग्स अनलॉक करें"), - ("Enable audio", "ऑडियो सक्षम करें"), - ("Unlock Network Settings", "नेटवर्क सेटिंग्स अनलॉक करें"), - ("Server", "सर्वर"), - ("Direct IP Access", "सीधी IP पहुंच"), - ("Proxy", "प्रॉक्सी"), - ("Apply", "लागू करें"), - ("Disconnect all devices?", "सभी डिवाइस डिस्कनेक्ट करें?"), - ("Clear", "साफ करें"), - ("Audio Input Device", "ऑडियो इनपुट डिवाइस"), - ("Use IP Whitelisting", "IP श्वेतसूची का उपयोग करें"), - ("Network", "नेटवर्क"), - ("Pin Toolbar", "टूलबार पिन करें"), - ("Unpin Toolbar", "टूलबार अनपिन करें"), - ("Recording", "रिकॉर्डिंग"), - ("Directory", "निर्देशिका"), - ("Automatically record incoming sessions", "आने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"), - ("Automatically record outgoing sessions", "जाने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"), - ("Change", "बदलें"), - ("Start session recording", "सत्र रिकॉर्डिंग शुरू करें"), - ("Stop session recording", "सत्र रिकॉर्डिंग रोकें"), - ("Enable recording session", "सत्र रिकॉर्डिंग सक्षम करें"), - ("Enable LAN discovery", "LAN खोज सक्षम करें"), - ("Deny LAN discovery", "LAN खोज अस्वीकार करें"), - ("Write a message", "संदेश लिखें"), - ("Prompt", "प्रॉम्प्ट"), - ("Please wait for confirmation of UAC...", "कृपया UAC की पुष्टि की प्रतीक्षा करें..."), - ("elevated_foreground_window_tip", "रिमोट डेस्कटॉप की वर्तमान विंडो को उच्च अनुमतियों की आवश्यकता है।"), - ("Disconnected", "डिस्कनेक्ट हो गया"), - ("Other", "अन्य"), - ("Confirm before closing multiple tabs", "एकाधिक टैब बंद करने से पहले पुष्टि करें"), - ("Keyboard Settings", "कीबोर्ड सेटिंग्स"), - ("Full Access", "पूर्ण पहुंच (Full Access)"), - ("Screen Share", "स्क्रीन शेयर"), - ("ubuntu-21-04-required", "Ubuntu 21.04 या उच्चतर आवश्यक है"), - ("wayland-requires-higher-linux-version", "Wayland के लिए उच्च Linux संस्करण आवश्यक है"), - ("xdp-portal-unavailable", "XDP पोर्टल अनुपलब्ध है"), - ("JumpLink", "JumpLink"), - ("Please Select the screen to be shared(Operate on the peer side).", "कृपया साझा की जाने वाली स्क्रीन चुनें (दूसरे सिस्टम पर संचालित करें)।"), - ("Show RustDesk", "RustDesk दिखाएं"), - ("This PC", "यह PC"), - ("or", "या"), - ("Elevate", "एलीवेट (Elevate) करें"), - ("Zoom cursor", "ज़ूम कर्सर"), - ("Accept sessions via password", "पासवर्ड के माध्यम से सत्र स्वीकार करें"), - ("Accept sessions via click", "क्लिक के माध्यम से सत्र स्वीकार करें"), - ("Accept sessions via both", "दोनों के माध्यम से सत्र स्वीकार करें"), - ("Please wait for the remote side to accept your session request...", "कृपया रिमोट साइड द्वारा आपके सत्र अनुरोध को स्वीकार करने की प्रतीक्षा करें..."), - ("One-time Password", "वन-टाइम पासवर्ड"), - ("Use one-time password", "वन-टाइम पासवर्ड का उपयोग करें"), - ("One-time password length", "वन-टाइम पासवर्ड की लंबाई"), - ("Request access to your device", "आपके डिवाइस तक पहुंच का अनुरोध"), - ("Hide connection management window", "कनेक्शन प्रबंधन विंडो छुपाएं"), - ("hide_cm_tip", "केवल तभी छुपाएं जब पासवर्ड से कनेक्शन की अनुमति हो"), - ("wayland_experiment_tip", "Wayland समर्थन अभी परीक्षण मोड में है"), - ("Right click to select tabs", "टैब चुनने के लिए राइट क्लिक करें"), - ("Skipped", "छोड़ दिया गया"), - ("Add to address book", "पता पुस्तिका में जोड़ें"), - ("Group", "समूह"), - ("Search", "खोजें"), - ("Closed manually by web console", "वेब कंसोल द्वारा मैन्युअल रूप से बंद किया गया"), - ("Local keyboard type", "स्थानीय कीबोर्ड प्रकार"), - ("Select local keyboard type", "स्थानीय कीबोर्ड प्रकार चुनें"), - ("software_render_tip", "यदि आपकी स्क्रीन काली है, तो इसे आज़माएं"), - ("Always use software rendering", "हमेशा सॉफ़्टवेयर रेंडरिंग का उपयोग करें"), - ("config_input", "इनपुट कॉन्फ़िगर करें"), - ("config_microphone", "माइक्रोफ़ोन कॉन्फ़िगर करें"), - ("request_elevation_tip", "रिमोट साइड से उच्च अनुमतियों का अनुरोध करें"), - ("Wait", "प्रतीक्षा करें"), - ("Elevation Error", "एलीवेशन (Elevation) त्रुटि"), - ("Ask the remote user for authentication", "रिमोट उपयोगकर्ता से प्रमाणीकरण मांगें"), - ("Choose this if the remote account is administrator", "यदि रिमोट खाता व्यवस्थापक (Admin) है तो इसे चुनें"), - ("Transmit the username and password of administrator", "व्यवस्थापक का उपयोगकर्ता नाम और पासवर्ड भेजें"), - ("still_click_uac_tip", "रिमोट उपयोगकर्ता को अभी भी UAC विंडो पर 'हाँ' क्लिक करना होगा।"), - ("Request Elevation", "एलीवेशन का अनुरोध करें"), - ("wait_accept_uac_tip", "कृपया रिमोट उपयोगकर्ता द्वारा UAC स्वीकार करने की प्रतीक्षा करें।"), - ("Elevate successfully", "सफलतापूर्वक एलीवेट किया गया"), - ("uppercase", "बड़े अक्षर (Uppercase)"), - ("lowercase", "छोटे अक्षर (Lowercase)"), - ("digit", "अंक (Digit)"), - ("special character", "विशेष वर्ण"), - ("length>=8", "लंबाई >= 8"), - ("Weak", "कमजोर"), - ("Medium", "मध्यम"), - ("Strong", "मजबूत"), - ("Switch Sides", "साइड्स बदलें"), - ("Please confirm if you want to share your desktop?", "कृपया पुष्टि करें कि क्या आप अपना डेस्कटॉप साझा करना चाहते हैं?"), - ("Display", "डिस्प्ले"), - ("Default View Style", "डिफ़ॉल्ट व्यू शैली"), - ("Default Scroll Style", "डिफ़ॉल्ट स्क्रॉल शैली"), - ("Default Image Quality", "डिफ़ॉल्ट इमेज गुणवत्ता"), - ("Default Codec", "डिफ़ॉल्ट कोडेक"), - ("Bitrate", "बिटरेट"), - ("FPS", "FPS"), - ("Auto", "ऑटो"), - ("Other Default Options", "अन्य डिफ़ॉल्ट विकल्प"), - ("Voice call", "वॉयस कॉल"), - ("Text chat", "टेक्स्ट चैट"), - ("Stop voice call", "वॉयस कॉल बंद करें"), - ("relay_hint_tip", "सीधा कनेक्शन संभव नहीं हो सकता; आप रिले के माध्यम से जुड़ने का प्रयास कर सकते हैं।"), - ("Reconnect", "पुनः कनेक्ट करें"), - ("Codec", "कोडेक"), - ("Resolution", "रिज़ॉल्यूशन"), - ("No transfers in progress", "कोई स्थानांतरण जारी नहीं है"), - ("Set one-time password length", "वन-टाइम पासवर्ड की लंबाई सेट करें"), - ("RDP Settings", "RDP सेटिंग्स"), - ("Sort by", "इसके अनुसार क्रमबद्ध करें"), - ("New Connection", "नया कनेक्शन"), - ("Restore", "पुनर्स्थापित करें"), - ("Minimize", "मिनिमाइज करें"), - ("Maximize", "मैक्सिमाइज करें"), - ("Your Device", "आपका डिवाइस"), - ("empty_recent_tip", "हाल के सत्र यहाँ दिखाई देंगे।"), - ("empty_favorite_tip", "पसंदीदा डिवाइस यहाँ दिखाई देंगे।"), - ("empty_lan_tip", "खोजे गए डिवाइस यहाँ दिखाई देंगे।"), - ("empty_address_book_tip", "आपके पता पुस्तिका में वर्तमान में कोई डिवाइस नहीं है।"), - ("Empty Username", "खाली उपयोगकर्ता नाम"), - ("Empty Password", "खाली पासवर्ड"), - ("Me", "मैं"), - ("identical_file_tip", "यह फ़ाइल पहले से ही मौजूद है।"), - ("show_monitors_tip", "टूलबार में मॉनिटर दिखाएं"), - ("View Mode", "व्यू मोड"), - ("login_linux_tip", "रिमोट Linux सत्र शुरू करने के लिए आपको लॉगिन करना होगा"), - ("verify_rustdesk_password_tip", "RustDesk पासवर्ड सत्यापित करें"), - ("remember_account_tip", "इस खाते को याद रखें"), - ("os_account_desk_tip", "रिमोट डेस्कटॉप को एक्सेस करने के लिए OS खाते का उपयोग करें"), - ("OS Account", "OS खाता"), - ("another_user_login_title_tip", "एक अन्य उपयोगकर्ता पहले से ही लॉगिन है"), - ("another_user_login_text_tip", "डिस्कनेक्ट करें और पुनः प्रयास करें"), - ("xorg_not_found_title_tip", "Xorg नहीं मिला"), - ("xorg_not_found_text_tip", "कृपया Xorg इंस्टॉल करें"), - ("no_desktop_title_tip", "कोई डेस्कटॉप उपलब्ध नहीं है"), - ("no_desktop_text_tip", "कृपया Linux डेस्कटॉप इंस्टॉल करें"), - ("No need to elevate", "एलीवेट करने की आवश्यकता नहीं है"), - ("System Sound", "सिस्टम साउंड"), - ("Default", "डिफ़ॉल्ट"), - ("New RDP", "नया RDP"), - ("Fingerprint", "फिंगरप्रिंट"), - ("Copy Fingerprint", "फिंगरप्रिंट कॉपी करें"), - ("no fingerprints", "कोई फिंगरप्रिंट नहीं"), - ("Select a peer", "एक पीयर (Peer) चुनें"), - ("Select peers", "पीयर्स चुनें"), - ("Plugins", "प्लगइन्स"), - ("Uninstall", "अनइंस्टॉल करें"), - ("Update", "अपडेट करें"), - ("Enable", "सक्षम करें"), - ("Disable", "अक्षम करें"), - ("Options", "विकल्प"), - ("resolution_original_tip", "मूल रिज़ॉल्यूशन"), - ("resolution_fit_local_tip", "स्थानीय स्क्रीन में फिट करें"), - ("resolution_custom_tip", "कस्टम रिज़ॉल्यूशन"), - ("Collapse toolbar", "टूलबार समेटें"), - ("Accept and Elevate", "स्वीकार करें और एलीवेट करें"), - ("accept_and_elevate_btn_tooltip", "कनेक्शन स्वीकार करें और UAC अनुमतियाँ मांगें।"), - ("clipboard_wait_response_timeout_tip", "क्लिपबोर्ड प्रतिक्रिया के लिए समय समाप्त हो गया।"), - ("Incoming connection", "आने वाला कनेक्शन"), - ("Outgoing connection", "जाने वाला कनेक्शन"), - ("Exit", "बाहर निकलें"), - ("Open", "खोलें"), - ("logout_tip", "क्या आप वाकई लॉगआउट करना चाहते हैं?"), - ("Service", "सेवा"), - ("Start", "शुरू करें"), - ("Stop", "रोकें"), - ("exceed_max_devices", "आप डिवाइस की अधिकतम सीमा को पार कर चुके हैं।"), - ("Sync with recent sessions", "हाल के सत्रों के साथ सिंक करें"), - ("Sort tags", "टैग क्रमबद्ध करें"), - ("Open connection in new tab", "नये टैब में कनेक्शन खोलें"), - ("Move tab to new window", "टैब को नयी विंडो में ले जाएं"), - ("Can not be empty", "खाली नहीं हो सकता"), - ("Already exists", "पहले से मौजूद है"), - ("Change Password", "पासवर्ड बदलें"), - ("Refresh Password", "पासवर्ड रिफ्रेश करें"), - ("ID", "ID"), - ("Grid View", "ग्रिड व्यू"), - ("List View", "लिस्ट व्यू"), - ("Select", "चुनें"), - ("Toggle Tags", "टैग टॉगल करें"), - ("pull_ab_failed_tip", "पता पुस्तिका अपडेट करने में विफल।"), - ("push_ab_failed_tip", "सर्वर पर पता पुस्तिका सिंक करने में विफल।"), - ("synced_peer_readded_tip", "हाल के सत्रों में मौजूद डिवाइस पता पुस्तिका में सिंक किए गए थे।"), - ("Change Color", "रंग बदलें"), - ("Primary Color", "प्राथमिक रंग"), - ("HSV Color", "HSV रंग"), - ("Installation Successful!", "इंस्टॉलेशन सफल रहा!"), - ("Installation failed!", "इंस्टॉलेशन विफल रहा!"), - ("Reverse mouse wheel", "माउस व्हील उल्टा करें"), - ("{} sessions", "{} सत्र"), - ("scam_title", "धोखाधड़ी की चेतावनी!"), - ("scam_text1", "यदि आप किसी ऐसे व्यक्ति से बात कर रहे हैं जिसे आप नहीं जानते और जिसने आपसे RustDesk उपयोग करने को कहा है, तो तुरंत डिस्कनेक्ट कर दें।"), - ("scam_text2", "यह एक घोटाला हो सकता है। अपना आईडी या पासवर्ड किसी को न दें।"), - ("Don't show again", "दोबारा न दिखाएं"), - ("I Agree", "मैं सहमत हूँ"), - ("Decline", "अस्वीकार करें"), - ("Timeout in minutes", "मिनटों में टाइमआउट"), - ("auto_disconnect_option_tip", "निष्क्रियता पर स्वचालित रूप से डिस्कनेक्ट करें"), - ("Connection failed due to inactivity", "निष्क्रियता के कारण कनेक्शन विफल रहा"), - ("Check for software update on startup", "स्टार्टअप पर सॉफ़्टवेयर अपडेट की जांच करें"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk सर्वर प्रो को संस्करण {} में अपग्रेड करें"), - ("pull_group_failed_tip", "समूह खींचने (Pull) में विफल"), - ("Filter by intersection", "इंटरसेक्शन द्वारा फ़िल्टर करें"), - ("Remove wallpaper during incoming sessions", "आने वाले सत्रों के दौरान वॉलपेपर हटा दें"), - ("Test", "परीक्षण"), - ("display_is_plugged_out_msg", "डिस्प्ले हटा दिया गया है।"), - ("No displays", "कोई डिस्प्ले नहीं"), - ("Open in new window", "नयी विंडो में खोलें"), - ("Show displays as individual windows", "डिस्प्ले को व्यक्तिगत विंडो के रूप में दिखाएं"), - ("Use all my displays for the remote session", "रिमोट सत्र के लिए मेरे सभी डिस्प्ले का उपयोग करें"), - ("selinux_tip", "डिवाइस पर SELinux सक्षम है।"), - ("Change view", "व्यू बदलें"), - ("Big tiles", "बड़ी टाइलें"), - ("Small tiles", "छोटी टाइलें"), - ("List", "लिस्ट"), - ("Virtual display", "वर्चुअल डिस्प्ले"), - ("Plug out all", "सभी अनप्लग करें"), - ("True color (4:4:4)", "सच्चा रंग (4:4:4)"), - ("Enable blocking user input", "उपयोगकर्ता इनपुट को ब्लॉक करना सक्षम करें"), - ("id_input_tip", "आप ID, उपनाम (Alias) या IP पता दर्ज कर सकते हैं।"), - ("privacy_mode_impl_mag_tip", "मैग्निफायर (Magnifier) गोपनीयता मोड"), - ("privacy_mode_impl_virtual_display_tip", "वर्चुअल डिस्प्ले गोपनीयता मोड"), - ("Enter privacy mode", "गोपनीयता मोड में प्रवेश करें"), - ("Exit privacy mode", "गोपनीयता मोड से बाहर निकलें"), - ("idd_not_support_under_win10_2004_tip", "वर्चुअल डिस्प्ले Windows 10 संस्करण 2004 या उच्चतर पर समर्थित है।"), - ("input_source_1_tip", "इनपुट स्रोत 1"), - ("input_source_2_tip", "इनपुट स्रोत 2"), - ("Swap control-command key", "Control और Command कुंजियों को बदलें"), - ("swap-left-right-mouse", "बाएं और दाएं माउस बटन को बदलें"), - ("2FA code", "2FA कोड"), - ("More", "अधिक"), - ("enable-2fa-title", "द्वि-कारक प्रमाणीकरण (2FA) सक्षम करें"), - ("enable-2fa-desc", "कृपया अपना ऑथेंटिकेटर ऐप सेट करें।"), - ("wrong-2fa-code", "गलत 2FA कोड।"), - ("enter-2fa-title", "2FA कोड दर्ज करें"), - ("Email verification code must be 6 characters.", "ईमेल सत्यापन कोड 6 अक्षरों का होना चाहिए।"), - ("2FA code must be 6 digits.", "2FA कोड 6 अंकों का होना चाहिए।"), - ("Multiple Windows sessions found", "एकाधिक Windows सत्र मिले"), - ("Please select the session you want to connect to", "कृपया वह सत्र चुनें जिससे आप जुड़ना चाहते हैं"), - ("powered_by_me", "मेरे द्वारा संचालित"), - ("outgoing_only_desk_tip", "यह केवल आउटगोइंग मोड है"), - ("preset_password_warning", "सुरक्षा के लिए, कृपया डिफ़ॉल्ट पासवर्ड बदलें।"), - ("Security Alert", "सुरक्षा चेतावनी"), - ("My address book", "मेरी पता पुस्तिका"), - ("Personal", "व्यक्तिगत"), - ("Owner", "स्वामी"), - ("Set shared password", "साझा पासवर्ड सेट करें"), - ("Exist in", "इसमें मौजूद है"), - ("Read-only", "केवल पढ़ने के लिए"), - ("Read/Write", "पढ़ना/लिखना"), - ("Full Control", "पूर्ण नियंत्रण"), - ("share_warning_tip", "सावधानी: आप अपना एक्सेस साझा कर रहे हैं।"), - ("Everyone", "हर कोई"), - ("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", "रिमोट विंडो फोकस का पालन करें"), - ("default_proxy_tip", "डिफ़ॉल्ट प्रॉक्सी सेटिंग"), - ("no_audio_input_device_tip", "कोई ऑडियो इनपुट डिवाइस नहीं मिला।"), - ("Incoming", "आने वाली"), - ("Outgoing", "जाने वाली"), - ("Clear Wayland screen selection", "Wayland स्क्रीन चयन साफ़ करें"), - ("clear_Wayland_screen_selection_tip", "Wayland के स्क्रीन चयन को रीसेट करें।"), - ("confirm_clear_Wayland_screen_selection_tip", "क्या आप वाकई स्क्रीन चयन साफ़ करना चाहते हैं?"), - ("android_new_voice_call_tip", "नया वॉयस कॉल अनुरोध"), - ("texture_render_tip", "टेक्सचर रेंडरिंग का उपयोग करें"), - ("Use texture rendering", "टेक्सचर रेंडरिंग का उपयोग करें"), - ("Floating window", "फ्लोटिंग विंडो"), - ("floating_window_tip", "बैकग्राउंड में रहने के दौरान RustDesk को दिखाएं"), - ("Keep screen on", "स्क्रीन चालू रखें"), - ("Never", "कभी नहीं"), - ("During controlled", "नियंत्रण के दौरान"), - ("During service is on", "जब सेवा चालू हो"), - ("Capture screen using DirectX", "DirectX का उपयोग करके स्क्रीन कैप्चर करें"), - ("Back", "पीछे"), - ("Apps", "ऐप्स"), - ("Volume up", "आवाज़ बढ़ाएं"), - ("Volume down", "आवाज़ कम करें"), - ("Power", "पावर"), - ("Telegram bot", "Telegram बॉट"), - ("enable-bot-tip", "सूचनाओं के लिए बोट सक्षम करें"), - ("enable-bot-desc", "निर्देशों के लिए हमारे टेलीग्राम बोट को देखें।"), - ("cancel-2fa-confirm-tip", "क्या आप वाकई 2FA रद्द करना चाहते हैं?"), - ("cancel-bot-confirm-tip", "क्या आप वाकई बोट रद्द करना चाहते हैं?"), - ("About RustDesk", "RustDesk के बारे में"), - ("Send clipboard keystrokes", "क्लिपबोर्ड कीस्ट्रोक्स भेजें"), - ("network_error_tip", "नेटवर्क कनेक्शन त्रुटि, कृपया पुनः प्रयास करें।"), - ("Unlock with PIN", "PIN से अनलॉक करें"), - ("Requires at least {} characters", "कम से कम {} अक्षरों की आवश्यकता है"), - ("Wrong PIN", "गलत PIN"), - ("Set PIN", "PIN सेट करें"), - ("Enable trusted devices", "विश्वसनीय डिवाइस सक्षम करें"), - ("Manage trusted devices", "विश्वसनीय डिवाइस प्रबंधित करें"), - ("Platform", "प्लेटफ़ॉर्म"), - ("Days remaining", "शेष दिन"), - ("enable-trusted-devices-tip", "केवल विश्वसनीय डिवाइस ही पासवर्ड के बिना जुड़ सकते हैं"), - ("Parent directory", "पैरेंट निर्देशिका"), - ("Resume", "फिर से शुरू करें"), - ("Invalid file name", "अमान्य फ़ाइल नाम"), - ("one-way-file-transfer-tip", "केवल एकतरफा फ़ाइल स्थानांतरण की अनुमति है"), - ("Authentication Required", "प्रमाणीकरण आवश्यक"), - ("Authenticate", "प्रमाणित करें"), - ("web_id_input_tip", "रिमोट आईडी दर्ज करें"), - ("Download", "डाउनलोड करें"), - ("Upload folder", "फ़ोल्डर अपलोड करें"), - ("Upload files", "फाइलें अपलोड करें"), - ("Clipboard is synchronized", "क्लिपबोर्ड सिंक हो गया है"), - ("Update client clipboard", "क्लाइंट क्लिपबोर्ड अपडेट करें"), - ("Untagged", "बिना टैग वाला"), - ("new-version-of-{}-tip", "{} का नया संस्करण उपलब्ध है"), - ("Accessible devices", "सुलभ डिवाइस"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), - ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), - ("Use D3D rendering", ""), - ("Printer", "प्रिंटर"), - ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), - ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), - ("printer-{}-not-installed-tip", "प्रिंटर {} इंस्टॉल नहीं है।"), - ("printer-{}-ready-tip", "प्रिंटर {} तैयार है।"), - ("Install {} Printer", "{} प्रिंटर इंस्टॉल करें"), - ("Outgoing Print Jobs", "आउटगोइंग प्रिंट कार्य"), - ("Incoming Print Jobs", "इनकमिंग प्रिंट कार्य"), - ("Incoming Print Job", "इनकमिंग प्रिंट कार्य"), - ("use-the-default-printer-tip", "डिफ़ॉल्ट प्रिंटर का उपयोग करें"), - ("use-the-selected-printer-tip", "चयनित प्रिंटर का उपयोग करें"), - ("auto-print-tip", "स्वचालित रूप से प्रिंट करें"), - ("print-incoming-job-confirm-tip", "प्रिंट कार्य स्वीकार करने से पहले पुष्टि करें"), - ("remote-printing-disallowed-tile-tip", "रिमोट प्रिंटिंग की अनुमति नहीं है"), - ("remote-printing-disallowed-text-tip", "कृपया सेटिंग्स में रिमोट प्रिंटिंग सक्षम करें।"), - ("save-settings-tip", "सेटिंग्स सुरक्षित करें"), - ("dont-show-again-tip", "दोबारा न दिखाएं"), - ("Take screenshot", "स्क्रीनशॉट लें"), - ("Taking screenshot", "स्क्रीनशॉट लिया जा रहा है"), - ("screenshot-merged-screen-not-supported-tip", "मर्ज की गई स्क्रीन के स्क्रीनशॉट समर्थित नहीं हैं।"), - ("screenshot-action-tip", "स्क्रीनशॉट लेने के बाद की कार्रवाई"), - ("Save as", "इस रूप में सहेजें"), - ("Copy to clipboard", "क्लिपबोर्ड पर कॉपी करें"), - ("Enable remote printer", "रिमोट प्रिंटर सक्षम करें"), - ("Downloading {}", "{} डाउनलोड हो रहा है"), - ("{} Update", "{} अपडेट"), - ("{}-to-update-tip", "अपडेट करने के लिए {}"), - ("download-new-version-failed-tip", "नया संस्करण डाउनलोड करने में विफल।"), - ("Auto update", "ऑटो अपडेट"), - ("update-failed-check-msi-tip", "अपडेट विफल, कृपया MSI फ़ाइल की जांच करें।"), - ("websocket_tip", "यदि पोर्ट ब्लॉक हैं, तो WebSocket का उपयोग करें।"), - ("Use WebSocket", "WebSocket का उपयोग करें"), - ("Trackpad speed", "ट्रैकपैड गति"), - ("Default trackpad speed", "डिफ़ॉल्ट ट्रैकपैड गति"), - ("Numeric one-time password", "संख्यात्मक वन-टाइम पासवर्ड"), - ("Enable IPv6 P2P connection", "IPv6 P2P कनेक्शन सक्षम करें"), - ("Enable UDP hole punching", "UDP होल पंचिंग सक्षम करें"), - ("View camera", "कैमरा देखें"), - ("Enable camera", "कैमरा सक्षम करें"), - ("No cameras", "कोई कैमरा नहीं मिला"), - ("view_camera_unsupported_tip", "रिमोट कैमरा समर्थित नहीं है।"), - ("Terminal", "टर्मिनल"), - ("Enable terminal", "टर्मिनल सक्षम करें"), - ("New tab", "नया टैब"), - ("Keep terminal sessions on disconnect", "डिस्कनेक्ट होने पर टर्मिनल सत्र चालू रखें"), - ("Terminal (Run as administrator)", "टर्मिनल (प्रशासक के रूप में चलाएं)"), - ("terminal-admin-login-tip", "प्रशासक लॉगिन आवश्यक है।"), - ("Failed to get user token.", "उपयोगकर्ता टोकन प्राप्त करने में विफल।"), - ("Incorrect username or password.", "गलत उपयोगकर्ता नाम या पासवर्ड।"), - ("The user is not an administrator.", "उपयोगकर्ता प्रशासक नहीं है।"), - ("Failed to check if the user is an administrator.", "जांचने में विफल कि क्या उपयोगकर्ता व्यवस्थापक है।"), - ("Supported only in the installed version.", "केवल इंस्टॉल किए गए संस्करण में समर्थित।"), - ("elevation_username_tip", "प्रशासक उपयोगकर्ता नाम दर्ज करें"), - ("Preparing for installation ...", "स्थापना की तैयारी..."), - ("Show my cursor", "मेरा कर्सर दिखाएं"), - ("Scale custom", "कस्टम पैमाना"), - ("Custom scale slider", "कस्टम स्केल स्लाइडर"), - ("Decrease", "घटाएं"), - ("Increase", "बढ़ाएं"), - ("Show virtual mouse", "वर्चुअल माउस दिखाएं"), - ("Virtual mouse size", "वर्चुअल माउस का आकार"), - ("Small", "छोटा"), - ("Large", "बड़ा"), - ("Show virtual joystick", "वर्चुअल जॉयस्टिक दिखाएं"), - ("Edit note", "नोट संपादित करें"), - ("Alias", "उपनाम (Alias)"), - ("ScrollEdge", "किनारे से स्क्रॉल"), - ("Allow insecure TLS fallback", "असुरक्षित TLS फ़ालबैक की अनुमति दें"), - ("allow-insecure-tls-fallback-tip", "पुराने सर्वर कनेक्शन के लिए उपयोग करें।"), - ("Disable UDP", "UDP अक्षम करें"), - ("disable-udp-tip", "कनेक्शन समस्याओं के लिए UDP बंद करें।"), - ("server-oss-not-support-tip", "OSS सर्वर इसका समर्थन नहीं करता।"), - ("input note here", "यहाँ नोट दर्ज करें"), - ("note-at-conn-end-tip", "कनेक्शन के अंत में नोट दिखाएं"), - ("Show terminal extra keys", "टर्मिनल की अतिरिक्त कुंजियाँ दिखाएं"), - ("Relative mouse mode", "सापेक्ष (Relative) माउस मोड"), - ("rel-mouse-not-supported-peer-tip", "रिमोट साइड पर समर्थित नहीं है।"), - ("rel-mouse-not-ready-tip", "तैयार नहीं है।"), - ("rel-mouse-lock-failed-tip", "माउस लॉक विफल।"), - ("rel-mouse-exit-{}-tip", "बाहर निकलने के लिए {} दबाएं"), - ("rel-mouse-permission-lost-tip", "अनुमति खो गई।"), - ("Changelog", "परिवर्तन सूची (Changelog)"), - ("keep-awake-during-outgoing-sessions-label", "आउटगोइंग सत्र के दौरान जागते रहें"), - ("keep-awake-during-incoming-sessions-label", "इनकमिंग सत्र के दौरान जागते रहें"), - ("Continue with {}", "{} के साथ जारी रखें"), - ("Display Name", "प्रदर्शित नाम"), - ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), - ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), - ].iter().cloned().collect(); -} diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 0593ff6b7..d00fc56b9 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Postavke tipkovnice"), ("Full Access", "Potpuni pristup"), ("Screen Share", "Dijeljenje zaslona"), - ("ubuntu-21-04-required", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"), - ("wayland-requires-higher-linux-version", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."), ("JumpLink", "Vidi"), ("Please Select the screen to be shared(Operate on the peer side).", "Molimo odaberite zaslon koji će biti podijeljen (Za rad na strani klijenta)"), ("Show RustDesk", "Prikaži RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3eb16890f..85153f618 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -57,7 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID-kiszolgáló"), ("Relay Server", "Továbbító-kiszolgáló"), ("API Server", "API-kiszolgáló"), - ("invalid_http", "A címnek mindenképpen http(s)://-rel kell kezdődnie."), + ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), ("Invalid IP", "A megadott IP-cím érvénytelen"), ("Invalid format", "Érvénytelen formátum"), ("server_not_support", "A kiszolgáló nem támogatja"), @@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), ("Configure", "Beállítás"), ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."), - ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."), ("Installing ...", "Telepítés ..."), ("Install", "Telepítse"), ("Installation", "Telepítés"), @@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), - ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."), - ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a „RustDesk Input” szolgáltatást."), + ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."), + ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."), ("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"), ("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."), ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."), ("Account", "Fiók"), ("Overwrite", "Felülírás"), @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Billentyűzetbeállítások"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), - ("ubuntu-21-04-required", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), - ("wayland-requires-higher-linux-version", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."), ("JumpLink", "Hiperhivatkozás"), ("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyőt."), ("Show RustDesk", "A RustDesk megjelenítése"), @@ -408,15 +407,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."), ("Always use software rendering", "Mindig szoftveres leképezést használjon"), - ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."), - ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."), + ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."), ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), ("Wait", "Várjon"), ("Elevation Error", "Emelt szintű hozzáférési hiba"), ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), ("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"), ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), - ("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), ("Request Elevation", "Emelt szintű jogok igénylése"), ("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), ("Elevate successfully", "Emelt szintű jogok megadva"), @@ -442,7 +441,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Hanghívás"), ("Text chat", "Szöveges csevegés"), ("Stop voice call", "Hanghívás leállítása"), - ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), ("Reconnect", "Újrakapcsolódás"), ("Codec", "Kodek"), ("Resolution", "Felbontás"), @@ -559,7 +558,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Kapcsolja ki az összeset"), ("True color (4:4:4)", "Valódi szín (4:4:4)"), ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), - ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "1. mód"), ("privacy_mode_impl_virtual_display_tip", "2. mód"), ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), @@ -622,7 +621,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Főkapcsoló"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), - ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"), ("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"), ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), ("About RustDesk", "A RustDesk névjegye"), @@ -643,7 +642,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), ("Authentication Required", "Hitelesítés szükséges"), ("Authenticate", "Hitelesítés"), - ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg az „@public” kulcsot. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), + ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), ("Download", "Letöltés"), ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), @@ -682,9 +681,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Downloading {}", "{} letöltése"), ("{} Update", "{} frissítés"), ("{}-to-update-tip", "{} bezárása és az új verzió telepítése."), - ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), + ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), ("Auto update", "Automatikus frissítés"), - ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), + ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), ("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."), ("Use WebSocket", "WebSocket használata"), ("Trackpad speed", "Érintőpad sebessége"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), ("Continue with {}", "Folytatás ezzel: {}"), ("Display Name", "Kijelző név"), - ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), - ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), - ("Enable privacy mode", "Adatvédelmi mód aktiválása"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index bcda0a3a8..f898c8bc4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Pengaturan Papan Ketik"), ("Full Access", "Akses penuh"), ("Screen Share", "Berbagi Layar"), - ("ubuntu-21-04-required", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), - ("wayland-requires-higher-linux-version", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), ("JumpLink", "Tautan Cepat"), ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan kepada rekan anda."), ("Show RustDesk", "Tampilkan RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Lanjutkan dengan {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a5132e027..aac87109d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Impostazioni tastiera"), ("Full Access", "Accesso completo"), ("Screen Share", "Condivisione schermo"), - ("ubuntu-21-04-required", "Wayland richiede Ubuntu 21.04 o versione successiva."), - ("wayland-requires-higher-linux-version", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), - ("xdp-portal-unavailable", "Acquisizione dello schermo di Wayland non riuscita. Il portale desktop XDG potrebbe essersi bloccato o non essere disponibile. Prova a riavviarlo con `systemctl --user restart xdg-desktop-portal`."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o versione successiva."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), ("JumpLink", "Vai a"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato dispositivo remoto)."), ("Show RustDesk", "Visualizza RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ("Continue with {}", "Continua con {}"), ("Display Name", "Visualizza nome"), - ("password-hidden-tip", "È impostata una password permanente (nascosta)."), - ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), - ("Enable privacy mode", "Abilita modalità privacy"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2879e86bf..e033de3b3 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "キーボードの設定"), ("Full Access", "フルアクセス"), ("Screen Share", "画面共有"), - ("ubuntu-21-04-required", "Wayland を使用するには、Ubuntu 21.04 以降のバージョンが必要です。"), - ("wayland-requires-higher-linux-version", "Wayland を使用するには、より新しい Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland を使用するには、Ubuntu 21.04 以降のバージョンが必要です。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland を使用するには、より新しい Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"), ("JumpLink", "表示"), ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(リモートコンピューターが操作します)"), ("Show RustDesk", "RustDesk を表示"), @@ -661,9 +660,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-not-installed-tip", "{} のプリンターがインストールされていません。"), ("printer-{}-ready-tip", "{} のプリンターがインストールされ、使用可能になっています。"), ("Install {} Printer", " {} のプリンターをインストール"), - ("Outgoing Print Jobs", "印刷ジョブの送信"), - ("Incoming Print Jobs", "印刷ジョブの受信"), - ("Incoming Print Job", "印刷ジョブの受信"), + ("Outgoing Print Jobs", "送信印刷ジョブ"), + ("Incoming Print Jobs", "受信印刷ジョブ"), + ("Incoming Print Job", "受信印刷ジョブ"), ("use-the-default-printer-tip", "既定のプリンターを使用する"), ("use-the-selected-printer-tip", "選択したプリンターを使用する"), ("auto-print-tip", "選択したプリンターを使用して自動的に印刷する"), @@ -710,7 +709,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"), ("Preparing for installation ...", "インストールの準備中です..."), ("Show my cursor", "自分のカーソルを表示する"), - ("Scale custom", "カスタムスケール"), + ("Scale custom", "カスタムスケーリング"), ("Custom scale slider", "カスタムスケールのスライダー"), ("Decrease", "縮小"), ("Increase", "拡大"), @@ -730,20 +729,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "ここにメモを入力"), ("note-at-conn-end-tip", "接続終了時にメモを要求する"), ("Show terminal extra keys", "ターミナルの追加キーを表示する"), - ("Relative mouse mode", "相対マウスモード"), - ("rel-mouse-not-supported-peer-tip", "接続先のデバイスは相対マウスモードに対応していません。"), - ("rel-mouse-not-ready-tip", "相対マウスモードはまだ準備できていません。再度お試しください。"), - ("rel-mouse-lock-failed-tip", "カーソルをロックできませんでした。相対マウスモードは無効化されています。"), - ("rel-mouse-exit-{}-tip", "「{}」を押して終了します。"), - ("rel-mouse-permission-lost-tip", "キーボード操作の権限が取り消されました。相対マウスモードは無効化されています。"), - ("Changelog", "更新履歴"), - ("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"), - ("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"), - ("Continue with {}", "{} で続行する"), - ("Display Name", "表示名"), - ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), - ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{} で続行"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 350d570b0..7230d1a1f 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "키보드 설정"), ("Full Access", "전체 액세스"), ("Screen Share", "화면 공유"), - ("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), - ("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), ("JumpLink", "점프 링크"), ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"), ("Show RustDesk", "RustDesk 표시"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("Continue with {}", "{}(으)로 계속"), ("Display Name", "표시 이름"), - ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), - ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), - ("Enable privacy mode", "개인정보 보호 모드 사용함"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 4476fadc7..c3715672d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", ""), ("Full Access", ""), ("Screen Share", ""), - ("ubuntu-21-04-required", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), - ("wayland-requires-higher-linux-version", "Wayland linux дистрибутивінің жоғарырақ нұсқасын қажет етеді. X11 жұмыс үстелін қолданып көріңіз немесе операциялық жүйеңізді өзгертіңіз."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland linux дистрибутивінің жоғарырақ нұсқасын қажет етеді. X11 жұмыс үстелін қолданып көріңіз немесе операциялық жүйеңізді өзгертіңіз."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), ("Show RustDesk", ""), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 47ace51ae..91c76291a 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Klaviatūros nustatymai"), ("Full Access", "Pilna prieiga"), ("Screen Share", "Ekrano bendrinimas"), - ("ubuntu-21-04-required", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."), - ("wayland-requires-higher-linux-version", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."), ("JumpLink", "Peržiūra"), ("Please Select the screen to be shared(Operate on the peer side).", "Prašome pasirinkti ekraną, kurį norite bendrinti (veikiantį kitoje pusėje)."), ("Show RustDesk", "Rodyti RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tęsti su {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 4f8e1f59f..0c8ba694e 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastatūras iestatījumi"), ("Full Access", "Pilna piekļuve"), ("Screen Share", "Ekrāna kopīgošana"), - ("ubuntu-21-04-required", "Wayland nepieciešama Ubuntu 21.04 vai jaunāka versija."), - ("wayland-requires-higher-linux-version", "Wayland nepieciešama augstāka Linux distro versija. Lūdzu, izmēģiniet X11 desktop vai mainiet savu OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nepieciešama Ubuntu 21.04 vai jaunāka versija."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nepieciešama augstāka Linux distro versija. Lūdzu, izmēģiniet X11 desktop vai mainiet savu OS."), ("JumpLink", "Skatīt"), ("Please Select the screen to be shared(Operate on the peer side).", "Lūdzu, atlasiet kopīgojamo ekrānu (darbojieties sesijas pusē)."), ("Show RustDesk", "Rādīt RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Turpināt ar {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs deleted file mode 100644 index 4dcfe9e74..000000000 --- a/src/lang/ml.rs +++ /dev/null @@ -1,749 +0,0 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "നില"), - ("Your Desktop", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ്"), - ("desk_tip", "ഈ ഐഡിയും പാസ്‌വേഡും ഉപയോഗിച്ച് നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് ആക്‌സസ് ചെയ്യാം."), - ("Password", "പാസ്‌വേഡ്"), - ("Ready", "തയ്യാറാണ്"), - ("Established", "ബന്ധം സ്ഥാപിച്ചു"), - ("connecting_status", "നെറ്റ്‌വർക്കുമായി ബന്ധിപ്പിക്കുന്നു..."), - ("Enable service", "സർവീസ് പ്രവർത്തനക്ഷമമാക്കുക"), - ("Start service", "സർവീസ് തുടങ്ങുക"), - ("Service is running", "സർവീസ് പ്രവർത്തിക്കുന്നു"), - ("Service is not running", "സർവീസ് പ്രവർത്തിക്കുന്നില്ല"), - ("not_ready_status", "തയ്യാറായിട്ടില്ല. ദയവായി നിങ്ങളുടെ കണക്ഷൻ പരിശോധിക്കുക"), - ("Control Remote Desktop", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് നിയന്ത്രിക്കുക"), - ("Transfer file", "ഫയൽ കൈമാറുക"), - ("Connect", "കണക്ട് ചെയ്യുക"), - ("Recent sessions", "സമീപകാല സെഷനുകൾ"), - ("Address book", "അഡ്രസ് ബുക്ക്"), - ("Confirmation", "സ്ഥിരീകരണം"), - ("TCP tunneling", "TCP ടണലിംഗ്"), - ("Remove", "നീക്കം ചെയ്യുക"), - ("Refresh random password", "പുതിയ പാസ്‌വേഡ് ജനറേറ്റ് ചെയ്യുക"), - ("Set your own password", "സ്വന്തം പാസ്‌വേഡ് സെറ്റ് ചെയ്യുക"), - ("Enable keyboard/mouse", "കീബോർഡ്/മൗസ് അനുവദിക്കുക"), - ("Enable clipboard", "ക്ലിപ്പ്ബോർഡ് അനുവദിക്കുക"), - ("Enable file transfer", "ഫയൽ കൈമാറ്റം അനുവദിക്കുക"), - ("Enable TCP tunneling", "TCP ടണലിംഗ് അനുവദിക്കുക"), - ("IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ്"), - ("ID/Relay Server", "ID/റിലേ സെർവർ"), - ("Import server config", "സെർവർ കോൺഫിഗറേഷൻ ഇമ്പോർട്ട് ചെയ്യുക"), - ("Export Server Config", "സെർവർ കോൺഫിഗറേഷൻ എക്‌സ്‌പോർട്ട് ചെയ്യുക"), - ("Import server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി ഇമ്പോർട്ട് ചെയ്തു"), - ("Export server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി എക്‌സ്‌പോർട്ട് ചെയ്തു"), - ("Invalid server configuration", "അസാധുവായ സെർവർ കോൺഫിഗറേഷൻ"), - ("Clipboard is empty", "ക്ലിപ്പ്ബോർഡ് ശൂന്യമാണ്"), - ("Stop service", "സർവീസ് നിർത്തുക"), - ("Change ID", "ഐഡി മാറ്റുക"), - ("Your new ID", "നിങ്ങളുടെ പുതിയ ഐഡി"), - ("length %min% to %max%", "നീളം %min% മുതൽ %max% വരെ"), - ("starts with a letter", "അക്ഷരത്തിൽ തുടങ്ങണം"), - ("allowed characters", "അനുവദനീയമായ അക്ഷരങ്ങൾ"), - ("id_change_tip", "ഐഡി മാറ്റിയാൽ നിലവിലുള്ള കണക്ഷൻ വിച്ഛേദിക്കപ്പെടും."), - ("Website", "വെബ്സൈറ്റ്"), - ("About", "വിവരങ്ങൾ"), - ("Slogan_tip", "മികച്ച അനുഭവത്തിനായി നിർമ്മിച്ച റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ"), - ("Privacy Statement", "സ്വകാര്യതാ പ്രസ്താവന"), - ("Mute", "നിശബ്ദമാക്കുക"), - ("Build Date", "നിർമ്മാണ തീയതി"), - ("Version", "പതിപ്പ്"), - ("Home", "ഹോം"), - ("Audio Input", "ഓഡിയോ ഇൻപുട്ട്"), - ("Enhancements", "മെച്ചപ്പെടുത്തലുകൾ"), - ("Hardware Codec", "ഹാർഡ്‌വെയർ കോഡെക്"), - ("Adaptive bitrate", "അഡാപ്റ്റീവ് ബിറ്റ്റേറ്റ്"), - ("ID Server", "ID സെർവർ"), - ("Relay Server", "റിലേ സെർവർ"), - ("API Server", "API സെർവർ"), - ("invalid_http", "അസാധുവായ HTTP ലിങ്ക്"), - ("Invalid IP", "അസാധുവായ IP"), - ("Invalid format", "അസാധുവായ ഫോർമാറ്റ്"), - ("server_not_support", "സെർവർ പിന്തുണയ്ക്കുന്നില്ല"), - ("Not available", "ലഭ്യമല്ല"), - ("Too frequent", "അമിതമായ തവണകൾ"), - ("Cancel", "റദ്ദാക്കുക"), - ("Skip", "ഒഴിവാക്കുക"), - ("Close", "അടയ്ക്കുക"), - ("Retry", "വീണ്ടും ശ്രമിക്കുക"), - ("OK", "ശരി"), - ("Password Required", "പാസ്‌വേഡ് ആവശ്യമാണ്"), - ("Please enter your password", "ദയവായി നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"), - ("Remember password", "പാസ്‌വേഡ് ഓർമ്മിക്കുക"), - ("Wrong Password", "തെറ്റായ പാസ്‌വേഡ്"), - ("Do you want to enter again?", "നിങ്ങൾക്ക് വീണ്ടും ശ്രമിക്കണോ?"), - ("Connection Error", "കണക്ഷൻ പിശക്"), - ("Error", "പിശക്"), - ("Reset by the peer", "മറുഭാഗത്തുനിന്ന് റീസെറ്റ് ചെയ്തു"), - ("Connecting...", "ബന്ധിപ്പിക്കുന്നു..."), - ("Connection in progress. Please wait.", "കണക്ഷൻ നടക്കുന്നു. ദയവായി കാത്തിരിക്കുക."), - ("Please try 1 minute later", "ദയവായി ഒരു മിനിറ്റിന് ശേഷം ശ്രമിക്കുക"), - ("Login Error", "ലോഗിൻ പിശക്"), - ("Successful", "വിജയിച്ചു"), - ("Connected, waiting for image...", "ബന്ധിപ്പിച്ചു, ചിത്രത്തിനായി കാത്തിരിക്കുന്നു..."), - ("Name", "പേര്"), - ("Type", "തരം"), - ("Modified", "മാറ്റം വരുത്തിയത്"), - ("Size", "വലിപ്പം"), - ("Show Hidden Files", "മറഞ്ഞിരിക്കുന്ന ഫയലുകൾ കാണിക്കുക"), - ("Receive", "സ്വീകരിക്കുക"), - ("Send", "അയക്കുക"), - ("Refresh File", "ഫയൽ പുതുക്കുക"), - ("Local", "ലോക്കൽ"), - ("Remote", "റിമോട്ട്"), - ("Remote Computer", "റിമോട്ട് കമ്പ്യൂട്ടർ"), - ("Local Computer", "ലോക്കൽ കമ്പ്യൂട്ടർ"), - ("Confirm Delete", "ഡിലീറ്റ് ചെയ്യുന്നത് സ്ഥിരീകരിക്കുക"), - ("Delete", "ഡിലീറ്റ് ചെയ്യുക"), - ("Properties", "പ്രോപ്പർട്ടീസ്"), - ("Multi Select", "ഒന്നിലധികം തിരഞ്ഞെടുക്കുക"), - ("Select All", "എല്ലാം തിരഞ്ഞെടുക്കുക"), - ("Unselect All", "തിരഞ്ഞെടുത്തവ ഒഴിവാക്കുക"), - ("Empty Directory", "ശൂന്യമായ ഡയറക്ടറി"), - ("Not an empty directory", "ഡയറക്ടറി ശൂന്യമല്ല"), - ("Are you sure you want to delete this file?", "ഈ ഫയൽ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), - ("Are you sure you want to delete this empty directory?", "ഈ ശൂന്യമായ ഡയറക്ടറി ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), - ("Are you sure you want to delete the file of this directory?", "ഈ ഡയറക്ടറിയിലെ ഫയലുകൾ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), - ("Do this for all conflicts", "എല്ലാ വൈരുദ്ധ്യങ്ങൾക്കും ഇതുതന്നെ ചെയ്യുക"), - ("This is irreversible!", "ഇത് പഴയപടിയാക്കാൻ കഴിയില്ല!"), - ("Deleting", "ഡിലീറ്റ് ചെയ്യുന്നു"), - ("files", "ഫയലുകൾ"), - ("Waiting", "കാത്തിരിക്കുന്നു"), - ("Finished", "പൂർത്തിയായി"), - ("Speed", "വേഗത"), - ("Custom Image Quality", "ഇമേജ് ക്വാളിറ്റി മാറ്റുക"), - ("Privacy mode", "സ്വകാര്യ മോഡ്"), - ("Block user input", "യൂസർ ഇൻപുട്ട് തടയുക"), - ("Unblock user input", "യൂസർ ഇൻപുട്ട് അനുവദിക്കുക"), - ("Adjust Window", "വിൻഡോ ക്രമീകരിക്കുക"), - ("Original", "ഒറിജിനൽ"), - ("Shrink", "ചുരുക്കുക"), - ("Stretch", "വലിപ്പിക്കുക"), - ("Scrollbar", "സ്ക്രോൾബാർ"), - ("ScrollAuto", "ഓട്ടോ സ്ക്രോൾ"), - ("Good image quality", "നല്ല ക്വാളിറ്റി"), - ("Balanced", "സന്തുലിതം"), - ("Optimize reaction time", "പ്രതികരണ സമയം മെച്ചപ്പെടുത്തുക"), - ("Custom", "കസ്റ്റം"), - ("Show remote cursor", "റിമോട്ട് കർസർ കാണിക്കുക"), - ("Show quality monitor", "ക്വാളിറ്റി മോണിറ്റർ കാണിക്കുക"), - ("Disable clipboard", "ക്ലിപ്പ്ബോർഡ് ഒഴിവാക്കുക"), - ("Lock after session end", "സെഷൻ കഴിഞ്ഞാൽ ലോക്ക് ചെയ്യുക"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del നൽകുക"), - ("Insert Lock", "ലോക്ക് ചെയ്യുക"), - ("Refresh", "പുതുക്കുക"), - ("ID does not exist", "ഐഡി നിലവിലില്ല"), - ("Failed to connect to rendezvous server", "സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Please try later", "ദയവായി പിന്നീട് ശ്രമിക്കുക"), - ("Remote desktop is offline", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് ഓഫ്‌ലൈനാണ്"), - ("Key mismatch", "കീ പൊരുത്തക്കേട്"), - ("Timeout", "സമയം കഴിഞ്ഞു"), - ("Failed to connect to relay server", "റിലേ സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Failed to connect via rendezvous server", "സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Failed to connect via relay server", "റിലേ സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Failed to make direct connection to remote desktop", "നേരിട്ട് ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Set Password", "പാസ്‌വേഡ് നൽകുക"), - ("OS Password", "OS പാസ്‌വേഡ്"), - ("install_tip", "മികച്ച പ്രകടനത്തിനായി ഇൻസ്റ്റാൾ ചെയ്യുക."), - ("Click to upgrade", "അപ്‌ഗ്രേഡ് ചെയ്യാൻ ക്ലിക്ക് ചെയ്യുക"), - ("Configure", "ക്രമീകരിക്കുക"), - ("config_acc", "അക്‌സസിബിലിറ്റി ക്രമീകരിക്കുക"), - ("config_screen", "സ്ക്രീൻ ക്രമീകരിക്കുക"), - ("Installing ...", "ഇൻസ്റ്റാൾ ചെയ്യുന്നു..."), - ("Install", "ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("Installation", "ഇൻസ്റ്റാളേഷൻ"), - ("Installation Path", "ഇൻസ്റ്റാളേഷൻ പാത്ത്"), - ("Create start menu shortcuts", "സ്റ്റാർട്ട് മെനുവിൽ ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"), - ("Create desktop icon", "ഡെസ്ക്ടോപ്പ് ഐക്കൺ ഉണ്ടാക്കുക"), - ("agreement_tip", "ഇൻസ്റ്റാൾ ചെയ്യുന്നതിലൂടെ നിങ്ങൾ കരാറുകൾ അംഗീകരിക്കുന്നു."), - ("Accept and Install", "അംഗീകരിച്ച് ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("End-user license agreement", "ലൈസൻസ് കരാർ"), - ("Generating ...", "ഉണ്ടാക്കുന്നു..."), - ("Your installation is lower version.", "നിങ്ങളുടെ ഇൻസ്റ്റാളേഷൻ പഴയ പതിപ്പാണ്."), - ("not_close_tcp_tip", "ടണൽ ഉപയോഗിക്കുമ്പോൾ ഈ വിൻഡോ അടയ്ക്കരുത്."), - ("Listening ...", "ശ്രദ്ധിക്കുന്നു..."), - ("Remote Host", "റിമോട്ട് ഹോസ്റ്റ്"), - ("Remote Port", "റിമോട്ട് പോർട്ട്"), - ("Action", "നടപടി"), - ("Add", "ചേർക്കുക"), - ("Local Port", "ലോക്കൽ പോർട്ട്"), - ("Local Address", "ലോക്കൽ അഡ്രസ്"), - ("Change Local Port", "ലോക്കൽ പോർട്ട് മാറ്റുക"), - ("setup_server_tip", "വേഗതയുള്ള കണക്ഷനായി സ്വന്തം സെർവർ സജ്ജമാക്കുക"), - ("Too short, at least 6 characters.", "വളരെ ചെറുതാണ്, കുറഞ്ഞത് 6 അക്ഷരങ്ങൾ വേണം."), - ("The confirmation is not identical.", "സ്ഥിരീകരണം ഒരേപോലെയല്ല."), - ("Permissions", "അനുമതികൾ"), - ("Accept", "സ്വീകരിക്കുക"), - ("Dismiss", "നിരസിക്കുക"), - ("Disconnect", "വിച്ഛേദിക്കുക"), - ("Enable file copy and paste", "ഫയൽ കോപ്പി-പേസ്റ്റ് അനുവദിക്കുക"), - ("Connected", "ബന്ധിപ്പിച്ചു"), - ("Direct and encrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്തതുമായ കണക്ഷൻ"), - ("Relayed and encrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്ത കണക്ഷൻ"), - ("Direct and unencrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്യാത്തതുമായ കണക്ഷൻ"), - ("Relayed and unencrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്യാത്ത കണക്ഷൻ"), - ("Enter Remote ID", "റിമോട്ട് ഐഡി നൽകുക"), - ("Enter your password", "നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"), - ("Logging in...", "ലോഗിൻ ചെയ്യുന്നു..."), - ("Enable RDP session sharing", "RDP സെഷൻ പങ്കിടൽ അനുവദിക്കുക"), - ("Auto Login", "ഓട്ടോ ലോഗിൻ"), - ("Enable direct IP access", "നേരിട്ടുള്ള IP ആക്‌സസ് അനുവദിക്കുക"), - ("Rename", "പേര് മാറ്റുക"), - ("Space", "സ്പേസ്"), - ("Create desktop shortcut", "ഡെസ്ക്ടോപ്പ് ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"), - ("Change Path", "പാത്ത് മാറ്റുക"), - ("Create Folder", "ഫോൾഡർ ഉണ്ടാക്കുക"), - ("Please enter the folder name", "ദയവായി ഫോൾഡറിന്റെ പേര് നൽകുക"), - ("Fix it", "പരിഹരിക്കുക"), - ("Warning", "മുന്നറിയിപ്പ്"), - ("Login screen using Wayland is not supported", "Wayland വഴിയുള്ള ലോഗിൻ സപ്പോർട്ട് ചെയ്യുന്നില്ല"), - ("Reboot required", "റീബൂട്ട് ആവശ്യമാണ്"), - ("Unsupported display server", "പിന്തുണയ്ക്കാത്ത ഡിസ്‌പ്ലേ സെർവർ"), - ("x11 expected", "x11 ആവശ്യമാണ്"), - ("Port", "പോർട്ട്"), - ("Settings", "ക്രമീകരണങ്ങൾ"), - ("Username", "യൂസർ നെയിം"), - ("Invalid port", "അസാധുവായ പോർട്ട്"), - ("Closed manually by the peer", "മറുഭാഗത്തുനിന്നും മാനുവലായി അടച്ചു"), - ("Enable remote configuration modification", "റിമോട്ട് കോൺഫിഗറേഷൻ മാറ്റങ്ങൾ അനുവദിക്കുക"), - ("Run without install", "ഇൻസ്റ്റാൾ ചെയ്യാതെ പ്രവർത്തിപ്പിക്കുക"), - ("Connect via relay", "റിലേ വഴി കണക്ട് ചെയ്യുക"), - ("Always connect via relay", "എപ്പോഴും റിലേ വഴി കണക്ട് ചെയ്യുക"), - ("whitelist_tip", "വൈറ്റ്‌ലിസ്റ്റ് ചെയ്ത ഐപികൾക്ക് മാത്രമേ എന്നെ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ"), - ("Login", "ലോഗിൻ"), - ("Verify", "പരിശോധിക്കുക"), - ("Remember me", "എന്നെ ഓർമ്മിക്കുക"), - ("Trust this device", "ഈ ഉപകരണം വിശ്വസിക്കുക"), - ("Verification code", "വെരിഫിക്കേഷൻ കോഡ്"), - ("verification_tip", "വെരിഫിക്കേഷൻ കോഡ് നിങ്ങളുടെ ഇമെയിലിലേക്ക് അയച്ചു"), - ("Logout", "ലോഗൗട്ട്"), - ("Tags", "ടാഗുകൾ"), - ("Search ID", "ഐഡി തിരയുക"), - ("whitelist_sep", "കോമ, സെമി കോളൻ അല്ലെങ്കിൽ സ്പേസ് ഉപയോഗിച്ച് തിരിക്കുക"), - ("Add ID", "ഐഡി ചേർക്കുക"), - ("Add Tag", "ടാഗ് ചേർക്കുക"), - ("Unselect all tags", "എല്ലാ ടാഗുകളും ഒഴിവാക്കുക"), - ("Network error", "നെറ്റ്‌വർക്ക് പിശക്"), - ("Username missed", "യൂസർ നെയിം നൽകിയില്ല"), - ("Password missed", "പാസ്‌വേഡ് നൽകിയില്ല"), - ("Wrong credentials", "തെറ്റായ വിവരങ്ങൾ"), - ("The verification code is incorrect or has expired", "കോഡ് തെറ്റാണ് അല്ലെങ്കിൽ കാലാവധി കഴിഞ്ഞു"), - ("Edit Tag", "ടാഗ് മാറ്റുക"), - ("Forget Password", "പാസ്‌വേഡ് മറന്നു"), - ("Favorites", "പ്രിയപ്പെട്ടവ"), - ("Add to Favorites", "പ്രിയപ്പെട്ടവയിലേക്ക് ചേർക്കുക"), - ("Remove from Favorites", "പ്രിയപ്പെട്ടവയിൽ നിന്ന് നീക്കം ചെയ്യുക"), - ("Empty", "ശൂന്യം"), - ("Invalid folder name", "അസാധുവായ ഫോൾഡർ പേര്"), - ("Socks5 Proxy", "Socks5 പ്രോക്സി"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) പ്രോക്സി"), - ("Discovered", "കണ്ടെത്തിയവ"), - ("install_daemon_tip", "കമ്പ്യൂട്ടർ തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കാൻ സർവീസ് ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("Remote ID", "റിമോട്ട് ഐഡി"), - ("Paste", "പേസ്റ്റ്"), - ("Paste here?", "ഇവിടെ പേസ്റ്റ് ചെയ്യണോ?"), - ("Are you sure to close the connection?", "കണക്ഷൻ നിർത്തണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), - ("Download new version", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുക"), - ("Touch mode", "ടച്ച് മോഡ്"), - ("Mouse mode", "മൗസ് മോഡ്"), - ("One-Finger Tap", "ഒരു വിരൽ ടാപ്പ്"), - ("Left Mouse", "മൗസ് ഇടത് ബട്ടൺ"), - ("One-Long Tap", "ഒരു നീണ്ട ടാപ്പ്"), - ("Two-Finger Tap", "രണ്ട് വിരൽ ടാപ്പ്"), - ("Right Mouse", "മൗസ് വലത് ബട്ടൺ"), - ("One-Finger Move", "ഒരു വിരൽ നീക്കം"), - ("Double Tap & Move", "രണ്ട് ടാപ്പും നീക്കവും"), - ("Mouse Drag", "മൗസ് ഡ്രാഗ്"), - ("Three-Finger vertically", "മൂന്ന് വിരൽ ലംബമായി"), - ("Mouse Wheel", "മൗസ് വീൽ"), - ("Two-Finger Move", "രണ്ട് വിരൽ നീക്കം"), - ("Canvas Move", "ക്യാൻവാസ് നീക്കുക"), - ("Pinch to Zoom", "സൂം ചെയ്യാൻ പിഞ്ച് ചെയ്യുക"), - ("Canvas Zoom", "ക്യാൻവാസ് സൂം"), - ("Reset canvas", "ക്യാൻവാസ് റീസെറ്റ് ചെയ്യുക"), - ("No permission of file transfer", "ഫയൽ കൈമാറ്റത്തിന് അനുമതിയില്ല"), - ("Note", "കുറിപ്പ്"), - ("Connection", "കണക്ഷൻ"), - ("Share screen", "സ്ക്രീൻ പങ്കിടുക"), - ("Chat", "ചാറ്റ്"), - ("Total", "ആകെ"), - ("items", "ഇനങ്ങൾ"), - ("Selected", "തിഞ്ഞെടുത്തവ"), - ("Screen Capture", "സ്ക്രീൻ ക്യാപ്ചർ"), - ("Input Control", "ഇൻപുട്ട് നിയന്ത്രണം"), - ("Audio Capture", "ഓഡിയോ ക്യാപ്ചർ"), - ("Do you accept?", "നിങ്ങൾ അംഗീകരിക്കുന്നുണ്ടോ?"), - ("Open System Setting", "സിസ്റ്റം സെറ്റിംഗ്സ് തുറക്കുക"), - ("How to get Android input permission?", "ആൻഡ്രോയിഡ് ഇൻപുട്ട് അനുമതി എങ്ങനെ നേടാം?"), - ("android_input_permission_tip1", "ഇൻപുട്ട് അനുമതിക്കായി ആക്‌സസിബിലിറ്റി സർവീസ് ഓൺ ചെയ്യുക."), - ("android_input_permission_tip2", "സെറ്റിംഗ്സിൽ RustDesk കണ്ടെത്തി അത് ഓൺ ചെയ്യുക."), - ("android_new_connection_tip", "പുതിയ കണക്ഷൻ അഭ്യർത്ഥന ലഭിച്ചു."), - ("android_service_will_start_tip", "സ്ക്രീൻ ക്യാപ്ചർ ഓൺ ചെയ്താൽ സർവീസ് താനേ തുടങ്ങും."), - ("android_stop_service_tip", "സർവീസ് നിർത്തുന്നത് എല്ലാ കണക്ഷനുകളും വിച്ഛേദിക്കും."), - ("android_version_audio_tip", "ആൻഡ്രോയിഡ് 10-ൽ കൂടുതൽ വേണം ഓഡിയോ ക്യാപ്ചർ ചെയ്യാൻ."), - ("android_start_service_tip", "സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങാൻ ക്ലിക്ക് ചെയ്യുക."), - ("android_permission_may_not_change_tip", "അനുമതികൾ പിന്നീട് മാറ്റാൻ കഴിയില്ല, ശ്രദ്ധിച്ച് തിരഞ്ഞെടുക്കുക."), - ("Account", "അക്കൗണ്ട്"), - ("Overwrite", "തിരുത്തിയെഴുതുക (Overwrite)"), - ("This file exists, skip or overwrite this file?", "ഈ ഫയൽ നിലവിലുണ്ട്, ഒഴിവാക്കണോ അതോ തിരുത്തിയെഴുതണോ?"), - ("Quit", "പുറത്തുകടക്കുക"), - ("Help", "സഹായം"), - ("Failed", "പരാജയപ്പെട്ടു"), - ("Succeeded", "വിജയിച്ചു"), - ("Someone turns on privacy mode, exit", "ആരോ പ്രൈവസി മോഡ് ഓൺ ചെയ്തു, പുറത്തുകടക്കുന്നു"), - ("Unsupported", "പിന്തുണയ്ക്കുന്നില്ല"), - ("Peer denied", "മറുഭാഗത്തുനിന്ന് നിരസിച്ചു"), - ("Please install plugins", "ദയവായി പ്ലഗിനുകൾ ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("Peer exit", "മറുഭാഗത്തുനിന്ന് പുറത്തുകടന്നു"), - ("Failed to turn off", "ഓഫ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Turned off", "ഓഫ് ചെയ്തു"), - ("Language", "ഭാഷ"), - ("Keep RustDesk background service", "RustDesk ബാക്ക്ഗ്രൗണ്ടിൽ പ്രവർത്തിപ്പിക്കുക"), - ("Ignore Battery Optimizations", "ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ അവഗണിക്കുക"), - ("android_open_battery_optimizations_tip", "കണക്ഷൻ മുറിയാതിരിക്കാൻ ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ സെറ്റിംഗ്സ് തുറക്കുക"), - ("Start on boot", "തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കുക"), - ("Start the screen sharing service on boot, requires special permissions", "തുടങ്ങുമ്പോൾ തന്നെ സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങുക, പ്രത്യേക അനുമതി ആവശ്യമാണ്"), - ("Connection not allowed", "കണക്ഷൻ അനുവദനീയമല്ല"), - ("Legacy mode", "ലെഗസി മോഡ്"), - ("Map mode", "മാപ്പ് മോഡ്"), - ("Translate mode", "ട്രാൻസ്ലേറ്റ് മോഡ്"), - ("Use permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് ഉപയോഗിക്കുക"), - ("Use both passwords", "രണ്ട് പാസ്‌വേഡുകളും ഉപയോഗിക്കുക"), - ("Set permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് സജ്ജമാക്കുക"), - ("Enable remote restart", "റിമോട്ട് റീസ്റ്റാർട്ട് അനുവദിക്കുക"), - ("Restart remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുക"), - ("Are you sure you want to restart", "റീസ്റ്റാർട്ട് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), - ("Restarting remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു"), - ("remote_restarting_tip", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു, ദയവായി കാത്തിരിക്കുക..."), - ("Copied", "കോപ്പി ചെയ്തു"), - ("Exit Fullscreen", "ഫുൾ സ്ക്രീനിൽ നിന്ന് പുറത്തുകടക്കുക"), - ("Fullscreen", "ഫുൾ സ്ക്രീൻ"), - ("Mobile Actions", "മൊബൈൽ നടപടികൾ"), - ("Select Monitor", "മോണിറ്റർ തിരഞ്ഞെടുക്കുക"), - ("Control Actions", "നിയന്ത്രണ നടപടികൾ"), - ("Display Settings", "ഡിസ്‌പ്ലേ ക്രമീകരണങ്ങൾ"), - ("Ratio", "അനുപാതം (Ratio)"), - ("Image Quality", "ചിത്രത്തിന്റെ ഗുണനിലവാരം"), - ("Scroll Style", "സ്ക്രോൾ സ്റ്റൈൽ"), - ("Show Toolbar", "ടൂൾബാർ കാണിക്കുക"), - ("Hide Toolbar", "ടൂൾബാർ മറയ്ക്കുക"), - ("Direct Connection", "നേരിട്ടുള്ള കണക്ഷൻ"), - ("Relay Connection", "റിലേ കണക്ഷൻ"), - ("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"), - ("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"), - ("Scale original", "ഒറിജിനൽ വലിപ്പം"), - ("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"), - ("General", "പൊതുവായവ"), - ("Security", "സുരക്ഷ"), - ("Theme", "തീം"), - ("Dark Theme", "ഡാർക്ക് തീം"), - ("Light Theme", "ലൈറ്റ് തീം"), - ("Dark", "ഡാർക്ക്"), - ("Light", "ലൈറ്റ്"), - ("Follow System", "സിസ്റ്റം അനുസരിച്ച്"), - ("Enable hardware codec", "ഹാർഡ്‌വെയർ കോഡെക് അനുവദിക്കുക"), - ("Unlock Security Settings", "സുരക്ഷാ ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"), - ("Enable audio", "ശബ്ദം അനുവദിക്കുക"), - ("Unlock Network Settings", "നെറ്റ്‌വർക്ക് ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"), - ("Server", "സെർവർ"), - ("Direct IP Access", "നേരിട്ടുള്ള IP ആക്‌സസ്"), - ("Proxy", "പ്രോക്സി"), - ("Apply", "പ്രയോഗിക്കുക"), - ("Disconnect all devices?", "എല്ലാ ഉപകരണങ്ങളും വിച്ഛേദിക്കണോ?"), - ("Clear", "വൃത്തിയാക്കുക"), - ("Audio Input Device", "ശബ്ദ ഇൻപുട്ട് ഉപകരണം"), - ("Use IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ് ഉപയോഗിക്കുക"), - ("Network", "നെറ്റ്‌വർക്ക്"), - ("Pin Toolbar", "ടൂൾബാർ പിൻ ചെയ്യുക"), - ("Unpin Toolbar", "ടൂൾബാർ അൺപിൻ ചെയ്യുക"), - ("Recording", "റെക്കോർഡിംഗ്"), - ("Directory", "ഡയറക്ടറി"), - ("Automatically record incoming sessions", "വരുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"), - ("Automatically record outgoing sessions", "പോകുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"), - ("Change", "മാറ്റുക"), - ("Start session recording", "റെക്കോർഡിംഗ് തുടങ്ങുക"), - ("Stop session recording", "റെക്കോർഡിംഗ് നിർത്തുക"), - ("Enable recording session", "സെഷൻ റെക്കോർഡിംഗ് അനുവദിക്കുക"), - ("Enable LAN discovery", "LAN കണ്ടെത്തൽ അനുവദിക്കുക"), - ("Deny LAN discovery", "LAN കണ്ടെത്തൽ നിരസിക്കുക"), - ("Write a message", "സന്ദേശം എഴുതുക"), - ("Prompt", "പ്രോംപ്റ്റ്"), - ("Please wait for confirmation of UAC...", "UAC സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുക..."), - ("elevated_foreground_window_tip", "റിമോട്ടിലെ വിൻഡോയ്ക്ക് കൂടുതൽ അനുമതി ആവശ്യമാണ്."), - ("Disconnected", "വിച്ഛേദിച്ചു"), - ("Other", "മറ്റുള്ളവ"), - ("Confirm before closing multiple tabs", "ടാബുകൾ അടയ്ക്കുന്നതിന് മുൻപ് സ്ഥിരീകരിക്കുക"), - ("Keyboard Settings", "കീബോർഡ് ക്രമീകരണങ്ങൾ"), - ("Full Access", "പൂർണ്ണ ആക്‌സസ്"), - ("Screen Share", "സ്ക്രീൻ ഷെയർ"), - ("ubuntu-21-04-required", "Ubuntu 21.04 എങ്കിലും വേണം"), - ("wayland-requires-higher-linux-version", "Wayland-ന് പുതിയ ലിനക്സ് പതിപ്പ് ആവശ്യമാണ്"), - ("xdp-portal-unavailable", "XDP പോർട്ടൽ ലഭ്യമല്ല"), - ("JumpLink", "ജമ്പ്‌ലിങ്ക്"), - ("Please Select the screen to be shared(Operate on the peer side).", "പങ്കിടാനുള്ള സ്ക്രീൻ തിരഞ്ഞെടുക്കുക (മറുഭാഗത്ത് ചെയ്യുക)."), - ("Show RustDesk", "RustDesk കാണിക്കുക"), - ("This PC", "ഈ പിസി"), - ("or", "അല്ലെങ്കിൽ"), - ("Elevate", "എലിവേറ്റ് ചെയ്യുക"), - ("Zoom cursor", "സൂം കർസർ"), - ("Accept sessions via password", "പാസ്‌വേഡ് വഴി സെഷനുകൾ അനുവദിക്കുക"), - ("Accept sessions via click", "ക്ലിക്ക് വഴി സെഷനുകൾ അനുവദിക്കുക"), - ("Accept sessions via both", "രണ്ടും വഴി സെഷനുകൾ അനുവദിക്കുക"), - ("Please wait for the remote side to accept your session request...", "മറുഭാഗം അനുമതി നൽകാനായി കാത്തിരിക്കുക..."), - ("One-time Password", "ഒറ്റത്തവണ പാസ്‌വേഡ്"), - ("Use one-time password", "ഒറ്റത്തവണ പാസ്‌വേഡ് ഉപയോഗിക്കുക"), - ("One-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം"), - ("Request access to your device", "നിങ്ങളുടെ ഉപകരണം ആക്‌സസ് ചെയ്യാൻ അനുമതി ചോദിക്കുന്നു"), - ("Hide connection management window", "കണക്ഷൻ മാനേജ്‌മെന്റ് വിൻഡോ മറയ്ക്കുക"), - ("hide_cm_tip", "പാസ്‌വേഡ് വഴിയുള്ള കണക്ഷൻ ആണെങ്കിൽ മാത്രം മറയ്ക്കുക"), - ("wayland_experiment_tip", "Wayland പിന്തുണ പരീക്ഷണാടിസ്ഥാനത്തിലാണ്"), - ("Right click to select tabs", "ടാബുകൾ തിരഞ്ഞെടുക്കാൻ വലത് ക്ലിക്ക് ചെയ്യുക"), - ("Skipped", "ഒഴിവാക്കി"), - ("Add to address book", "അഡ്രസ് ബുക്കിലേക്ക് ചേർക്കുക"), - ("Group", "ഗ്രൂപ്പ്"), - ("Search", "തിരയുക"), - ("Closed manually by web console", "വെബ് കൺസോൾ വഴി മാനുവലായി അടച്ചു"), - ("Local keyboard type", "ലോക്കൽ കീബോർഡ് തരം"), - ("Select local keyboard type", "ലോക്കൽ കീബോർഡ് തരം തിരഞ്ഞെടുക്കുക"), - ("software_render_tip", "സ്ക്രീൻ കറുത്തിരിക്കുകയാണെങ്കിൽ ഇത് പരീക്ഷിക്കുക"), - ("Always use software rendering", "എപ്പോഴും സോഫ്റ്റ്‌വെയർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), - ("config_input", "ഇൻപുട്ട് ക്രമീകരിക്കുക"), - ("config_microphone", "മൈക്രോഫോൺ ക്രമീകരിക്കുക"), - ("request_elevation_tip", "മറുഭാഗത്തുനിന്ന് എലവേഷൻ ആവശ്യപ്പെടുക"), - ("Wait", "കാത്തിരിക്കുക"), - ("Elevation Error", "എലവേഷൻ പിശക്"), - ("Ask the remote user for authentication", "റിമോട്ട് ഉപയോക്താവിനോട് അനുമതി ചോദിക്കുക"), - ("Choose this if the remote account is administrator", "റിമോട്ട് അക്കൗണ്ട് അഡ്മിനിസ്ട്രേറ്റർ ആണെങ്കിൽ ഇത് തിരഞ്ഞെടുക്കുക"), - ("Transmit the username and password of administrator", "അഡ്മിനിസ്ട്രേറ്റർ വിവരങ്ങൾ അയക്കുക"), - ("still_click_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC വിൻഡോയിൽ 'അതെ' എന്ന് ക്ലിക്ക് ചെയ്യേണ്ടതുണ്ട്."), - ("Request Elevation", "എലവേഷൻ ആവശ്യപ്പെടുക"), - ("wait_accept_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC അംഗീകരിക്കാൻ കാത്തിരിക്കുക."), - ("Elevate successfully", "വിജയകരമായി എലവേറ്റ് ചെയ്തു"), - ("uppercase", "വലിയ അക്ഷരം (Uppercase)"), - ("lowercase", "ചെറിയ അക്ഷരം (Lowercase)"), - ("digit", "അക്കം"), - ("special character", "പ്രത്യേക ചിഹ്നം"), - ("length>=8", "നീളം >= 8"), - ("Weak", "ദുർബലം"), - ("Medium", "ഇടത്തരം"), - ("Strong", "ശക്തം"), - ("Switch Sides", "വശങ്ങൾ മാറ്റുക"), - ("Please confirm if you want to share your desktop?", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് പങ്കിടണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), - ("Display", "ഡിസ്‌പ്ലേ"), - ("Default View Style", "സാധാരണ വ്യൂ സ്റ്റൈൽ"), - ("Default Scroll Style", "സാധാരണ സ്ക്രോൾ സ്റ്റൈൽ"), - ("Default Image Quality", "സാധാരണ ഇമേജ് ക്വാളിറ്റി"), - ("Default Codec", "സാധാരണ കോഡെക്"), - ("Bitrate", "ബിറ്റ്റേറ്റ്"), - ("FPS", "FPS"), - ("Auto", "ഓട്ടോ"), - ("Other Default Options", "മറ്റ് സാധാരണ ഓപ്ഷനുകൾ"), - ("Voice call", "വോയിസ് കോൾ"), - ("Text chat", "ടെക്സ്റ്റ് ചാറ്റ്"), - ("Stop voice call", "വോയിസ് കോൾ നിർത്തുക"), - ("relay_hint_tip", "നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ല; റിലേ വഴി ശ്രമിക്കാം."), - ("Reconnect", "വീണ്ടും കണക്ട് ചെയ്യുക"), - ("Codec", "കോഡെക്"), - ("Resolution", "റെസല്യൂഷൻ"), - ("No transfers in progress", "കൈമാറ്റങ്ങളൊന്നും നടക്കുന്നില്ല"), - ("Set one-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം നിശ്ചയിക്കുക"), - ("RDP Settings", "RDP ക്രമീകരണങ്ങൾ"), - ("Sort by", "ക്രമീകരിക്കുക"), - ("New Connection", "പുതിയ കണക്ഷൻ"), - ("Restore", "പുനഃസ്ഥാപിക്കുക"), - ("Minimize", "ചുരുക്കുക"), - ("Maximize", "വലുതാക്കുക"), - ("Your Device", "നിങ്ങളുടെ ഉപകരണം"), - ("empty_recent_tip", "സമീപകാല സെഷനുകൾ ഇവിടെ കാണാം."), - ("empty_favorite_tip", "പ്രിയപ്പെട്ടവ ഇവിടെ കാണാം."), - ("empty_lan_tip", "ലോക്കൽ നെറ്റ്‌വർക്കിലെ ഉപകരണങ്ങൾ ഇവിടെ കാണാം."), - ("empty_address_book_tip", "അഡ്രസ് ബുക്ക് ശൂന്യമാണ്."), - ("Empty Username", "യൂസർ നെയിം നൽകിയില്ല"), - ("Empty Password", "പാസ്‌വേഡ് നൽകിയില്ല"), - ("Me", "ഞാൻ"), - ("identical_file_tip", "ഈ ഫയൽ നിലവിലുണ്ട്."), - ("show_monitors_tip", "ടൂൾബാറിൽ മോണിറ്ററുകൾ കാണിക്കുക"), - ("View Mode", "വ്യൂ മോഡ്"), - ("login_linux_tip", "റിമോട്ട് ലിനക്സ് സെഷനായി ലോഗിൻ ചെയ്യണം"), - ("verify_rustdesk_password_tip", "RustDesk പാസ്‌വേഡ് പരിശോധിക്കുക"), - ("remember_account_tip", "ഈ അക്കൗണ്ട് ഓർമ്മിക്കുക"), - ("os_account_desk_tip", "ആക്‌സസിനായി OS അക്കൗണ്ട് ഉപയോഗിക്കുക"), - ("OS Account", "OS അക്കൗണ്ട്"), - ("another_user_login_title_tip", "മറ്റൊരു ഉപയോക്താവ് ലോഗിൻ ചെയ്തിട്ടുണ്ട്"), - ("another_user_login_text_tip", "വിച്ഛേദിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക"), - ("xorg_not_found_title_tip", "Xorg കണ്ടെത്താനായില്ല"), - ("xorg_not_found_text_tip", "ദയവായി Xorg ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("no_desktop_title_tip", "ഡെസ്ക്ടോപ്പ് ലഭ്യമല്ല"), - ("no_desktop_text_tip", "ദയവായി ലിനക്സ് ഡെസ്ക്ടോപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("No need to elevate", "എലവേറ്റ് ചെയ്യേണ്ടതില്ല"), - ("System Sound", "സിസ്റ്റം സൗണ്ട്"), - ("Default", "ഡിഫോൾട്ട്"), - ("New RDP", "പുതിയ RDP"), - ("Fingerprint", "ഫിംഗർപ്രിന്റ്"), - ("Copy Fingerprint", "ഫിംഗർപ്രിന്റ് കോപ്പി ചെയ്യുക"), - ("no fingerprints", "ഫിംഗർപ്രിന്റുകൾ ഇല്ല"), - ("Select a peer", "ഒരാളെ തിരഞ്ഞെടുക്കുക"), - ("Select peers", "തിരഞ്ഞെടുക്കുക"), - ("Plugins", "പ്ലഗിനുകൾ"), - ("Uninstall", "അൺഇൻസ്റ്റാൾ ചെയ്യുക"), - ("Update", "അപ്ഡേറ്റ് ചെയ്യുക"), - ("Enable", "പ്രവർത്തനക്ഷമമാക്കുക"), - ("Disable", "പ്രവർത്തനരഹിതമാക്കുക"), - ("Options", "ഓപ്ഷനുകൾ"), - ("resolution_original_tip", "ഒറിജിനൽ റെസല്യൂഷൻ"), - ("resolution_fit_local_tip", "ലോക്കൽ സ്ക്രീനിന് അനുയോജ്യം"), - ("resolution_custom_tip", "കസ്റ്റം റെസല്യൂഷൻ"), - ("Collapse toolbar", "ടൂൾബാർ ചുരുക്കുക"), - ("Accept and Elevate", "അംഗീകരിച്ച് എലവേറ്റ് ചെയ്യുക"), - ("accept_and_elevate_btn_tooltip", "കണക്ഷൻ അംഗീകരിച്ച് UAC അനുമതികൾ നൽകുക."), - ("clipboard_wait_response_timeout_tip", "ക്ലിപ്പ്ബോർഡ് മറുപടിക്കായി കാത്തിരുന്നു സമയം കഴിഞ്ഞു."), - ("Incoming connection", "വരുന്ന കണക്ഷൻ"), - ("Outgoing connection", "പോകുന്ന കണക്ഷൻ"), - ("Exit", "പുറത്തുകടക്കുക"), - ("Open", "തുറക്കുക"), - ("logout_tip", "നിങ്ങൾക്ക് ലോഗൗട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?"), - ("Service", "സർവീസ്"), - ("Start", "തുടങ്ങുക"), - ("Stop", "നിർത്തുക"), - ("exceed_max_devices", "നിങ്ങൾ ഉപകരണങ്ങളുടെ പരിധി കവിഞ്ഞു."), - ("Sync with recent sessions", "സമീപകാല സെഷനുകളുമായി സિંക് ചെയ്യുക"), - ("Sort tags", "ടാഗുകൾ ക്രമീകരിക്കുക"), - ("Open connection in new tab", "പുതിയ ടാബിൽ തുറക്കുക"), - ("Move tab to new window", "ടാബ് പുതിയ വിൻഡോയിലേക്ക് മാറ്റുക"), - ("Can not be empty", "ശൂന്യമാകാൻ പാടില്ല"), - ("Already exists", "നിലവിലുണ്ട്"), - ("Change Password", "പാസ്‌വേഡ് മാറ്റുക"), - ("Refresh Password", "പാസ്‌വേഡ് പുതുക്കുക"), - ("ID", "ഐഡി"), - ("Grid View", "ഗ്രിഡ് വ്യൂ"), - ("List View", "ലിസ്റ്റ് വ്യൂ"), - ("Select", "തിരഞ്ഞെടുക്കുക"), - ("Toggle Tags", "ടാഗുകൾ മാറ്റുക"), - ("pull_ab_failed_tip", "അഡ്രസ് ബുക്ക് അപ്‌ഡേറ്റ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), - ("push_ab_failed_tip", "അഡ്രസ് ബുക്ക് സિંക് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), - ("synced_peer_readded_tip", "സമീപകാല ഉപകരണം അഡ്രസ് ബുക്കിലേക്ക് സિંക് ചെയ്തു."), - ("Change Color", "നിറം മാറ്റുക"), - ("Primary Color", "പ്രധാന നിറം"), - ("HSV Color", "HSV നിറം"), - ("Installation Successful!", "ഇൻസ്റ്റാളേഷൻ വിജയിച്ചു!"), - ("Installation failed!", "ഇൻസ്റ്റാളേഷൻ പരാജയപ്പെട്ടു!"), - ("Reverse mouse wheel", "മൗസ് വീൽ തിരിക്കുക"), - ("{} sessions", "{} സെഷനുകൾ"), - ("scam_title", "തട്ടിപ്പ് മുന്നറിയിപ്പ്!"), - ("scam_text1", "നിങ്ങൾക്ക് പരിചയമില്ലാത്ത ആരെങ്കിലും RustDesk ഉപയോഗിക്കാൻ ആവശ്യപ്പെട്ടാൽ ഉടൻ കണക്ഷൻ വിച്ഛേദിക്കുക."), - ("scam_text2", "ഇതൊരു തട്ടിപ്പായിരിക്കാം. ആർക്കും പാസ്‌വേഡ് നൽകരുത്."), - ("Don't show again", "വീണ്ടും കാണിക്കരുത്"), - ("I Agree", "ഞാൻ സമ്മതിക്കുന്നു"), - ("Decline", "നിരസിക്കുന്നു"), - ("Timeout in minutes", "മിനിറ്റുകളിൽ സമയം നിശ്ചയിക്കുക"), - ("auto_disconnect_option_tip", "പ്രവർത്തനമില്ലെങ്കിൽ താനേ വിച്ഛേദിക്കുക"), - ("Connection failed due to inactivity", "പ്രവർത്തനമില്ലാത്തതിനാൽ കണക്ഷൻ വിച്ഛേദിച്ചു"), - ("Check for software update on startup", "തുടങ്ങുമ്പോൾ അപ്‌ഡേറ്റ് ഉണ്ടോ എന്ന് പരിശോധിക്കുക"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "സെർവർ പ്രോ {} ലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക"), - ("pull_group_failed_tip", "ഗ്രൂപ്പ് വിവരങ്ങൾ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), - ("Filter by intersection", "ഇന്റർസെക്ഷൻ വഴി ഫിൽട്ടർ ചെയ്യുക"), - ("Remove wallpaper during incoming sessions", "കണക്ഷൻ സമയത്ത് വാൾപേപ്പർ മാറ്റുക"), - ("Test", "പരിശോധിക്കുക"), - ("display_is_plugged_out_msg", "ഡിസ്‌പ്ലേ ഊരിയിരിക്കുകയാണ്."), - ("No displays", "ഡിസ്‌പ്ലേകൾ ഇല്ല"), - ("Open in new window", "പുതിയ വിൻഡോയിൽ തുറക്കുക"), - ("Show displays as individual windows", "ഓരോ ഡിസ്‌പ്ലേയും ഓരോ വിൻഡോയായി കാണിക്കുക"), - ("Use all my displays for the remote session", "എല്ലാ ഡിസ്‌പ്ലേകളും ഉപയോഗിക്കുക"), - ("selinux_tip", "SELinux പ്രവർത്തനക്ഷമമാണ്."), - ("Change view", "കാഴ്ച മാറ്റുക"), - ("Big tiles", "വലിയ ടൈലുകൾ"), - ("Small tiles", "ചെറിയ ടൈലുകൾ"), - ("List", "ലിസ്റ്റ്"), - ("Virtual display", "വെർച്വൽ ഡിസ്‌പ്ലേ"), - ("Plug out all", "എല്ലാം ഊരുക"), - ("True color (4:4:4)", "ട്രൂ കളർ (4:4:4)"), - ("Enable blocking user input", "യൂസർ ഇൻപുട്ട് തടയുന്നത് അനുവദിക്കുക"), - ("id_input_tip", "നിങ്ങൾക്ക് ഐഡി, ഏലിയാസ് അല്ലെങ്കിൽ ഐപി നൽകാം."), - ("privacy_mode_impl_mag_tip", "മാഗ്നിഫയർ സ്വകാര്യ മോഡ്"), - ("privacy_mode_impl_virtual_display_tip", "വെർച്വൽ ഡിസ്‌പ്ലേ സ്വകാര്യ മോഡ്"), - ("Enter privacy mode", "സ്വകാര്യ മോഡിലേക്ക് കടക്കുക"), - ("Exit privacy mode", "സ്വകാര്യ മോഡിൽ നിന്ന് പുറത്തുകടക്കുക"), - ("idd_not_support_under_win10_2004_tip", "Windows 10 (2004) എങ്കിലും വേണം."), - ("input_source_1_tip", "ഇൻപുട്ട് സോഴ്സ് 1"), - ("input_source_2_tip", "ഇൻപുട്ട് സോഴ്സ് 2"), - ("Swap control-command key", "Control-Command കീകൾ പരസ്പരം മാറ്റുക"), - ("swap-left-right-mouse", "ഇടത്-വലത് മൗസ് ബട്ടണുകൾ മാറ്റുക"), - ("2FA code", "2FA കോഡ്"), - ("More", "കൂടുതൽ"), - ("enable-2fa-title", "2FA ഓൺ ചെയ്യുക"), - ("enable-2fa-desc", "അതന്റിക്കേറ്റർ ആപ്പ് സജ്ജമാക്കുക."), - ("wrong-2fa-code", "തെറ്റായ 2FA കോഡ്."), - ("enter-2fa-title", "2FA കോഡ് നൽകുക"), - ("Email verification code must be 6 characters.", "ഇമെയിൽ കോഡ് 6 അക്ഷരങ്ങൾ വേണം."), - ("2FA code must be 6 digits.", "2FA കോഡ് 6 അക്കങ്ങൾ വേണം."), - ("Multiple Windows sessions found", "ഒന്നിലധികം വിൻഡോസ് സെഷനുകൾ കണ്ടെത്തി"), - ("Please select the session you want to connect to", "ബന്ധിപ്പിക്കേണ്ട സെഷൻ തിരഞ്ഞെടുക്കുക"), - ("powered_by_me", "ഞാൻ നിർമ്മിച്ചത്"), - ("outgoing_only_desk_tip", "ഇതൊരു ഔട്ട്‌ഗോയിംഗ് മോഡ് മാത്രമാണ്"), - ("preset_password_warning", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മാറ്റുക."), - ("Security Alert", "സുരക്ഷാ മുന്നറിയിപ്പ്"), - ("My address book", "എന്റെ അഡ്രസ് ബുക്ക്"), - ("Personal", "വ്യക്തിഗതം"), - ("Owner", "ഉടമസ്ഥൻ"), - ("Set shared password", "പങ്കിട്ട പാസ്‌വേഡ് സജ്ജമാക്കുക"), - ("Exist in", "നിലവിലുള്ളത്"), - ("Read-only", "വായിക്കാൻ മാത്രം"), - ("Read/Write", "വായിക്കാനും എഴുതാനും"), - ("Full Control", "പൂർണ്ണ നിയന്ത്രണം"), - ("share_warning_tip", "നിങ്ങളുടെ വിവരങ്ങൾ പങ്കിടുകയാണ്."), - ("Everyone", "എല്ലാവരും"), - ("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", "റിമോട്ട് വിൻഡോ ഫോക്കസിനെ പിന്തുടരുക"), - ("default_proxy_tip", "ഡിഫോൾട്ട് പ്രോക്സി ക്രമീകരണം"), - ("no_audio_input_device_tip", "ഓഡിയോ ഇൻപുട്ട് ഉപകരണം കണ്ടെത്തിയില്ല."), - ("Incoming", "വരുന്നവ"), - ("Outgoing", "പോകുന്നവ"), - ("Clear Wayland screen selection", "Wayland സ്ക്രീൻ സെലക്ഷൻ മാറ്റുക"), - ("clear_Wayland_screen_selection_tip", "സ്ക്രീൻ സെലക്ഷൻ റീസെറ്റ് ചെയ്യുക."), - ("confirm_clear_Wayland_screen_selection_tip", "സെലക്ഷൻ മാറ്റണമെന്ന് ഉറപ്പാണോ?"), - ("android_new_voice_call_tip", "പുതിയ വോയിസ് കോൾ അഭ്യർത്ഥന"), - ("texture_render_tip", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), - ("Use texture rendering", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), - ("Floating window", "ഫ്ലോട്ടിംഗ് വിൻഡോ"), - ("floating_window_tip", "ബാക്ക്ഗ്രൗണ്ടിലാണെങ്കിലും RustDesk കാണിക്കുക"), - ("Keep screen on", "സ്ക്രീൻ ഓഫ് ആകാതെ വെക്കുക"), - ("Never", "ഒരിക്കലുമില്ല"), - ("During controlled", "നിയന്ത്രിക്കുമ്പോൾ"), - ("During service is on", "സർവീസ് ഓൺ ആയിരിക്കുമ്പോൾ"), - ("Capture screen using DirectX", "DirectX ഉപയോഗിച്ച് സ്ക്രീൻ ക്യാപ്ചർ ചെയ്യുക"), - ("Back", "പുറകോട്ട്"), - ("Apps", "ആപ്പുകൾ"), - ("Volume up", "ശബ്ദം കൂട്ടുക"), - ("Volume down", "ശബ്ദം കുറയ്ക്കുക"), - ("Power", "പവർ"), - ("Telegram bot", "ടെലഗ്രാം ബോട്ട്"), - ("enable-bot-tip", "അറിയിപ്പുകൾക്കായി ബോട്ട് ഓൺ ചെയ്യുക"), - ("enable-bot-desc", "ടെലഗ്രാം ബോട്ട് സജ്ജമാക്കുക."), - ("cancel-2fa-confirm-tip", "2FA റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"), - ("cancel-bot-confirm-tip", "ബോട്ട് റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"), - ("About RustDesk", "RustDesk-നെ കുറിച്ച്"), - ("Send clipboard keystrokes", "ക്ലിപ്പ്ബോർഡ് കീസ്ട്രോക്കുകൾ അയക്കുക"), - ("network_error_tip", "നെറ്റ്‌വർക്ക് പിശക്, വീണ്ടും ശ്രമിക്കുക."), - ("Unlock with PIN", "പിൻ ഉപയോഗിച്ച് അൺലോക്ക് ചെയ്യുക"), - ("Requires at least {} characters", "കുറഞ്ഞത് {} അക്ഷരങ്ങൾ വേണം"), - ("Wrong PIN", "തെറ്റായ പിൻ"), - ("Set PIN", "പിൻ സജ്ജമാക്കുക"), - ("Enable trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ അനുവദിക്കുക"), - ("Manage trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ നിയന്ത്രിക്കുക"), - ("Platform", "പ്ലാറ്റ്‌ഫോം"), - ("Days remaining", "ബാക്കിയുള്ള ദിവസങ്ങൾ"), - ("enable-trusted-devices-tip", "വിശ്വസനീയമായവയ്ക്ക് പാസ്‌വേഡ് വേണ്ട"), - ("Parent directory", "പ്രധാന ഡയറക്ടറി"), - ("Resume", "തുടരുക"), - ("Invalid file name", "അസാധുവായ ഫയൽ പേര്"), - ("one-way-file-transfer-tip", "ഒരു വശത്തേക്ക് മാത്രമുള്ള ഫയൽ കൈമാറ്റം"), - ("Authentication Required", "അംഗീകാരം ആവശ്യമാണ്"), - ("Authenticate", "അംഗീകരിക്കുക"), - ("web_id_input_tip", "റിമോട്ട് ഐഡി നൽകുക"), - ("Download", "ഡൗൺലോഡ്"), - ("Upload folder", "ഫോൾഡർ അപ്‌ലോഡ് ചെയ്യുക"), - ("Upload files", "ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യുക"), - ("Clipboard is synchronized", "ക്ലിപ്പ്ബോർഡ് സങ്കലനം ചെയ്തു"), - ("Update client clipboard", "ക്ലയന്റ് ക്ലിപ്പ്ബോർഡ് പുതുക്കുക"), - ("Untagged", "ടാഗ് ചെയ്യാത്തവ"), - ("new-version-of-{}-tip", "{} പുതിയ പതിപ്പ് ലഭ്യമാണ്"), - ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), - ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), - ("Use D3D rendering", ""), - ("Printer", "പ്രിന്റർ"), - ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), - ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), - ("printer-{}-not-installed-tip", "പ്രിന്റർ {} ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല."), - ("printer-{}-ready-tip", "പ്രിന്റർ {} തയ്യാറാണ്."), - ("Install {} Printer", "{} പ്രിന്റർ ഇൻസ്റ്റാൾ ചെയ്യുക"), - ("Outgoing Print Jobs", "പോകുന്ന പ്രിന്റ് ജോലികൾ"), - ("Incoming Print Jobs", "വരുന്ന പ്രിന്റ് ജോലികൾ"), - ("Incoming Print Job", "വരുന്ന പ്രിന്റ് ജോലി"), - ("use-the-default-printer-tip", "ഡിഫോൾട്ട് പ്രിന്റർ ഉപയോഗിക്കുക"), - ("use-the-selected-printer-tip", "തിഞ്ഞെടുത്ത പ്രിന്റർ ഉപയോഗിക്കുക"), - ("auto-print-tip", "താനേ പ്രിന്റ് ചെയ്യുക"), - ("print-incoming-job-confirm-tip", "പ്രിന്റ് ചെയ്യുന്നതിന് മുൻപ് ചോദിക്കുക"), - ("remote-printing-disallowed-tile-tip", "റിമോട്ട് പ്രിന്റിംഗ് അനുവദനീയമല്ല"), - ("remote-printing-disallowed-text-tip", "സെറ്റിംഗ്സിൽ റിമോട്ട് പ്രിന്റിംഗ് ഓൺ ചെയ്യുക."), - ("save-settings-tip", "സെറ്റിംഗ്സ് സേവ് ചെയ്യുക"), - ("dont-show-again-tip", "വീണ്ടും കാണിക്കരുത്"), - ("Take screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുക"), - ("Taking screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുന്നു"), - ("screenshot-merged-screen-not-supported-tip", "മെർജ് ചെയ്ത സ്ക്രീൻഷോട്ട് പിന്തുണയ്ക്കുന്നില്ല."), - ("screenshot-action-tip", "സ്ക്രീൻഷോട്ടിന് ശേഷമുള്ള നടപടി"), - ("Save as", "പേരിൽ സേവ് ചെയ്യുക"), - ("Copy to clipboard", "ക്ലിപ്പ്ബോർഡിലേക്ക് കോപ്പി ചെയ്യുക"), - ("Enable remote printer", "റിമോട്ട് പ്രിന്റർ അനുവദിക്കുക"), - ("Downloading {}", "{} ഡൗൺലോഡ് ചെയ്യുന്നു"), - ("{} Update", "{} അപ്‌ഡേറ്റ്"), - ("{}-to-update-tip", "അപ്‌ഡേറ്റ് ചെയ്യാൻ {}"), - ("download-new-version-failed-tip", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), - ("Auto update", "ഓട്ടോ അപ്‌ഡേറ്റ്"), - ("update-failed-check-msi-tip", "അപ്‌ഡേറ്റ് പരാജയപ്പെട്ടു, MSI ഫയൽ പരിശോധിക്കുക."), - ("websocket_tip", "പോട്ടുകൾ തടഞ്ഞിട്ടുണ്ടെങ്കിൽ WebSocket ഉപയോഗിക്കുക."), - ("Use WebSocket", "WebSocket ഉപയോഗിക്കുക"), - ("Trackpad speed", "ട്രാക്ക്പാഡ് വേഗത"), - ("Default trackpad speed", "സാധാരണ ട്രാക്ക്പാഡ് വേഗത"), - ("Numeric one-time password", "അക്കങ്ങൾ മാത്രമുള്ള OTP"), - ("Enable IPv6 P2P connection", "IPv6 P2P കണക്ഷൻ അനുവദിക്കുക"), - ("Enable UDP hole punching", "UDP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"), - ("View camera", "ക്യാമറ കാണുക"), - ("Enable camera", "ക്യാമറ ഓൺ ചെയ്യുക"), - ("No cameras", "ക്യാമറകൾ കണ്ടെത്തിയില്ല"), - ("view_camera_unsupported_tip", "റിമോട്ട് ക്യാമറ പിന്തുണയ്ക്കുന്നില്ല."), - ("Terminal", "ടെർമിനൽ"), - ("Enable terminal", "ടെർമിനൽ അനുവദിക്കുക"), - ("New tab", "പുതിയ ടാബ്"), - ("Keep terminal sessions on disconnect", "വിച്ഛേദിക്കുമ്പോൾ ടെർമിനൽ സെഷൻ നിർത്തരുത്"), - ("Terminal (Run as administrator)", "ടെർമിനൽ (അഡ്മിനിസ്ട്രേറ്ററായി)"), - ("terminal-admin-login-tip", "അഡ്മിൻ ലോഗിൻ ആവശ്യമാണ്."), - ("Failed to get user token.", "യൂസർ ടോക്കൺ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു."), - ("Incorrect username or password.", "തെറ്റായ യൂസർ നെയിം അല്ലെങ്കിൽ പാസ്‌വേഡ്."), - ("The user is not an administrator.", "ഉപയോക്താവ് അഡ്മിനിസ്ട്രേറ്ററല്ല."), - ("Failed to check if the user is an administrator.", "അഡ്മിൻ ആണോ എന്ന് പരിശോധിക്കുന്നതിൽ പരാജയപ്പെട്ടു."), - ("Supported only in the installed version.", "ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പിൽ മാത്രം ലഭ്യം."), - ("elevation_username_tip", "അഡ്മിനിസ്ട്രേറ്റർ പേര് നൽകുക"), - ("Preparing for installation ...", "ഇൻസ്റ്റാളേഷനായി ഒരുങ്ങുന്നു..."), - ("Show my cursor", "എന്റെ കർസർ കാണിക്കുക"), - ("Scale custom", "കസ്റ്റം സ്കെയിൽ"), - ("Custom scale slider", "കസ്റ്റം സ്കെയിൽ സ്ലൈഡർ"), - ("Decrease", "കുറയ്ക്കുക"), - ("Increase", "കൂട്ടുക"), - ("Show virtual mouse", "വെർച്വൽ മൗസ് കാണിക്കുക"), - ("Virtual mouse size", "വെർച്വൽ മൗസ് വലിപ്പം"), - ("Small", "ചെറുത്"), - ("Large", "വലുത്"), - ("Show virtual joystick", "വെർച്വൽ ജോയ്സ്റ്റിക് കാണിക്കുക"), - ("Edit note", "കുറിപ്പ് മാറ്റുക"), - ("Alias", "ഏലിയാസ് (Alias)"), - ("ScrollEdge", "സ്ക്രോൾ എഡ്ജ്"), - ("Allow insecure TLS fallback", "സുരക്ഷിതമല്ലാത്ത TLS അനുവദിക്കുക"), - ("allow-insecure-tls-fallback-tip", "പഴയ സെർവറുകൾക്കായി ഉപയോഗിക്കുക."), - ("Disable UDP", "UDP ഒഴിവാക്കുക"), - ("disable-udp-tip", "കണക്ഷൻ പ്രശ്നങ്ങൾക്ക് UDP ഒഴിവാക്കുക."), - ("server-oss-not-support-tip", "OSS സെർവർ ഇത് പിന്തുണയ്ക്കുന്നില്ല."), - ("input note here", "ഇവിടെ കുറിപ്പ് എഴുതുക"), - ("note-at-conn-end-tip", "കണക്ഷൻ കഴിയുമ്പോൾ കുറിപ്പ് കാണിക്കുക"), - ("Show terminal extra keys", "ടെർമിനൽ കീകൾ കാണിക്കുക"), - ("Relative mouse mode", "റിലേറ്റീവ് മൗസ് മോഡ്"), - ("rel-mouse-not-supported-peer-tip", "മറുഭാഗം പിന്തുണയ്ക്കുന്നില്ല."), - ("rel-mouse-not-ready-tip", "തയ്യാറായിട്ടില്ല."), - ("rel-mouse-lock-failed-tip", "മൗസ് ലോക്ക് പരാജയപ്പെട്ടു."), - ("rel-mouse-exit-{}-tip", "പുറത്തുകടക്കാൻ {} അമർത്തുക"), - ("rel-mouse-permission-lost-tip", "അനുമതി നഷ്ടപ്പെട്ടു."), - ("Changelog", "മാറ്റങ്ങൾ (Changelog)"), - ("keep-awake-during-outgoing-sessions-label", "സെഷൻ നടക്കുമ്പോൾ ഉറക്കത്തിലാകരുത്"), - ("keep-awake-during-incoming-sessions-label", "സെഷൻ വരുമ്പോൾ ഉറക്കത്തിലാകരുത്"), - ("Continue with {}", "{} ഉപയോഗിച്ച് തുടരുക"), - ("Display Name", "ഡിസ്‌പ്ലേ പേര്"), - ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), - ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), - ].iter().cloned().collect(); -} diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 9325dfa1f..9c38fcbb8 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastaturinnstillinger"), ("Full Access", "Full tilgang"), ("Screen Share", "Skjermdeling"), - ("ubuntu-21-04-required", "Wayland krever Ubuntu version 21.04 eller nyere."), - ("wayland-requires-higher-linux-version", "Wayland krever en nyere versjon av Linux. Prøv X11 desktop eller skift OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland krever Ubuntu version 21.04 eller nyere."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland krever en nyere versjon av Linux. Prøv X11 desktop eller skift OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "vennligst velg den skjermen, som skal deles (fjernstyres)."), ("Show RustDesk", "Vis RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsett med {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 55d272666..99b859248 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Toetsenbordinstellingen"), ("Full Access", "Volledige Toegang"), ("Screen Share", "Scherm Delen"), - ("ubuntu-21-04-required", "Wayland vereist Ubuntu 21.04 of hoger."), - ("wayland-requires-higher-linux-version", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of hoger."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), ("Show RustDesk", "Toon RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ("Continue with {}", "Ga verder met {}"), ("Display Name", "Naam Weergeven"), - ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), - ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), - ("Enable privacy mode", "Privacymodus inschakelen"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fdf4ae8c5..000c05921 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Ustawienia klawiatury"), ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), - ("ubuntu-21-04-required", "Wayland wymaga Ubuntu 21.04 lub nowszego."), - ("wayland-requires-higher-linux-version", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), - ("xdp-portal-unavailable", "Nie udało się przechwycić ekranu Wayland. Portal XDG Desktop mógł ulec awarii lub jest niedostępny. Spróbuj go ponownie uruchomić poleceniem `systemctl --user restart xdg-desktop-portal`."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), ("JumpLink", "Podgląd"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po zdalnego urządzenia)."), ("Show RustDesk", "Pokaż RustDesk"), @@ -740,10 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"), ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), ("Continue with {}", "Kontynuuj z {}"), - ("Display Name", "Nazwa wyświetlana"), - ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), - ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4138b46e4..ccbdd574e 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Configurações do teclado"), ("Full Access", "Controlo total"), ("Screen Share", ""), - ("ubuntu-21-04-required", "Wayland requer Ubuntu 21.04 ou versão superior."), - ("wayland-requires-higher-linux-version", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), ("Show RustDesk", ""), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 1428a71d0..a7a2f7db6 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Configurações de teclado"), ("Full Access", "Acesso completo"), ("Screen Share", "Compartilhamento de tela"), - ("ubuntu-21-04-required", "Wayland requer Ubuntu 21.04 ou versão superior."), - ("wayland-requires-higher-linux-version", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do parceiro)."), ("Show RustDesk", "Exibir RustDesk"), @@ -740,10 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("Continue with {}", "Continuar com {}"), - ("Display Name", "Nome de Exibição"), - ("password-hidden-tip", "A senha permanente está definida como (oculta)."), - ("preset-password-in-use-tip", "A senha predefinida está sendo usada."), - ("Enable privacy mode", "Habilitar modo de privacidade"), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index bde4a4201..8917b2a46 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -62,7 +62,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid format", "Format nevalid"), ("server_not_support", "Încă nu este compatibil cu serverul"), ("Not available", "Indisponibil"), - ("Too frequent", "Prea frecvent"), + ("Too frequent", "Modificat prea frecvent"), ("Cancel", "Anulează"), ("Skip", "Omite"), ("Close", "Închide"), @@ -87,7 +87,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Modified", "Modificat"), ("Size", "Dimensiune"), ("Show Hidden Files", "Afișează fișiere ascunse"), - ("Receive", "Primește"), + ("Receive", "Acceptă"), ("Send", "Trimite"), ("Refresh File", "Actualizează fișier"), ("Local", "Local"), @@ -108,7 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do this for all conflicts", "Aplică la toate conflictele"), ("This is irreversible!", "Această acțiune este ireversibilă!"), ("Deleting", "În curs de ștergere..."), - ("files", "fișiere"), + ("files", "fișier"), ("Waiting", "În așteptare..."), ("Finished", "Finalizat"), ("Speed", "Viteză"), @@ -203,7 +203,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "Este necesar X11"), ("Port", "Port"), ("Settings", "Setări"), - ("Username", "Nume utilizator"), + ("Username", " Nume utilizator"), ("Invalid port", "Port nevalid"), ("Closed manually by the peer", "Conexiune închisă manual de dispozitivul pereche"), ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), @@ -216,7 +216,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember me", "Reține-mă"), ("Trust this device", "Acest dispozitiv este de încredere"), ("Verification code", "Cod de verificare"), - ("verification_tip", "Introdu codul de verificare trimis la adresa ta de e-mail sau generat de aplicația de autentificare."), + ("verification_tip", ""), ("Logout", "Deconectează-te"), ("Tags", "Etichete"), ("Search ID", "Caută după ID"), @@ -228,9 +228,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Lipsește numele de utilizator"), ("Password missed", "Lipsește parola"), ("Wrong credentials", "Nume sau parolă greșită"), - ("The verification code is incorrect or has expired", "Codul de verificare este incorect sau a expirat"), + ("The verification code is incorrect or has expired", ""), ("Edit Tag", "Modifică etichetă"), - ("Forget Password", "Parolă uitată"), + ("Forget Password", "Uită parola"), ("Favorites", "Favorite"), ("Add to Favorites", "Adaugă la Favorite"), ("Remove from Favorites", "Șterge din Favorite"), @@ -263,7 +263,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Zoom", "Mărire ecran"), ("Reset canvas", "Reinițializează ecranul"), ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), - ("Note", "Notă"), + ("Note", "Reține"), ("Connection", "Conexiune"), ("Share screen", "Partajează ecran"), ("Chat", "Mesaje"), @@ -276,14 +276,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Accepți?"), ("Open System Setting", "Deschide setări sistem"), ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), - ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilizeze serviciul „Accesibilitate\"."), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul „Accesibilitate”."), ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, deschide [Aplicații instalate] și pornește serviciul [RustDesk Input]."), ("android_new_connection_tip", "Ai primit o nouă solicitare de controlare a dispozitivului actual."), ("android_service_will_start_tip", "Activarea setării de capturare a ecranului va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."), ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."), ("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Capturare ecran] pentru a porni serviciul de partajare a ecranului."), - ("android_permission_may_not_change_tip", "Este posibil ca unele permisiuni să nu poată fi modificate în funcție de versiunea de Android."), + ("android_permission_may_not_change_tip", ""), ("Account", "Cont"), ("Overwrite", "Suprascrie"), ("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"), @@ -304,15 +304,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), ("Start on boot", "Pornește la boot"), ("Start the screen sharing service on boot, requires special permissions", "Pornește serviciul de partajare a ecranului la boot; necesită permisiuni speciale"), - ("Connection not allowed", "Conexiune neautorizată"), + ("Connection not allowed", "Conexiune neautoriztă"), ("Legacy mode", "Mod legacy"), ("Map mode", "Mod hartă"), ("Translate mode", "Mod traducere"), ("Use permanent password", "Folosește parola permanentă"), - ("Use both passwords", "Folosește ambele parole"), + ("Use both passwords", "Folosește ambele programe"), ("Set permanent password", "Setează parola permanentă"), ("Enable remote restart", "Activează repornirea la distanță"), - ("Restart remote device", "Repornește dispozitivul la distanță"), + ("Restart remote device", "Repornește dispozivul la distanță"), ("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"), ("Restarting remote device", "Se repornește dispozitivul la distanță"), ("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."), @@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Detașează bara de instrumente"), ("Recording", "Înregistrare"), ("Directory", "Director"), - ("Automatically record incoming sessions", "Înregistrează automat sesiunile primite"), - ("Automatically record outgoing sessions", "Înregistrează automat sesiunile de ieșire"), + ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Automatically record outgoing sessions", ""), ("Change", "Modifică"), ("Start session recording", "Începe înregistrarea"), ("Stop session recording", "Oprește înregistrarea"), @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Setări tastatură"), ("Full Access", "Acces total"), ("Screen Share", "Partajare ecran"), - ("ubuntu-21-04-required", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), - ("wayland-requires-higher-linux-version", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), - ("xdp-portal-unavailable", "Portalul XDG Desktop nu este disponibil. Asigură-te că rulezi o sesiune Wayland cu suport pentru portal."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), ("JumpLink", "Afișează"), ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), ("Show RustDesk", "Afișează RustDesk"), @@ -436,13 +435,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Image Quality", "Calitatea implicită a imaginii"), ("Default Codec", "Codec implicit"), ("Bitrate", "Rată de biți"), - ("FPS", "FPS"), + ("FPS", "CPS"), ("Auto", "Auto"), ("Other Default Options", "Alte opțiuni implicite"), ("Voice call", "Apel vocal"), ("Text chat", "Conversație text"), ("Stop voice call", "Încheie apel vocal"), - ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r\" la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), + ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r” la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), ("Reconnect", "Reconectează-te"), ("Codec", "Codec"), ("Resolution", "Rezoluție"), @@ -503,247 +502,243 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit", "Ieși"), ("Open", "Deschide"), ("logout_tip", "Sigur vrei să te deconectezi?"), - ("Service", "Serviciu"), - ("Start", "Pornește"), - ("Stop", "Oprește"), - ("exceed_max_devices", "Numărul maxim de dispozitive a fost depășit"), - ("Sync with recent sessions", "Sincronizează cu sesiunile recente"), - ("Sort tags", "Sortează etichete"), - ("Open connection in new tab", "Deschide conexiunea într-o filă nouă"), - ("Move tab to new window", "Mută fila într-o fereastră nouă"), - ("Can not be empty", "Nu poate fi gol"), - ("Already exists", "Există deja"), - ("Change Password", "Schimbă parola"), - ("Refresh Password", "Reîmprospătează parola"), - ("ID", "ID"), - ("Grid View", "Vizualizare grilă"), - ("List View", "Vizualizare listă"), - ("Select", "Selectează"), - ("Toggle Tags", "Comută etichete"), - ("pull_ab_failed_tip", "Sincronizarea agendei a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), - ("push_ab_failed_tip", "Salvarea agendei pe server a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), - ("synced_peer_readded_tip", "Dispozitivele pereche eliminate au fost re-adăugate automat din sesiunile recente."), - ("Change Color", "Schimbă culoarea"), - ("Primary Color", "Culoare principală"), - ("HSV Color", "Culoare HSV"), - ("Installation Successful!", "Instalare reușită!"), - ("Installation failed!", "Instalare eșuată!"), - ("Reverse mouse wheel", "Inversează rotiță mouse"), - ("{} sessions", "{} sesiuni"), - ("scam_title", "Avertisment de securitate"), - ("scam_text1", "Escrocii se pot da drept angajați ai asistenței tehnice și îți pot solicita să instalezi sau să rulezi RustDesk pentru a-ți accesa dispozitivul."), - ("scam_text2", "Dacă nu ai contactat tu primul asistența tehnică, te rugăm să închizi această aplicație imediat."), - ("Don't show again", "Nu mai afișa"), - ("I Agree", "Sunt de acord"), - ("Decline", "Refuză"), - ("Timeout in minutes", "Timp de expirare în minute"), - ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), - ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), - ("Check for software update on startup", "Verifică actualizări la pornire"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), - ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), - ("Filter by intersection", "Filtrează prin intersecție"), - ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), - ("Test", "Test"), - ("display_is_plugged_out_msg", "Monitorul selectat a fost deconectat. Sesiunea continuă pe monitorul disponibil."), - ("No displays", "Niciun monitor"), - ("Open in new window", "Deschide în fereastră nouă"), - ("Show displays as individual windows", "Afișează monitoarele ca ferestre individuale"), - ("Use all my displays for the remote session", "Folosește toate monitoarele mele pentru sesiunea la distanță"), - ("selinux_tip", "SELinux este activat pe acest sistem. Este posibil ca unele funcții să nu funcționeze corect. Te rugăm să consulți documentația pentru instrucțiuni de configurare."), - ("Change view", "Schimbă vizualizarea"), - ("Big tiles", "Dale mari"), - ("Small tiles", "Dale mici"), - ("List", "Listă"), - ("Virtual display", "Monitor virtual"), - ("Plug out all", "Deconectează toate"), - ("True color (4:4:4)", "Culori reale (4:4:4)"), - ("Enable blocking user input", "Activează blocarea intrărilor utilizatorului"), - ("id_input_tip", "Introdu ID-ul sau adresa IP a dispozitivului la distanță"), - ("privacy_mode_impl_mag_tip", "Modul privat prin Magnificare — nu este suportat pe toate sistemele"), - ("privacy_mode_impl_virtual_display_tip", "Modul privat prin monitor virtual — necesită driverul de monitor virtual"), - ("Enter privacy mode", "Intră în modul privat"), - ("Exit privacy mode", "Ieși din modul privat"), - ("idd_not_support_under_win10_2004_tip", "Driverul de monitor virtual nu este suportat pe versiuni de Windows anterioare versiunii 2004 (build 19041)."), - ("input_source_1_tip", "Sursă de intrare 1 — folosește metodele standard de simulare a tastaturii și mouse-ului"), - ("input_source_2_tip", "Sursă de intrare 2 — folosește driver-ul RustDesk pentru simulare la nivel de kernel"), - ("Swap control-command key", "Schimbă tastele Control și Command"), - ("swap-left-right-mouse", "Schimbă butoanele stâng și drept ale mouse-ului"), - ("2FA code", "Cod 2FA"), - ("More", "Mai mult"), - ("enable-2fa-title", "Activează autentificarea în doi pași (2FA)"), - ("enable-2fa-desc", "Scanează codul QR cu o aplicație de autentificare (de ex. Google Authenticator) și introdu codul generat pentru a confirma activarea."), - ("wrong-2fa-code", "Cod 2FA incorect"), - ("enter-2fa-title", "Introdu codul de autentificare în doi pași"), - ("Email verification code must be 6 characters.", "Codul de verificare prin e-mail trebuie să aibă 6 caractere."), - ("2FA code must be 6 digits.", "Codul 2FA trebuie să conțină 6 cifre."), - ("Multiple Windows sessions found", "Au fost găsite mai multe sesiuni Windows"), - ("Please select the session you want to connect to", "Selectează sesiunea la care vrei să te conectezi"), - ("powered_by_me", "Realizat cu RustDesk"), - ("outgoing_only_desk_tip", "Acest dispozitiv este configurat doar pentru conexiuni de ieșire și nu acceptă conexiuni de intrare."), - ("preset_password_warning", "Parola prestabilită nu este recomandată din motive de securitate. Te rugăm să o schimbi cât mai curând posibil."), - ("Security Alert", "Alertă de securitate"), - ("My address book", "Agenda mea"), - ("Personal", "Personal"), - ("Owner", "Proprietar"), - ("Set shared password", "Setează parola partajată"), - ("Exist in", "Există în"), - ("Read-only", "Doar citire"), - ("Read/Write", "Citire/Scriere"), - ("Full Control", "Control total"), - ("share_warning_tip", "Datele partajate vor fi vizibile pentru toți membrii grupului selectat. Asigură-te că partajezi doar informații adecvate."), - ("Everyone", "Toată lumea"), - ("ab_web_console_tip", "Gestionează agenda prin consola web RustDesk Pro."), - ("allow-only-conn-window-open-tip", "Permite conexiunile numai atunci când fereastra de gestionare a conexiunilor este deschisă"), - ("no_need_privacy_mode_no_physical_displays_tip", "Modul privat nu este necesar deoarece nu există monitoare fizice conectate."), - ("Follow remote cursor", "Urmărește cursorul de la distanță"), - ("Follow remote window focus", "Urmărește fereastra activă de la distanță"), - ("default_proxy_tip", "Proxy-ul implicit este utilizat pentru toate conexiunile dacă nu este specificat altul."), - ("no_audio_input_device_tip", "Nu a fost găsit niciun dispozitiv de intrare audio. Conectează un microfon și reîncearcă."), - ("Incoming", "Intrare"), - ("Outgoing", "Ieșire"), - ("Clear Wayland screen selection", "Șterge selecția de ecran Wayland"), - ("clear_Wayland_screen_selection_tip", "Șterge selecția de ecran Wayland salvată, astfel încât să poți alege un alt ecran la următoarea conexiune."), - ("confirm_clear_Wayland_screen_selection_tip", "Sigur vrei să ștergi selecția de ecran Wayland?"), - ("android_new_voice_call_tip", "Ai primit un nou apel vocal. Apasă pentru a accepta sau respinge."), - ("texture_render_tip", "Randarea prin textură poate îmbunătăți performanța grafică pe unele dispozitive. Repornește aplicația dacă apar probleme de afișare."), - ("Use texture rendering", "Folosește randarea prin textură"), - ("Floating window", "Fereastră flotantă"), - ("floating_window_tip", "Fereastra flotantă ajută la menținerea serviciului de partajare a ecranului activ în fundal pe Android."), - ("Keep screen on", "Menține ecranul pornit"), - ("Never", "Niciodată"), - ("During controlled", "În timpul controlului"), - ("During service is on", "Cât timp serviciul este activ"), - ("Capture screen using DirectX", "Capturează ecranul folosind DirectX"), - ("Back", "Înapoi"), - ("Apps", "Aplicații"), - ("Volume up", "Mărește volumul"), - ("Volume down", "Micșorează volumul"), - ("Power", "Alimentare"), - ("Telegram bot", "Bot Telegram"), - ("enable-bot-tip", "Activează botul Telegram pentru a primi notificări și a gestiona conexiunile."), - ("enable-bot-desc", "Configurează un bot Telegram pentru notificări RustDesk. Introdu token-ul botului și ID-ul chat-ului."), - ("cancel-2fa-confirm-tip", "Sigur vrei să dezactivezi autentificarea în doi pași? Aceasta va reduce securitatea contului tău."), - ("cancel-bot-confirm-tip", "Sigur vrei să dezactivezi botul Telegram?"), - ("About RustDesk", "Despre RustDesk"), - ("Send clipboard keystrokes", "Trimite conținutul clipboard-ului ca apăsări de taste"), - ("network_error_tip", "Eroare de rețea. Verifică conexiunea la internet și încearcă din nou."), - ("Unlock with PIN", "Deblochează cu PIN"), - ("Requires at least {} characters", "Necesită cel puțin {} caractere"), - ("Wrong PIN", "PIN incorect"), - ("Set PIN", "Setează PIN"), - ("Enable trusted devices", "Activează dispozitive de încredere"), - ("Manage trusted devices", "Gestionează dispozitivele de încredere"), - ("Platform", "Platformă"), - ("Days remaining", "Zile rămase"), - ("enable-trusted-devices-tip", "Dispozitivele de încredere pot accesa contul fără verificare suplimentară."), - ("Parent directory", "Director părinte"), - ("Resume", "Reia"), - ("Invalid file name", "Nume de fișier nevalid"), - ("one-way-file-transfer-tip", "Transferul de fișiere în sens unic permite doar trimiterea sau primirea de fișiere, nu ambele direcții simultan."), - ("Authentication Required", "Autentificare necesară"), - ("Authenticate", "Autentifică-te"), - ("web_id_input_tip", "Introdu ID-ul RustDesk al dispozitivului la care vrei să te conectezi"), - ("Download", "Descarcă"), - ("Upload folder", "Încarcă folder"), - ("Upload files", "Încarcă fișiere"), - ("Clipboard is synchronized", "Clipboard-ul este sincronizat"), - ("Update client clipboard", "Actualizează clipboard-ul clientului"), - ("Untagged", "Neetichetat"), - ("new-version-of-{}-tip", "Este disponibilă o nouă versiune a {}. Fă clic pentru a actualiza."), - ("Accessible devices", "Dispozitive accesibile"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "Versiunea clientului RustDesk de la distanță este mai mică decât {}. Te rugăm să o actualizezi pentru o compatibilitate completă."), - ("d3d_render_tip", "Randarea Direct3D poate îmbunătăți performanța pe sistemele Windows cu suport hardware adecvat."), - ("Use D3D rendering", "Folosește randarea D3D"), - ("Printer", "Imprimantă"), - ("printer-os-requirement-tip", "Imprimarea la distanță necesită Windows 10 sau o versiune superioară."), - ("printer-requires-installed-{}-client-tip", "Imprimarea la distanță necesită instalarea clientului {} pe dispozitivul local."), - ("printer-{}-not-installed-tip", "Imprimanta {} nu este instalată. Instalează driverul imprimantei pentru a continua."), - ("printer-{}-ready-tip", "Imprimanta {} este pregătită pentru utilizare."), - ("Install {} Printer", "Instalează imprimanta {}"), - ("Outgoing Print Jobs", "Lucrări de imprimare de ieșire"), - ("Incoming Print Jobs", "Lucrări de imprimare de intrare"), - ("Incoming Print Job", "Lucrare de imprimare de intrare"), - ("use-the-default-printer-tip", "Folosește imprimanta implicită a sistemului pentru lucrările de imprimare primite."), - ("use-the-selected-printer-tip", "Folosește imprimanta selectată pentru lucrările de imprimare primite."), - ("auto-print-tip", "Imprimă automat lucrările primite fără confirmare."), - ("print-incoming-job-confirm-tip", "Ai primit o lucrare de imprimare. Vrei să o imprimești?"), - ("remote-printing-disallowed-tile-tip", "Imprimare la distanță nepermisă"), - ("remote-printing-disallowed-text-tip", "Dispozitivul la distanță nu permite imprimarea. Contactează administratorul pentru a activa această funcție."), - ("save-settings-tip", "Salvează setările curente ca implicite pentru sesiunile viitoare."), - ("dont-show-again-tip", "Nu mai afișa acest mesaj"), - ("Take screenshot", "Fă captură de ecran"), - ("Taking screenshot", "Se face captura de ecran..."), - ("screenshot-merged-screen-not-supported-tip", "Captura de ecran a ecranului combinat nu este suportată în prezent."), - ("screenshot-action-tip", "Selectează acțiunea pentru captura de ecran: salvează ca fișier sau copiază în clipboard."), - ("Save as", "Salvează ca"), - ("Copy to clipboard", "Copiază în clipboard"), - ("Enable remote printer", "Activează imprimanta la distanță"), - ("Downloading {}", "Se descarcă {}"), - ("{} Update", "Actualizare {}"), - ("{}-to-update-tip", "Este disponibilă o actualizare pentru {}. Fă clic pentru a descărca și instala."), - ("download-new-version-failed-tip", "Descărcarea noii versiuni a eșuat. Verifică conexiunea la internet și încearcă din nou."), - ("Auto update", "Actualizare automată"), - ("update-failed-check-msi-tip", "Actualizarea a eșuat. Încearcă să descarci și să instalezi manual fișierul MSI."), - ("websocket_tip", "WebSocket oferă o conexiune mai stabilă în unele medii de rețea restrictive."), - ("Use WebSocket", "Folosește WebSocket"), - ("Trackpad speed", "Viteza touchpad-ului"), - ("Default trackpad speed", "Viteza implicită a touchpad-ului"), - ("Numeric one-time password", "Parolă unică numerică"), - ("Enable IPv6 P2P connection", "Activează conexiunea P2P prin IPv6"), - ("Enable UDP hole punching", "Activează traversarea UDP (hole punching)"), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("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", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), ("View camera", "Vezi camera"), - ("Enable camera", "Activează camera"), - ("No cameras", "Nicio cameră disponibilă"), - ("view_camera_unsupported_tip", "Vizualizarea camerei nu este suportată pe dispozitivul la distanță."), - ("Terminal", "Terminal"), - ("Enable terminal", "Activează terminalul"), - ("New tab", "Filă nouă"), - ("Keep terminal sessions on disconnect", "Păstrează sesiunile de terminal la deconectare"), - ("Terminal (Run as administrator)", "Terminal (Rulează ca administrator)"), - ("terminal-admin-login-tip", "Introdu datele de autentificare ale administratorului pentru a rula terminalul cu privilegii sporite."), - ("Failed to get user token.", "Obținerea tokenului de utilizator a eșuat."), - ("Incorrect username or password.", "Nume de utilizator sau parolă incorectă."), - ("The user is not an administrator.", "Utilizatorul nu este administrator."), - ("Failed to check if the user is an administrator.", "Verificarea privilegiilor de administrator a eșuat."), - ("Supported only in the installed version.", "Suportat doar în versiunea instalată."), - ("elevation_username_tip", "Introdu numele de utilizator al contului de administrator pentru a solicita sporirea privilegiilor."), - ("Preparing for installation ...", "Se pregătește instalarea..."), - ("Show my cursor", "Afișează cursorul meu"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), ("Scale custom", "Scalare personalizată"), ("Custom scale slider", "Glisor pentru scalare personalizată"), ("Decrease", "Micșorează"), ("Increase", "Mărește"), - ("Show virtual mouse", "Afișează mouse virtual"), - ("Virtual mouse size", "Dimensiunea mouse-ului virtual"), - ("Small", "Mic"), - ("Large", "Mare"), - ("Show virtual joystick", "Afișează joystick virtual"), - ("Edit note", "Editează notă"), - ("Alias", "Alias"), - ("ScrollEdge", "Derulare la margine"), - ("Allow insecure TLS fallback", "Permite revenirea la TLS nesecurizat"), - ("allow-insecure-tls-fallback-tip", "Permite conexiunile cu certificate TLS nevalide sau expirate. Nu este recomandat din motive de securitate."), - ("Disable UDP", "Dezactivează UDP"), - ("disable-udp-tip", "Dezactivează conexiunile UDP și folosește doar TCP. Poate reduce performanța conexiunii."), - ("server-oss-not-support-tip", "Serverul open-source nu suportă această funcție. Folosește RustDesk Pro pentru funcționalitate completă."), - ("input note here", "Introdu o notă aici"), - ("note-at-conn-end-tip", "Afișează această notă la sfârșitul sesiunii de conexiune."), - ("Show terminal extra keys", "Afișează taste suplimentare pentru terminal"), - ("Relative mouse mode", "Mod mouse relativ"), - ("rel-mouse-not-supported-peer-tip", "Dispozitivul pereche nu suportă modul mouse relativ."), - ("rel-mouse-not-ready-tip", "Modul mouse relativ nu este pregătit. Încearcă din nou."), - ("rel-mouse-lock-failed-tip", "Blocarea mouse-ului în modul relativ a eșuat."), - ("rel-mouse-exit-{}-tip", "Apasă {} pentru a ieși din modul mouse relativ."), - ("rel-mouse-permission-lost-tip", "Permisiunea pentru modul mouse relativ a fost pierdută."), - ("Changelog", "Jurnal de modificări"), - ("keep-awake-during-outgoing-sessions-label", "Menține ecranul activ în timpul sesiunilor de ieșire"), - ("keep-awake-during-incoming-sessions-label", "Menține ecranul activ în timpul sesiunilor de intrare"), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuă cu {}"), - ("Display Name", "Nume afișat"), - ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), - ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 2605582f4..35114efe3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Настройки клавиатуры"), ("Full Access", "Полный доступ"), ("Screen Share", "Демонстрация экрана"), - ("ubuntu-21-04-required", "Wayland требуется Ubuntu версии 21.04 или новее."), - ("wayland-requires-higher-linux-version", "Для Wayland требуется более поздняя версия дистрибутива Linux. Используйте рабочий стол X11 или смените ОС."), - ("xdp-portal-unavailable", "Невозможно сделать снимок экрана Wayland. Возможно, в XDG Desktop Portal сбой или он недоступен. Попробуйте перезапустить его с помощью `systemctl --user restart xdg-desktop-portal`."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требуется Ubuntu версии 21.04 или новее."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland требуется более поздняя версия дистрибутива Linux. Используйте рабочий стол X11 или смените ОС."), ("JumpLink", "Просмотр"), ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для демонстрации (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), @@ -666,7 +665,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incoming Print Job", "Входящее задание печати"), ("use-the-default-printer-tip", "Использовать принтер по умолчанию"), ("use-the-selected-printer-tip", "Использовать выбранный принтер"), - ("auto-print-tip", "Автоматически выполнять печать на выбранном принтере"), + ("auto-print-tip", "Автоматически выполнять печать на выбранном принтере."), ("print-incoming-job-confirm-tip", "Получено задание на печать с удалённого устройства. Выполнить его локально?"), ("remote-printing-disallowed-tile-tip", "Удалённая печать запрещена"), ("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ("Continue with {}", "Продолжить с {}"), ("Display Name", "Отображаемое имя"), - ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), - ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), - ("Enable privacy mode", "Использовать режим конфиденциальности"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 06919b752..2eef86908 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Impostatziones de tecladu"), ("Full Access", "Atzessu cumpridu"), ("Screen Share", "Cumpartzidura de ischermu"), - ("ubuntu-21-04-required", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), - ("wayland-requires-higher-linux-version", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), ("JumpLink", "Bae a"), ("Please Select the screen to be shared(Operate on the peer side).", "Seletziona s'ischermu de cumpartzire (òpera dae s'ala de su dispositivu remotu)."), ("Show RustDesk", "Mustra RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Sighi cun {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 963f48728..0b45d7e12 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Nastavenia klávesnice"), ("Full Access", "Úplný prístup"), ("Screen Share", "Zdielanie obrazovky"), - ("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), - ("wayland-requires-higher-linux-version", "Wayland vyžaduje vyššiu verziu linuxovej distribúcie. Skúste X11 desktop alebo zmeňte OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyššiu verziu linuxovej distribúcie. Skúste X11 desktop alebo zmeňte OS."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), ("Show RustDesk", "Zobraziť RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovať s {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0f85af0c3..d8e22a3c4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Nastavitve tipkovnice"), ("Full Access", "Poln dostop"), ("Screen Share", "Deljenje zaslona"), - ("ubuntu-21-04-required", "Wayland zahteva Ubuntu 21.04 ali novejši"), - ("wayland-requires-higher-linux-version", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ali novejši"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), ("JumpLink", "Pogled"), ("Please Select the screen to be shared(Operate on the peer side).", "Izberite zaslon za delitev (na oddaljeni strani)."), ("Show RustDesk", "Prikaži RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nadaljuj z {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 7c965cd45..b7b7321ab 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Cilësimet e tastierës"), ("Full Access", "Qasje e plotë"), ("Screen Share", "Ndarja e ekranit"), - ("ubuntu-21-04-required", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), - ("wayland-requires-higher-linux-version", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Ju lutemi zgjidhni ekranin që do të ndahet (Vepro në anën e kolegëve"), ("Show RustDesk", "Shfaq RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Vazhdo me {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index fc33e4671..46cb14cdd 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Postavke tastature"), ("Full Access", "Pun pristup"), ("Screen Share", "Deljenje ekrana"), - ("ubuntu-21-04-required", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), - ("wayland-requires-higher-linux-version", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), ("JumpLink", "Vidi"), ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), ("Show RustDesk", "Prikazi RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 664dc4745..d2d1a3911 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tangentbordsinställningar"), ("Full Access", "Full tillgång"), ("Screen Share", "Skärmdelning"), - ("ubuntu-21-04-required", "Wayland kräver Ubuntu 21.04 eller högre."), - ("wayland-requires-higher-linux-version", "Wayland kräver en högre version av linux. Försök igen eller byt OS."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kräver Ubuntu 21.04 eller högre."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kräver en högre version av linux. Försök igen eller byt OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Välj skärm att dela"), ("Show RustDesk", "Visa RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsätt med {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 93aeb6462..7e3ae5cd0 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "விசைப்பலகை அமைப்புகள்"), ("Full Access", "முழு அணுகல்"), ("Screen Share", "திரை பகிர்வு"), - ("ubuntu-21-04-required", "Wayland க்கு Ubuntu 21.04+ தேவை"), - ("wayland-requires-higher-linux-version", "Wayland க்கு உயர் Linux பதிப்பு தேவை. X11 முயற்சிக்கவும் அல்லது OS மாற்றவும்."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland க்கு Ubuntu 21.04+ தேவை"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland க்கு உயர் Linux பதிப்பு தேவை. X11 முயற்சிக்கவும் அல்லது OS மாற்றவும்."), ("JumpLink", "ஜம்ப் லிங்க்"), ("Please Select the screen to be shared(Operate on the peer side).", "பகிரப்பட வேண்டிய திரை தேர்ந்தெடுக்கவும்"), ("Show RustDesk", "RustDesk ஐ காட்டு"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} உடன் தொடர்"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 33b359c5e..b21f64f14 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", ""), ("Full Access", ""), ("Screen Share", ""), - ("ubuntu-21-04-required", ""), - ("wayland-requires-higher-linux-version", ""), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", ""), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), ("JumpLink", ""), ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a24c60bf6..dbfc1096c 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), ("Full Access", "การเข้าถึงทั้งหมด"), ("Screen Share", "การแชร์จอ"), - ("ubuntu-21-04-required", "Wayland ต้องการ Ubuntu เวอร์ชัน 21.04 หรือสูงกว่า"), - ("wayland-requires-higher-linux-version", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชัน 21.04 หรือสูงกว่า"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), ("Show RustDesk", "แสดง RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ทำต่อด้วย {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index c28086cc9..e70d0a497 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Klavye Ayarları"), ("Full Access", "Tam Erişim"), ("Screen Share", "Ekran Paylaşımı"), - ("ubuntu-21-04-required", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), - ("wayland-requires-higher-linux-version", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), ("Show RustDesk", "RustDesk'i Göster"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), ("Display Name", "Görünen Ad"), - ("password-hidden-tip", "Parola gizli"), - ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), - ("Enable privacy mode", "Gizlilik modunu etkinleştir"), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6df025303..0e01fcde5 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "鍵盤設定"), ("Full Access", "完全存取"), ("Screen Share", "僅分享螢幕畫面"), - ("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更新的版本。"), - ("wayland-requires-higher-linux-version", "Wayland 需要更新版的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更新的版本。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更新版的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對方的裝置上操作)。"), ("Show RustDesk", "顯示 RustDesk"), @@ -740,10 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"), ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), ("Continue with {}", "使用 {} 登入"), - ("Display Name", "顯示名稱"), - ("password-hidden-tip", "固定密碼已設定(已隱藏)"), - ("preset-password-in-use-tip", "目前正在使用預設密碼"), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 7107bc261..b49b2e5ae 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Налаштування клавіатури"), ("Full Access", "Повний доступ"), ("Screen Share", "Демонстрація екрана"), - ("ubuntu-21-04-required", "Wayland потребує Ubuntu 21.04 або новішої версії."), - ("wayland-requires-higher-linux-version", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland потребує Ubuntu 21.04 або новішої версії."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), ("JumpLink", "Перегляд"), ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (на віддаленому пристрої)."), ("Show RustDesk", "Показати RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продовжити з {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 0910025ed..8f5888509 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -377,9 +377,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Cài đặt bàn phím"), ("Full Access", "Toàn quyền truy cập"), ("Screen Share", "Chia sẻ màn hình"), - ("ubuntu-21-04-required", "Wayland yêu cầu Ubuntu 21.04 trở lên."), - ("wayland-requires-higher-linux-version", "Wayland yêu cầu phiên bản Linux mới hơn. Hãy thử X11 hoặc đổi hệ điều hành."), - ("xdp-portal-unavailable", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu Ubuntu 21.04 trở lên."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland yêu cầu phiên bản Linux mới hơn. Hãy thử X11 hoặc đổi hệ điều hành."), ("JumpLink", "Xem"), ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng chọn màn hình chia sẻ (Thao tác ở phía đối tác)."), ("Show RustDesk", "Hiện RustDesk"), @@ -741,9 +740,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tiếp tục với {}"), ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), - ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 9a4bb37ec..9493e1cae 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -6,7 +6,7 @@ use hbb_common::{ anyhow::anyhow, bail, config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config}, - libc::{c_char, c_int, c_long, c_uint, c_ulong, c_void}, + libc::{c_char, c_int, c_long, c_uint, c_void}, log, message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, @@ -29,12 +29,6 @@ use wallpaper; pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; -#[derive(Clone, Debug)] -struct ActiveUserLookupCache { - uid: String, - username: String, -} - const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; @@ -56,8 +50,6 @@ lazy_static::lazy_static! { } } }; - static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex> = - std::sync::Mutex::new(None); // https://github.com/rustdesk/rustdesk/issues/13705 // Check if `sudo -E` actually preserves environment. // @@ -90,27 +82,6 @@ lazy_static::lazy_static! { }; } -#[inline] -fn update_active_user_lookup_cache(desktop: &Desktop) { - if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() { - if desktop.uid.is_empty() || desktop.username.is_empty() { - *cache = None; - } else { - *cache = Some(ActiveUserLookupCache { - uid: desktop.uid.clone(), - username: desktop.username.clone(), - }); - } - } -} - -#[inline] -fn get_active_user_id_name_from_cache() -> Option<(String, String)> { - let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?; - let entry = cache.as_ref()?; - Some((entry.uid.clone(), entry.username.clone())) -} - thread_local! { // XDO context - created via libxdo-sys (which uses dynamic loading stub). // If libxdo is not available, xdo will be null and xdo-based functions become no-ops. @@ -126,55 +97,10 @@ thread_local! { static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); } -// X11 error event structure for the custom error handler. -// See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers -#[repr(C)] -struct XErrorEvent { - type_: c_int, - display: *mut c_void, // Display* - resourceid: c_ulong, // XID - serial: c_ulong, - error_code: u8, - request_code: u8, - minor_code: u8, -} - -type XErrorHandler = unsafe extern "C" fn(*mut c_void, *mut XErrorEvent) -> c_int; - -const X11_BAD_WINDOW: u8 = 3; -const XDO_SUCCESS: c_int = 0; -const XDO_ERROR: c_int = 1; - -/// Atomic flag set by the custom X error handler when a BadWindow error occurs. -static X_BAD_WINDOW_DETECTED: AtomicBool = AtomicBool::new(false); -static X_UNEXPECTED_ERROR_DETECTED: AtomicBool = AtomicBool::new(false); - -/// Custom X error handler that catches BadWindow errors (error_code == 3) instead of -/// letting the default handler terminate the process. -/// See issue: https://github.com/rustdesk/rustdesk/issues/9003 -unsafe extern "C" fn handle_x_error(_display: *mut c_void, event: *mut XErrorEvent) -> c_int { - if !event.is_null() && (*event).error_code == X11_BAD_WINDOW { - X_BAD_WINDOW_DETECTED.store(true, Ordering::SeqCst); - log::debug!("Caught X11 BadWindow error (suppressed), window was likely destroyed"); - return 0; - } - X_UNEXPECTED_ERROR_DETECTED.store(true, Ordering::SeqCst); - if !event.is_null() { - log::warn!( - "X11 error: error_code={}, request_code={}, minor_code={}", - (*event).error_code, - (*event).request_code, - (*event).minor_code, - ); - } - 0 -} - #[link(name = "X11")] extern "C" { fn XOpenDisplay(display_name: *const c_char) -> *mut c_void; // fn XCloseDisplay(d: *mut c_void) -> c_int; - fn XSetErrorHandler(handler: Option) -> Option; } #[link(name = "Xfixes")] @@ -305,47 +231,25 @@ pub fn get_focused_display(displays: Vec) -> Option { if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 { return; } - - // XSetErrorHandler is process-global, not scoped to this Display/thread. - // This path is currently called by the single window_focus service thread. - // While installed, this handler can still observe unrelated X11 errors from - // other threads; unexpected errors make this geometry query fail. - X_BAD_WINDOW_DETECTED.store(false, Ordering::SeqCst); - X_UNEXPECTED_ERROR_DETECTED.store(false, Ordering::SeqCst); - let prev_handler = XSetErrorHandler(Some(handle_x_error)); - - let loc_ret = libxdo_sys::xdo_get_window_location( + if libxdo_sys::xdo_get_window_location( *xdo as *const _, window, &mut x as _, &mut y as _, std::ptr::null_mut(), - ); - let size_ret = if loc_ret == XDO_SUCCESS { - libxdo_sys::xdo_get_window_size( - *xdo as *const _, - window, - &mut width, - &mut height, - ) - } else { - XDO_ERROR - }; - - // Do not call XSync(DISPLAY) here: DISPLAY is a separate - // XOpenDisplay() connection, while libxdo owns the Display* - // used by these geometry queries. These libxdo calls are - // synchronous XGetWindowAttributes-based queries, so the target - // BadWindow is expected to be delivered before the calls return. - XSetErrorHandler(prev_handler); - if X_BAD_WINDOW_DETECTED.load(Ordering::SeqCst) - || X_UNEXPECTED_ERROR_DETECTED.load(Ordering::SeqCst) - || loc_ret != XDO_SUCCESS - || size_ret != XDO_SUCCESS + ) != 0 + { + return; + } + if libxdo_sys::xdo_get_window_size( + *xdo as *const _, + window, + &mut width, + &mut height, + ) != 0 { return; } - let center_x = x + (width / 2) as c_int; let center_y = y + (height / 2) as c_int; res = displays.iter().position(|d| { @@ -818,7 +722,6 @@ pub fn start_os_service() { let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { desktop.refresh(); - update_active_user_lookup_cache(&desktop); // Duplicate logic here with should_start_server // Login wayland will try to start a headless --server. @@ -891,29 +794,13 @@ pub fn start_os_service() { } #[inline] -/// Returns the cached active `(uid, username)` snapshot when available. -/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_user_id_name() -> (String, String) { - if let Some(id_name) = get_active_user_id_name_from_cache() { - return id_name; - } let vec_id_name = get_values_of_seat0(&[1, 2]); (vec_id_name[0].clone(), vec_id_name[1].clone()) } #[inline] -/// Returns the cached active uid when available. -/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_userid() -> String { - if let Some((uid, _)) = get_active_user_id_name_from_cache() { - return uid; - } - get_values_of_seat0(&[1])[0].clone() -} - -#[inline] -/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache. -pub fn get_active_userid_fresh() -> String { get_values_of_seat0(&[1])[0].clone() } @@ -968,12 +855,7 @@ fn _get_display_manager() -> String { } #[inline] -/// Returns the cached active username when available. -/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_username() -> String { - if let Some((_, username)) = get_active_user_id_name_from_cache() { - return username; - } get_values_of_seat0(&[2])[0].clone() } @@ -2268,10 +2150,7 @@ pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> { || err_name == "org.freedesktop.DBus.Error.UnknownObject" || err_name == "org.freedesktop.DBus.Error.ServiceUnknown" { - log::info!( - "GNOME shortcuts inhibitor permission was not set ({})", - err_name - ); + log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name); Ok(()) } else { bail!("Failed to clear permission: {}", e) diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index 0a512939b..03f1f6250 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -2,7 +2,7 @@ use super::{linux::*, ResultType}; use crate::client::{ LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, - LOGIN_MSG_DESKTOP_XSESSION_FAILED, LOGIN_MSG_PASSWORD_WRONG, + LOGIN_MSG_DESKTOP_XSESSION_FAILED, }; use hbb_common::{ allow_err, bail, log, @@ -94,49 +94,6 @@ fn detect_headless() -> Option<&'static str> { None } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum XSessionStartErrorKind { - Auth, - Env, -} - -const XSESSION_AUTH_FAILURE_DETAIL: &str = "authentication failed"; - -#[derive(Debug)] -struct XSessionStartError { - kind: XSessionStartErrorKind, - detail: String, -} - -impl XSessionStartError { - fn auth(detail: String) -> Self { - Self { - kind: XSessionStartErrorKind::Auth, - detail, - } - } - - fn env(detail: String) -> Self { - Self { - kind: XSessionStartErrorKind::Env, - detail, - } - } -} - -impl std::fmt::Display for XSessionStartError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.detail) - } -} - -fn map_xsession_start_error_to_login_msg(kind: XSessionStartErrorKind) -> &'static str { - match kind { - XSessionStartErrorKind::Auth => LOGIN_MSG_PASSWORD_WRONG, - XSessionStartErrorKind::Env => LOGIN_MSG_DESKTOP_XSESSION_FAILED, - } -} - pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { debug_assert!(crate::is_server()); if _username.is_empty() { @@ -179,21 +136,14 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { } } Err(e) => { - match e.kind { - XSessionStartErrorKind::Auth => { - log::warn!("Failed to authenticate xsession user {}", e); - } - XSessionStartErrorKind::Env => { - log::error!("Failed to start xsession {}", e); - } - } - map_xsession_start_error_to_login_msg(e.kind).to_owned() + log::error!("Failed to start xsession {}", e); + LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned() } } } } -fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), XSessionStartError> { +fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> { let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); if let Some(desktop_manager) = &mut (*desktop_manager) { if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() { @@ -211,9 +161,7 @@ fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), desktop_manager.is_running(), )) } else { - Err(XSessionStartError::env( - crate::client::LOGIN_MSG_DESKTOP_NOT_INITED.to_owned(), - )) + bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED); } } @@ -299,15 +247,10 @@ impl DesktopManager { self.is_child_running.load(Ordering::SeqCst) } - fn try_start_x_session( - &mut self, - username: &str, - password: &str, - ) -> Result<(), XSessionStartError> { + fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> { match get_user_by_name(username) { Some(userinfo) => { - let mut client = pam::Client::with_password(&pam_get_service_name()) - .map_err(|e| XSessionStartError::env(format!("failed to init pam client, {}", e)))?; + let mut client = pam::Client::with_password(&pam_get_service_name())?; client .conversation_mut() .set_credentials(username, password); @@ -324,24 +267,17 @@ impl DesktopManager { Ok(()) } Err(e) => { - Err(XSessionStartError::env(format!( - "failed to start x session, {}", - e - ))) + bail!("failed to start x session, {}", e); } } } - Err(_e) => { - Err(XSessionStartError::auth( - XSESSION_AUTH_FAILURE_DETAIL.to_owned(), - )) + Err(e) => { + bail!("failed to check user pass for {}, {}", username, e); } } } None => { - Err(XSessionStartError::auth( - XSESSION_AUTH_FAILURE_DETAIL.to_owned(), - )) + bail!("failed to get userinfo of {}", username); } } } diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 9027d9d89..74c20c80d 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -580,8 +580,9 @@ extern "C" return rdp_or_console; } - BOOL is_session_locked(DWORD session_id) + BOOL is_session_locked(BOOL include_rdp) { + DWORD session_id = get_current_session(include_rdp); if (session_id == 0xFFFFFFFF) { return FALSE; } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 1dc4a788a..ee8aa7c6f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -73,19 +73,10 @@ use winapi::{ }; use windows::Win32::{ Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE}, - Security::{ - GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser, - WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER, - }, System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, }, - System::Threading::{ - OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken, - QueryFullProcessImageNameW as WinQueryFullProcessImageNameW, - PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION, - }, }; use windows_service::{ define_windows_service, @@ -97,14 +88,6 @@ use windows_service::{ }; use winreg::{enums::*, RegKey}; -mod acl; -pub(crate) use acl::current_process_user_sid_string; -pub use acl::{ - set_path_permission, set_path_permission_for_portable_service_shmem_dir, - set_path_permission_for_portable_service_shmem_file, - validate_path_for_portable_service_shmem_dir, -}; - pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window pub const EXPLORER_EXE: &'static str = "explorer.exe"; pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; @@ -540,7 +523,7 @@ const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; extern "C" { fn get_current_session(rdp: BOOL) -> DWORD; - fn is_session_locked(session_id: DWORD) -> BOOL; + fn is_session_locked(include_rdp: BOOL) -> BOOL; fn LaunchProcessWin( cmd: *const u16, session_id: DWORD, @@ -582,56 +565,6 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD { unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } } -#[inline] -fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option { - let share_rdp_enabled = is_share_rdp(); - if get_available_sessions(false) - .iter() - .any(|e| e.sid == session_id) - { - return Some(session_id); - } - let current_active_session = - unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) }; - if current_active_session == u32::MAX { - None - } else { - Some(current_active_session) - } -} - -#[inline] -fn authorize_service_scoped_ipc_connection( - stream: &ipc::Connection, - expected_active_session_id: Option, -) -> bool { - let (authorized, peer_pid, peer_session_id, peer_is_system) = - stream.service_authorization_status_for_session(expected_active_session_id); - if !authorized { - ipc::log_rejected_windows_ipc_connection( - crate::POSTFIX_SERVICE, - peer_pid, - peer_session_id, - expected_active_session_id, - peer_is_system, - None, - ); - return false; - } - if let Err(err) = - ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE) - { - log::warn!( - "Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", - crate::POSTFIX_SERVICE, - peer_pid, - err - ); - return false; - } - true -} - extern "system" { fn BlockInput(v: BOOL) -> BOOL; } @@ -698,15 +631,6 @@ async fn run_service(_arguments: Vec) -> ResultType<()> { Ok(res) => match res { Some(Ok(stream)) => { let mut stream = ipc::Connection::new(stream); - // Keep IPC authorization consistent with the session we are currently serving. - // Recompute expected session right before authorization to avoid using a stale - // session_id after awaiting incoming.next(). - let expected_active_session_id = - resolve_expected_active_session_id_for_service(session_id); - if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id) - { - continue; - } if let Ok(Some(data)) = stream.next_timeout(1000).await { match data { ipc::Data::Close => { @@ -1217,22 +1141,6 @@ pub fn get_active_user_home() -> Option { None } -#[cfg(not(feature = "flutter"))] -#[inline] -pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> { - // Keep parity with history for now: derive LocalAppData from user profile path. - // If users report redirected/non-standard LocalAppData issues, switch to: - // `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution. - let user_dir = hbb_common::directories_next::UserDirs::new()?; - let dir = user_dir - .home_dir() - .join("AppData") - .join("Local") - .join("rustdesk-sciter"); - let dst = dir.join("rustdesk.exe"); - Some((dir, dst)) -} - pub fn is_prelogin() -> bool { let Some(username) = get_current_session_username() else { return false; @@ -1241,21 +1149,20 @@ pub fn is_prelogin() -> bool { } pub fn is_locked() -> bool { - let Some(session_id) = get_current_process_session_id() else { - return false; - }; - unsafe { is_session_locked(session_id) == TRUE } + unsafe { is_session_locked(share_rdp()) == TRUE } } +// `is_logon_ui()` is regardless of multiple sessions now. +// It only check if "LogonUI.exe" exists. +// +// If there're mulitple sessions (logged in users), +// some are in the login screen, while the others are not. +// Then this function may not work fine if the session we want to handle(connect) is not in the login screen. +// But it's a rare case and cannot be simply handled, so it will not be dealt with for the time being. #[inline] pub fn is_logon_ui() -> ResultType { - let Some(current_sid) = get_current_process_session_id() else { - return Ok(false); - }; let pids = get_pids("LogonUI.exe")?; - Ok(pids - .into_iter() - .any(|pid| get_session_id_of_process(pid) == Some(current_sid))) + Ok(!pids.is_empty()) } pub fn is_root() -> bool { @@ -1564,7 +1471,7 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); let cur_exe = current_exe.to_str().unwrap_or("").to_owned(); - let shortcut_icon_location = get_shortcut_icon_location(&path, &cur_exe); + let shortcut_icon_location = get_shortcut_icon_location(&cur_exe); let mk_shortcut = write_cmds( format!( " @@ -1602,7 +1509,7 @@ oLink.Save .to_str() .unwrap_or("") .to_owned(); - let tray_shortcut = get_tray_shortcut(&path, &exe, &cur_exe, &tmp_path)?; + let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?; let mut reg_value_desktop_shortcuts = "0".to_owned(); let mut reg_value_start_menu_shortcuts = "0".to_owned(); let mut reg_value_printer = "0".to_owned(); @@ -1713,7 +1620,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" {install_remote_printer} {sleep} ", - display_icon = get_custom_icon(&path, &cur_exe).unwrap_or(exe.to_string()), + display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()), version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, after_install = get_after_install( @@ -2122,9 +2029,6 @@ pub fn update_install_option(k: &str, v: &str) -> ResultType<()> { if !is_installed() || !crate::is_server() { return Ok(()); } - if ![REG_NAME_INSTALL_PRINTER].contains(&k) || !["0", "1"].contains(&v) { - return Ok(()); - } let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); let cmds = @@ -2217,16 +2121,12 @@ unsafe fn set_default_dll_directories() -> bool { true } -fn get_custom_icon(install_dir: &str, exe: &str) -> Option { - const RELATIVE_ICON_PATH: &str = "data\\flutter_assets\\assets\\icon.ico"; +fn get_custom_icon(exe: &str) -> Option { if crate::is_custom_client() { if let Some(p) = PathBuf::from(exe).parent() { - let alter_icon_path = p.join(RELATIVE_ICON_PATH); + let alter_icon_path = p.join("data\\flutter_assets\\assets\\icon.ico"); if alter_icon_path.exists() { - // During installation, files under `install_dir` may not exist yet. - // So we validate the icon from the current executable directory first. - // But for shortcut/registry icon location, we should point to the final - // installed path so the icon works across different Windows users. + // Verify that the icon is not a symlink for security if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) { if metadata.is_symlink() { log::warn!( @@ -2236,11 +2136,7 @@ fn get_custom_icon(install_dir: &str, exe: &str) -> Option { return None; } if metadata.is_file() { - return if install_dir.is_empty() { - Some(alter_icon_path.to_string_lossy().to_string()) - } else { - Some(format!("{}\\{}", install_dir, RELATIVE_ICON_PATH)) - }; + return Some(alter_icon_path.to_string_lossy().to_string()); } } } @@ -2250,12 +2146,12 @@ fn get_custom_icon(install_dir: &str, exe: &str) -> Option { } #[inline] -fn get_shortcut_icon_location(install_dir: &str, exe: &str) -> String { +fn get_shortcut_icon_location(exe: &str) -> String { if exe.is_empty() { return "".to_owned(); } - get_custom_icon(install_dir, exe) + get_custom_icon(exe) .map(|p| format!("oLink.IconLocation = \"{}\"", p)) .unwrap_or_default() } @@ -2266,7 +2162,7 @@ pub fn create_shortcut(id: &str) -> ResultType<()> { // Replace ':' with '_' for filename since ':' is not allowed in Windows filenames // https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384 let filename = id.replace(':', "_"); - let shortcut_icon_location = get_shortcut_icon_location("", &exe); + let shortcut_icon_location = get_shortcut_icon_location(&exe); let shortcut = write_cmds( format!( " @@ -2419,33 +2315,16 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst is_run_as_system, crate::username(), ); - let mut arg_elevate = if is_setup { + let arg_elevate = if is_setup { "--noinstall --elevate" } else { "--elevate" - } - .to_owned(); - let mut arg_run_as_system = if is_setup { + }; + let arg_run_as_system = if is_setup { "--noinstall --run-as-system" } else { "--run-as-system" - } - .to_owned(); - let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args(); - if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() { - log::error!("Invalid portable service shared memory argument, aborting elevation flow"); - // This is a malformed bootstrap argument in a privilege-sensitive path. - // Keep fail-closed process termination here to avoid continuing elevation - // with inconsistent shared-memory contract. - std::process::exit(1); - } - if let Some(shmem_name) = shmem_name_from_args { - let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name); - arg_elevate.push(' '); - arg_elevate.push_str(&shmem_arg); - arg_run_as_system.push(' '); - arg_run_as_system.push_str(&shmem_arg); - } + }; if is_root() { if is_run_as_system { log::info!("run portable service"); @@ -2456,7 +2335,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst Ok(elevated) => { if elevated { if !is_run_as_system { - if run_as_system(arg_run_as_system.as_str()).is_ok() { + if run_as_system(arg_run_as_system).is_ok() { std::process::exit(0); } else { log::error!( @@ -2467,7 +2346,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst } } else { if !is_elevate { - if let Ok(true) = elevate(arg_elevate.as_str()) { + if let Ok(true) = elevate(arg_elevate) { std::process::exit(0); } else { log::error!("Failed to elevate, error {}", io::Error::last_os_error()); @@ -2525,115 +2404,6 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } -#[inline] -unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType> { - let mut token_user_size = 0u32; - let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size); - match get_info_result { - Ok(()) => { - if token_user_size == 0 { - bail!( - "Failed to get {} token user size: unexpected zero buffer size", - subject - ); - } - } - Err(e) => { - // Allow expected size-probe failures if Windows still returns required size. - let is_insufficient_buffer = - e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32); - let is_bad_length = - e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32); - if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 { - bail!("Failed to get {} token user size: {}", subject, e); - } - } - } - - let mut buffer = vec![0u8; token_user_size as usize]; - WinGetTokenInformation( - token, - TokenUser, - Some(buffer.as_mut_ptr() as *mut core::ffi::c_void), - token_user_size, - &mut token_user_size, - ) - .map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?; - - let min_size = std::mem::size_of::(); - if buffer.len() < min_size { - bail!( - "Failed to parse {} token user: buffer too small (got {}, need >= {})", - subject, - buffer.len(), - min_size - ); - } - Ok(buffer) -} - -/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process. -/// -/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18). -/// -/// TODO: After a few releases of real-world validation, consider replacing -/// the legacy `is_local_system()` with this implementation. -pub fn is_process_running_as_system(process_id: DWORD) -> ResultType { - unsafe { - let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) - .map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?; - - let mut token = WinHANDLE::default(); - let result = (|| -> ResultType { - WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token) - .map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?; - - let token_subject = format!("process {}", process_id); - let buffer = read_token_user_buffer(token, token_subject.as_str())?; - let token_user: TOKEN_USER = - std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER); - Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool()) - })(); - - if !token.is_invalid() { - let _ = WinCloseHandle(token); - } - let _ = WinCloseHandle(process); - result - } -} - -pub fn get_process_executable_path(process_id: DWORD) -> ResultType { - const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024; - unsafe { - let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) - .map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?; - - let result = (|| -> ResultType { - let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN]; - let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32; - WinQueryFullProcessImageNameW( - process, - windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0), - windows::core::PWSTR(buffer.as_mut_ptr()), - &mut length, - ) - .map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?; - if length == 0 { - bail!( - "Failed to query process {} image path: empty result", - process_id - ); - } - buffer.truncate(length as usize); - Ok(PathBuf::from(OsString::from_wide(&buffer))) - })(); - - let _ = WinCloseHandle(process); - result - } -} - pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -2926,6 +2696,16 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> return Ok(()); } +pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> { + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg(format!("*S-1-1-0:(OI)(CI){}", permission)) + .arg("/T") + .spawn()?; + Ok(()) +} + #[inline] fn str_to_device_name(name: &str) -> [u16; 32] { let mut device_name: Vec = wide_string(name); @@ -3169,9 +2949,9 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { pub fn install_service() -> bool { log::info!("Installing service..."); let _installing = crate::platform::InstallingService::new(); - let (_, path, _, exe) = get_install_info(); + let (_, _, _, exe) = get_install_info(); let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); - let tray_shortcut = get_tray_shortcut(&path, &exe, &exe, &tmp_path).unwrap_or_default(); + let tray_shortcut = get_tray_shortcut(&exe, &tmp_path).unwrap_or_default(); let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); Config::set_option("stop-service".into(), "".into()); crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed); @@ -3280,8 +3060,7 @@ pub fn update_me(debug: bool) -> ResultType<()> { let version = crate::VERSION.replace("-", "."); let size = get_directory_size_kb(&path); let build_date = crate::BUILD_DATE; - // Use the icon in the previous installation directory if possible. - let display_icon = get_custom_icon("", &exe).unwrap_or(exe.to_string()); + let display_icon = get_custom_icon(&exe).unwrap_or(exe.to_string()); let is_msi = is_msi_installed().ok(); @@ -3638,13 +3417,8 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> { Ok(()) } -pub fn get_tray_shortcut( - install_dir: &str, - exe: &str, - icon_source_exe: &str, - tmp_path: &str, -) -> ResultType { - let shortcut_icon_location = get_shortcut_icon_location(install_dir, icon_source_exe); +pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType { + let shortcut_icon_location = get_shortcut_icon_location(exe); Ok(write_cmds( format!( " @@ -4489,87 +4263,6 @@ pub(super) fn get_pids_with_first_arg_by_wmic, S2: AsRef>( #[cfg(test)] mod tests { use super::*; - - // Test-only reusable Win32 HANDLE RAII helper. - // If a future non-test path needs the same pattern, move it out of this test module. - // - // This struct is similar to `hbb_common::platform::windows::RAIIHandle`, - // but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate. - struct HandleGuard(WinHANDLE); - - impl HandleGuard { - #[inline] - fn new(handle: WinHANDLE) -> Self { - Self(handle) - } - - #[inline] - fn get(&self) -> WinHANDLE { - self.0 - } - } - - impl Drop for HandleGuard { - fn drop(&mut self) { - unsafe { - if !self.0.is_invalid() { - let _ = WinCloseHandle(self.0); - } - } - } - } - - #[test] - fn test_is_process_running_as_system_invalid_pid_errors() { - assert!(is_process_running_as_system(u32::MAX).is_err()); - } - - #[test] - fn test_is_process_running_as_system_matches_current_process_token_user() { - let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() }; - let actual = is_process_running_as_system(pid).unwrap(); - - let expected = unsafe { - // Keep this test consistent: use only the `windows` crate APIs/types. - let process = HandleGuard::new( - WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid) - .expect("WinOpenProcess should succeed for current process"), - ); - let mut token = WinHANDLE::default(); - WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token) - .expect("WinOpenProcessToken should succeed for current process"); - let token = HandleGuard::new(token); - - let mut token_user_size = 0u32; - let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size); - assert_ne!(token_user_size, 0, "TokenUser size should be non-zero"); - - let mut buffer = vec![0u8; token_user_size as usize]; - WinGetTokenInformation( - token.get(), - TokenUser, - Some(buffer.as_mut_ptr() as *mut core::ffi::c_void), - token_user_size, - &mut token_user_size, - ) - .expect("WinGetTokenInformation(TokenUser) should succeed for current process"); - - let min_size = std::mem::size_of::(); - assert!( - buffer.len() >= min_size, - "TokenUser buffer too small (got {}, need >= {})", - buffer.len(), - min_size - ); - let token_user: TOKEN_USER = - std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER); - let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool(); - expected - }; - - assert_eq!(actual, expected); - } - #[test] fn test_uninstall_cert() { println!("uninstall driver certs: {:?}", cert::uninstall_cert()); diff --git a/src/platform/windows/acl.rs b/src/platform/windows/acl.rs deleted file mode 100644 index 682e66fed..000000000 --- a/src/platform/windows/acl.rs +++ /dev/null @@ -1,903 +0,0 @@ -// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary - -use super::{read_token_user_buffer, wide_string, ResultType}; -use hbb_common::{anyhow::anyhow, bail}; -use std::{ - fs, io, - os::windows::{ffi::OsStrExt, fs::MetadataExt}, - path::Path, -}; -use windows::{ - core::{PCWSTR, PWSTR}, - Win32::{ - Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL}, - Security::{ - Authorization::{ - ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW, - SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS, - SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W, - }, - ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE, - OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, - TOKEN_QUERY, TOKEN_USER, - }, - Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE}, - System::Threading::{GetCurrentProcess, OpenProcessToken}, - }, -}; - -const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400; - -#[inline] -fn is_reparse_point(metadata: &fs::Metadata) -> bool { - (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0 -} - -fn apply_grant_sid_allow_ace_to_path( - path: &Path, - sid_ptr: *mut std::ffi::c_void, - access_mask: u32, - is_group: bool, - is_dir: bool, -) -> ResultType<()> { - // Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW. - // https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c-- - let mut old_dacl: *mut ACL = std::ptr::null_mut(); - let mut security_descriptor = PSECURITY_DESCRIPTOR::default(); - let path_utf16: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let get_named_result = unsafe { - GetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(&mut old_dacl), - None, - &mut security_descriptor, - ) - }; - if get_named_result.0 != 0 { - bail!( - "GetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - get_named_result.0 - ); - } - let _sd_guard = LocalAllocGuard(security_descriptor.0); - - let inherit_flags = if is_dir { - ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0) - } else { - NO_INHERITANCE - }; - let explicit_access = [make_sid_trustee_entry( - sid_ptr, - access_mask, - inherit_flags, - is_group, - )]; - let old_acl_option = if old_dacl.is_null() { - None - } else { - Some(old_dacl as *const ACL) - }; - let mut new_acl: *mut ACL = std::ptr::null_mut(); - let set_entries_result = unsafe { - SetEntriesInAclW( - Some(explicit_access.as_slice()), - old_acl_option, - &mut new_acl, - ) - }; - if set_entries_result.0 != 0 { - bail!( - "SetEntriesInAclW failed for '{}': win32_error={}", - path.display(), - set_entries_result.0 - ); - } - if new_acl.is_null() { - bail!( - "SetEntriesInAclW returned null ACL for '{}'", - path.display() - ); - } - let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void); - - let set_named_result = unsafe { - SetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(new_acl), - None, - ) - }; - if set_named_result.0 != 0 { - bail!( - "SetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - set_named_result.0 - ); - } - Ok(()) -} - -/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be -/// readable/executable across user contexts. -/// -/// `access_mask` is the Win32 file access mask to grant recursively. -pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> { - let metadata = fs::symlink_metadata(dir).map_err(|e| { - anyhow!( - "Failed to inspect ACL target directory '{}': {}", - dir.display(), - e - ) - })?; - if is_reparse_point(&metadata) { - bail!( - "ACL target directory is a reparse point and is rejected: '{}'", - dir.display() - ); - } - if !metadata.file_type().is_dir() { - bail!("ACL target is not a directory: '{}'", dir.display()); - } - - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?; - let mut stack = vec![dir.to_path_buf()]; - while let Some(path) = stack.pop() { - let metadata = fs::symlink_metadata(&path) - .map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?; - if is_reparse_point(&metadata) { - continue; - } - let is_dir = metadata.file_type().is_dir(); - apply_grant_sid_allow_ace_to_path( - &path, - everyone_sid.as_sid_ptr(), - access_mask, - true, - is_dir, - )?; - if !is_dir { - continue; - } - for entry in fs::read_dir(&path) - .map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))? - { - let entry = entry.map_err(|e| { - anyhow!( - "Failed to read ACL target dir entry under '{}': {}", - path.display(), - e - ) - })?; - stack.push(entry.path()); - } - } - Ok(()) -} - -/// Returns the current process user SID as a standard SID string -/// (for example: `S-1-5-18`). -/// -/// Source: -/// - Official SID-to-string API (`ConvertSidToStringSidW`): -/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw -pub(crate) fn current_process_user_sid_string() -> ResultType { - let mut token = HANDLE::default(); - let result = (|| -> ResultType { - unsafe { - OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) - .map_err(|e| anyhow!("Failed to open current process token: {}", e))?; - } - - let buffer = unsafe { read_token_user_buffer(token, "current process")? }; - let token_user: TOKEN_USER = - unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) }; - if token_user.User.Sid.0.is_null() { - bail!("Token SID is null"); - } - - let mut sid_string_ptr = PWSTR::null(); - unsafe { - ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| { - anyhow!( - "ConvertSidToStringSidW failed for current process token SID: {}", - e - ) - })?; - } - if sid_string_ptr.is_null() { - bail!("ConvertSidToStringSidW returned null SID string pointer"); - } - let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void); - unsafe { - sid_string_ptr - .to_string() - .map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e)) - } - })(); - - if !token.is_invalid() { - unsafe { - let _ = CloseHandle(token); - } - } - result -} - -/// Hardens ACLs for portable-service shared-memory path (directory or file). -/// -/// Why: -/// - Shared memory used by portable service carries runtime control/data and must not inherit -/// broad/default ACLs. -/// - We explicitly grant only trusted principals and remove broad groups to reduce local -/// privilege-boundary bypass risk. -/// -/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`): -/// - common (directory + file): -/// - `S-1-5-18` (LocalSystem): full control -/// - `S-1-5-32-544` (Built-in Administrators): full control -/// - `current_process_user_sid_string()` result: full control -/// - directory (`portable_service_shmem` parent): -/// - keep `Authenticated Users` directory-level write so other local accounts can -/// create their own runtime shmem files after account switching -/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself; -/// it is intentionally not inherited by children. -/// Reference: -/// - File access rights: -/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants -/// - ACE inheritance rules: -/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules -/// - remove `Everyone` and `Users` grants -/// - file (`shared_memory*` flink): -/// - remove broad grants: -/// - `S-1-1-0` (Everyone) -/// - `S-1-5-11` (Authenticated Users) -/// - `S-1-5-32-545` (Users) -/// -/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids -pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> { - set_path_permission_for_portable_service_shmem_impl(path, true) -} - -#[inline] -pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> { - validate_portable_service_shmem_dir_target(path) -} - -#[inline] -pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> { - set_path_permission_for_portable_service_shmem_impl(path, false) -} - -#[derive(Debug)] -pub(super) struct LocalAllocGuard(*mut std::ffi::c_void); - -impl LocalAllocGuard { - #[inline] - pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void { - self.0 - } -} - -impl Drop for LocalAllocGuard { - fn drop(&mut self) { - if self.0.is_null() { - return; - } - // Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW / - // ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed. - unsafe { - let _ = LocalFree(Some(HLOCAL(self.0))); - } - } -} - -#[inline] -pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType { - let sid_utf16 = wide_string(sid); - let mut sid_ptr = PSID::default(); - unsafe { - ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr) - .map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?; - } - if sid_ptr.0.is_null() { - bail!("ConvertStringSidToSidW returned null SID for '{}'", sid); - } - Ok(LocalAllocGuard(sid_ptr.0)) -} - -#[inline] -fn make_sid_trustee_entry( - sid_ptr: *mut std::ffi::c_void, - access_permissions: u32, - inheritance: ACE_FLAGS, - is_group: bool, -) -> EXPLICIT_ACCESS_W { - // `is_group` is explicitly provided by the caller from the concrete SID semantic - // (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user). - EXPLICIT_ACCESS_W { - grfAccessPermissions: access_permissions, - grfAccessMode: SET_ACCESS, - grfInheritance: inheritance, - Trustee: TRUSTEE_W { - pMultipleTrustee: std::ptr::null_mut(), - MultipleTrusteeOperation: Default::default(), - TrusteeForm: TRUSTEE_IS_SID, - TrusteeType: if is_group { - TRUSTEE_IS_GROUP - } else { - TRUSTEE_IS_USER - }, - // SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID. - ptstrName: PWSTR::from_raw(sid_ptr as *mut u16), - }, - } -} - -fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> { - let metadata = fs::symlink_metadata(path).map_err(|e| { - anyhow!( - "Failed to inspect portable service shared-memory ACL directory '{}': {}", - path.display(), - e - ) - })?; - if is_reparse_point(&metadata) { - bail!( - "Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'", - path.display() - ); - } - if !metadata.file_type().is_dir() { - bail!( - "Portable service shared-memory ACL target is not a directory: '{}'", - path.display() - ); - } - Ok(()) -} - -fn set_path_permission_for_portable_service_shmem_impl( - path: &Path, - expect_dir: bool, -) -> ResultType<()> { - if expect_dir { - validate_portable_service_shmem_dir_target(path)?; - } else { - let metadata_result = fs::symlink_metadata(path); - match metadata_result { - Ok(metadata) => { - if metadata.file_type().is_dir() { - bail!( - "Portable service shared-memory ACL target is a directory, expected file-like path: '{}'", - path.display() - ); - } - if is_reparse_point(&metadata) { - bail!( - "Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'", - path.display() - ); - } - } - Err(e) - if e.kind() == io::ErrorKind::NotFound - || e.kind() == io::ErrorKind::PermissionDenied => - { - // Keep going and let Win32 ACL APIs return the final OS error. - // `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into - // a false "not found" signal under restricted directory ACLs. - } - Err(e) => { - bail!( - "Failed to inspect portable service shared-memory ACL target '{}': {}", - path.display(), - e - ); - } - } - } - - let user_sid = current_process_user_sid_string()?; - let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?; - let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?; - let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?; - let authenticated_users_sid = if expect_dir { - Some(sid_string_to_local_alloc_guard("S-1-5-11")?) - } else { - None - }; - - let inherit_flags = if expect_dir { - ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0) - } else { - NO_INHERITANCE - }; - let mut entries = vec![ - make_sid_trustee_entry( - local_system_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0, - inherit_flags, - false, - ), - make_sid_trustee_entry( - administrators_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0, - inherit_flags, - true, - ), - make_sid_trustee_entry( - current_user_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0, - inherit_flags, - false, - ), - ]; - if let Some(auth_sid) = authenticated_users_sid.as_ref() { - // Keep the shared parent directory multi-user writable at directory level. - entries.push(make_sid_trustee_entry( - auth_sid.as_sid_ptr(), - FILE_GENERIC_WRITE.0, - NO_INHERITANCE, - true, - )); - } - - // Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected. - // This avoids carrying over broad legacy ACEs from inherited/default ACLs. - // Reference: - // - SetEntriesInAclW: - // https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw - // - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION): - // https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow - let mut new_acl: *mut ACL = std::ptr::null_mut(); - let set_entries_result = - unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) }; - if set_entries_result.0 != 0 { - bail!( - "SetEntriesInAclW failed for '{}': win32_error={}", - path.display(), - set_entries_result.0 - ); - } - if new_acl.is_null() { - bail!( - "SetEntriesInAclW returned null ACL for '{}'", - path.display() - ); - } - let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void); - - let path_utf16: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; - let set_named_result = unsafe { - SetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - security_info, - None, - None, - Some(new_acl), - None, - ) - }; - if set_named_result.0 != 0 { - bail!( - "SetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - set_named_result.0 - ); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::{ - current_process_user_sid_string, set_path_permission, - set_path_permission_for_portable_service_shmem_dir, - set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard, - LocalAllocGuard, ResultType, - }; - use hbb_common::bail; - use std::{ - fs, - os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file}, - path::{Path, PathBuf}, - }; - use windows::{ - core::PCWSTR, - Win32::{ - Security::{ - AclSizeInformation, - Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT}, - EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl, - ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION, - DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED, - }, - Storage::FileSystem::{ - FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, - }, - }, - }; - - const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0; - - fn unique_acl_test_path(prefix: &str) -> PathBuf { - std::env::temp_dir().join(format!( - "rustdesk_acl_{}_{}_{}", - prefix, - std::process::id(), - hbb_common::rand::random::() - )) - } - - fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool { - match symlink_dir(target, link) { - Ok(()) => true, - Err(err) => { - eprintln!( - "skip {}: failed to create directory reparse point (symlink): {}", - test_name, err - ); - false - } - } - } - - fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool { - match symlink_file(target, link) { - Ok(()) => true, - Err(err) => { - eprintln!( - "skip {}: failed to create file reparse point (symlink): {}", - test_name, err - ); - false - } - } - } - - fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> { - let mut dacl: *mut ACL = std::ptr::null_mut(); - let mut sd = PSECURITY_DESCRIPTOR::default(); - let path_utf16: Vec = path - .as_os_str() - .encode_wide() - .chain(std::iter::once(0)) - .collect(); - let result = unsafe { - GetNamedSecurityInfoW( - PCWSTR::from_raw(path_utf16.as_ptr()), - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - None, - None, - Some(&mut dacl), - None, - &mut sd, - ) - }; - if result.0 != 0 { - bail!( - "GetNamedSecurityInfoW failed for '{}': win32_error={}", - path.display(), - result.0 - ); - } - if dacl.is_null() || sd.0.is_null() { - bail!("DACL/security descriptor missing for '{}'", path.display()); - } - Ok((dacl, LocalAllocGuard(sd.0))) - } - - fn has_allow_ace_with_mask( - dacl: *const ACL, - sid_ptr: *mut std::ffi::c_void, - mask: u32, - ) -> bool { - let mut info = ACL_SIZE_INFORMATION::default(); - if unsafe { - GetAclInformation( - dacl, - &mut info as *mut _ as *mut std::ffi::c_void, - std::mem::size_of::() as u32, - AclSizeInformation, - ) - } - .is_err() - { - return false; - } - for index in 0..info.AceCount { - let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); - if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() { - continue; - } - let header = unsafe { &*(ace_ptr as *const ACE_HEADER) }; - if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 { - continue; - } - let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) }; - let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void); - if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok() - && (allowed.Mask & mask) == mask - { - return true; - } - } - false - } - - fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool { - has_allow_ace_with_mask(dacl, sid_ptr, 0) - } - - fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool { - let mut control: u16 = 0; - let mut revision: u32 = 0; - if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() { - return false; - } - (control & SE_DACL_PROTECTED.0) != 0 - } - - #[test] - fn test_portable_service_shmem_dir_acl_policy() { - let dir = unique_acl_test_path("dir"); - fs::create_dir_all(&dir).unwrap(); - set_path_permission_for_portable_service_shmem_dir(&dir).unwrap(); - - let (dacl, sd_guard) = get_file_dacl(&dir).unwrap(); - let current_user_sid = - sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap(); - let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap(); - let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap(); - let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap(); - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); - let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap(); - - assert!(has_allow_ace_with_mask( - dacl, - system_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - admin_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - current_user_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - auth_users_sid.as_sid_ptr(), - FILE_GENERIC_WRITE.0 - )); - assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr())); - assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr())); - assert!(is_dacl_protected(PSECURITY_DESCRIPTOR( - sd_guard.as_sid_ptr() - ))); - - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_portable_service_shmem_file_acl_policy() { - let dir = unique_acl_test_path("file"); - fs::create_dir_all(&dir).unwrap(); - let file = dir.join("shared_memory_portable_service_test"); - fs::write(&file, b"x").unwrap(); - set_path_permission_for_portable_service_shmem_file(&file).unwrap(); - - let (dacl, sd_guard) = get_file_dacl(&file).unwrap(); - let current_user_sid = - sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap(); - let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap(); - let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap(); - let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap(); - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); - let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap(); - - assert!(has_allow_ace_with_mask( - dacl, - system_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - admin_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(has_allow_ace_with_mask( - dacl, - current_user_sid.as_sid_ptr(), - FILE_ALL_ACCESS.0 - )); - assert!(!has_any_allow_ace_for_sid( - dacl, - auth_users_sid.as_sid_ptr() - )); - assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr())); - assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr())); - assert!(is_dacl_protected(PSECURITY_DESCRIPTOR( - sd_guard.as_sid_ptr() - ))); - - let _ = fs::remove_file(&file); - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_set_path_permission_rx_applies_recursively() { - let root = unique_acl_test_path("set_path_permission"); - let child_dir = root.join("child"); - let child_file = child_dir.join("helper.exe"); - fs::create_dir_all(&child_dir).unwrap(); - fs::write(&child_file, b"x").unwrap(); - - if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) { - let text = err.to_string(); - let _ = fs::remove_file(&child_file); - let _ = fs::remove_dir_all(&root); - if text.contains("win32_error=5") || text.contains("Access is denied") { - eprintln!( - "skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}", - text - ); - return; - } - panic!("set_path_permission failed unexpectedly: {}", text); - } - - let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); - let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0; - for target in [&root, &child_dir, &child_file] { - let (dacl, _sd_guard) = get_file_dacl(target).unwrap(); - assert!( - has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask), - "Everyone RX grant missing on '{}'", - target.display() - ); - } - - let _ = fs::remove_file(&child_file); - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn test_portable_service_shmem_dir_acl_rejects_file_target() { - let dir = unique_acl_test_path("dir_target_file"); - fs::create_dir_all(&dir).unwrap(); - let file = dir.join("target.txt"); - fs::write(&file, b"x").unwrap(); - let result = set_path_permission_for_portable_service_shmem_dir(&file); - assert!(result.is_err()); - let _ = fs::remove_file(&file); - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_portable_service_shmem_file_acl_rejects_dir_target() { - let dir = unique_acl_test_path("file_target_dir"); - fs::create_dir_all(&dir).unwrap(); - let result = set_path_permission_for_portable_service_shmem_file(&dir); - assert!(result.is_err()); - let _ = fs::remove_dir_all(&dir); - } - - #[test] - fn test_portable_service_shmem_file_acl_rejects_missing_target() { - let path = unique_acl_test_path("missing").join("shared_memory_missing"); - let result = set_path_permission_for_portable_service_shmem_file(&path); - assert!(result.is_err()); - } - - #[test] - fn test_set_path_permission_rejects_reparse_entrypoint() { - let root = unique_acl_test_path("reparse_entry"); - let real_dir = root.join("real"); - let link_dir = root.join("link"); - fs::create_dir_all(&real_dir).unwrap(); - if !try_create_dir_reparse_point( - &real_dir, - &link_dir, - "test_set_path_permission_rejects_reparse_entrypoint", - ) { - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - return; - } - - let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0); - let text = result.err().map(|e| e.to_string()).unwrap_or_default(); - assert!( - text.contains("reparse point"), - "expected reparse-point rejection, got '{}'", - text - ); - - let _ = fs::remove_dir(&link_dir); - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn test_portable_service_shmem_dir_acl_rejects_reparse_target() { - let root = unique_acl_test_path("reparse_shmem_dir"); - let real_dir = root.join("real"); - let link_dir = root.join("link"); - fs::create_dir_all(&real_dir).unwrap(); - if !try_create_dir_reparse_point( - &real_dir, - &link_dir, - "test_portable_service_shmem_dir_acl_rejects_reparse_target", - ) { - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - return; - } - - let result = set_path_permission_for_portable_service_shmem_dir(&link_dir); - let text = result.err().map(|e| e.to_string()).unwrap_or_default(); - assert!( - text.contains("reparse point"), - "expected reparse-point rejection, got '{}'", - text - ); - - let _ = fs::remove_dir(&link_dir); - let _ = fs::remove_dir_all(&real_dir); - let _ = fs::remove_dir_all(&root); - } - - #[test] - fn test_portable_service_shmem_file_acl_rejects_reparse_target() { - let root = unique_acl_test_path("reparse_shmem_file"); - let real_file = root.join("real.txt"); - let link_file = root.join("link.txt"); - fs::create_dir_all(&root).unwrap(); - fs::write(&real_file, b"x").unwrap(); - if !try_create_file_reparse_point( - &real_file, - &link_file, - "test_portable_service_shmem_file_acl_rejects_reparse_target", - ) { - let _ = fs::remove_file(&real_file); - let _ = fs::remove_dir_all(&root); - return; - } - - let result = set_path_permission_for_portable_service_shmem_file(&link_file); - let text = result.err().map(|e| e.to_string()).unwrap_or_default(); - assert!( - text.contains("reparse point"), - "expected reparse-point rejection, got '{}'", - text - ); - - let _ = fs::remove_file(&link_file); - let _ = fs::remove_file(&real_file); - let _ = fs::remove_dir_all(&root); - } -} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 89d7fa01e..3ef280a2a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -41,30 +41,6 @@ lazy_static::lazy_static! { static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); -pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false); -// register_pk retry interval (ms) when device is awaiting deployment -const DEPLOY_RETRY_INTERVAL: i64 = 30_000; -lazy_static::lazy_static! { - static ref LAST_NOT_DEPLOYED_REGISTER: Mutex> = Mutex::new(None); -} - -// Single source of truth for the "awaiting deployment" backoff. The server has -// already told us this device is not in its db; until the operator runs -// `rustdesk --deploy --token ` there is no point re-running the -// register path more often than DEPLOY_RETRY_INTERVAL. Gating in the timer -// loops (rather than only inside register_pk) also avoids the -// last_register_sent / fails / latency / UDP-rebind churn the loop would -// otherwise spin on while no response ever comes back. -async fn deploy_register_throttled() -> bool { - if !NEEDS_DEPLOY.load(Ordering::SeqCst) { - return false; - } - LAST_NOT_DEPLOYED_REGISTER - .lock() - .await - .map(|t| (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL) - .unwrap_or(false) -} #[derive(Clone)] pub struct RendezvousMediator { @@ -250,14 +226,6 @@ impl RendezvousMediator { if SHOULD_EXIT.load(Ordering::SeqCst) { break; } - // The server already told us this device is not deployed. Skip - // the whole register / fails / latency / UDP-rebind path until - // DEPLOY_RETRY_INTERVAL elapses, otherwise the loop spins every - // few seconds (log spam + misapplied network-recovery rebind) - // until the operator runs `rustdesk --deploy`. - if deploy_register_throttled().await { - continue; - } let now = Some(Instant::now()); let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true); let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false); @@ -321,22 +289,10 @@ impl RendezvousMediator { Config::set_key_confirmed(true); Config::set_host_key_confirmed(&self.host_prefix, true); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); - NEEDS_DEPLOY.store(false, Ordering::SeqCst); } Ok(register_pk_response::Result::UUID_MISMATCH) => { self.handle_uuid_mismatch(sink).await?; } - Ok(register_pk_response::Result::NOT_DEPLOYED) => { - if !NEEDS_DEPLOY.load(Ordering::SeqCst) { - log::warn!("Server requires deployment. Run `rustdesk --deploy --token ` on this device."); - } - NEEDS_DEPLOY.store(true, Ordering::SeqCst); - // Clear key_confirmed so the UI reflects the truth: this device is - // not currently registered. Covers the case where an online device - // was deleted by an admin while running. - Config::set_key_confirmed(false); - Config::set_host_key_confirmed(&self.host_prefix, false); - } _ => { log::error!("unknown RegisterPkResponse"); } @@ -722,21 +678,6 @@ impl RendezvousMediator { } async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> { - // Throttle register_pk when the device is awaiting deployment: server - // already told us we're not in its db; sending more often than every - // DEPLOY_RETRY_INTERVAL ms is wasted traffic until the operator runs - // `rustdesk --deploy --token `. - if NEEDS_DEPLOY.load(Ordering::SeqCst) { - let mut last = LAST_NOT_DEPLOYED_REGISTER.lock().await; - if let Some(t) = *last { - if (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL { - return Ok(()); - } - } - *last = Some(Instant::now()); - } else { - *LAST_NOT_DEPLOYED_REGISTER.lock().await = None; - } let mut msg_out = Message::new(); let pk = Config::get_key_pair().1; let uuid = hbb_common::get_uuid(); diff --git a/src/server.rs b/src/server.rs index 86f7b5396..dddc762bf 100644 --- a/src/server.rs +++ b/src/server.rs @@ -67,7 +67,6 @@ pub mod input_service { } mod connection; -mod login_failure_check; pub mod display_service; #[cfg(windows)] pub mod portable_service; @@ -732,7 +731,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option { if !synced { if conn.send(&Data::SyncConfig(None)).await.is_ok() { @@ -773,12 +772,6 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option { log::error!("sync config to root failed: {}", e); - match crate::ipc::connect_service(1000).await { + match crate::ipc::connect(1000, "_service").await { Ok(mut _conn) => { conn = _conn; log::info!("reconnected to ipc_service"); diff --git a/src/server/connection.rs b/src/server/connection.rs index 538503d9c..1ffb1a25e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,8 +1,3 @@ -#[cfg(target_os = "windows")] -use super::login_failure_check::try_acquire_os_credential_login_gate; -use super::login_failure_check::{ - evaluate_os_credential_policy, record_os_credential_failure, FailureScope, -}; use super::{input_service::*, *}; #[cfg(feature = "unix-file-copy-paste")] use crate::clipboard::try_empty_clipboard_files; @@ -27,10 +22,11 @@ use crate::{ #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use cidr_utils::cidr::IpCidr; +#[cfg(target_os = "linux")] +use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ - config::decode_permanent_password_h1_from_storage, config::{self, keys, Config, TrustedDevice}, fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, @@ -76,58 +72,11 @@ lazy_static::lazy_static! { static ref ALIVE_CONNS: Arc::>> = Default::default(); pub static ref AUTHED_CONNS: Arc::>> = Default::default(); pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::>> = Default::default(); + static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::>> = Default::default(); } -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); - static ref PENDING_SWITCH_SIDES_UUID: Arc::>> = Default::default(); -} - -#[cfg(target_os = "windows")] -const TERMINAL_OS_LOGIN_FAILED_MSG: &str = "Incorrect username or password."; - -fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { - if a.len() != b.len() { - return false; - } - // Avoid data-dependent early exits. - let mut x: u8 = 0; - for i in 0..a.len() { - x |= a[i] ^ b[i]; - } - x == 0 -} - -#[cfg(target_os = "linux")] -fn should_check_linux_headless_os_auth_before_desktop_start( - is_headless_allowed: bool, - username: &str, -) -> bool { - is_headless_allowed - && !username.trim().is_empty() - && linux_desktop_manager::get_username().is_empty() -} - -#[cfg(target_os = "linux")] -fn should_record_linux_headless_os_auth_failure( - is_headless_allowed: bool, - username: &str, - err_msg: &str, -) -> bool { - is_headless_allowed - && !username.trim().is_empty() - && err_msg == crate::client::LOGIN_MSG_PASSWORD_WRONG -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn should_use_terminal_os_login_scope(is_terminal: bool, os_login_username: &str) -> bool { - cfg!(target_os = "windows") && is_terminal && !os_login_username.trim().is_empty() -} - #[cfg(any(target_os = "windows", target_os = "linux"))] lazy_static::lazy_static! { static ref WALLPAPER_REMOVER: Arc>> = Default::default(); @@ -279,7 +228,6 @@ pub struct Connection { restart: bool, recording: bool, block_input: bool, - privacy_mode: bool, control_permissions: Option, last_test_delay: Option, network_delay: u32, @@ -470,7 +418,6 @@ impl Connection { restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions), recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions), block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions), - privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions), control_permissions, last_test_delay: None, network_delay: 0, @@ -567,9 +514,6 @@ impl Connection { if !conn.block_input { conn.send_permission(Permission::BlockInput, false).await; } - if !conn.privacy_mode { - conn.send_permission(Permission::PrivacyMode, false).await; - } let mut test_delay_timer = crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT)); let mut last_recv_time = Instant::now(); @@ -717,46 +661,6 @@ impl Connection { } else if &name == "block_input" { conn.block_input = enabled; conn.send_permission(Permission::BlockInput, enabled).await; - } else if &name == "privacy_mode" { - // Keep permission state and runtime state consistent: - // when revoking the permission, try to leave privacy mode first. - // Otherwise we could end up in an inconsistent state where - // permission looks disabled while privacy mode is still active. - if !enabled && privacy_mode::is_in_privacy_mode() { - if let Some(conn_id) = privacy_mode::get_privacy_mode_conn_id() { - if conn_id == conn.inner.id() { - let impl_key = - privacy_mode::get_cur_impl_key().unwrap_or_default(); - let turn_off_res = - privacy_mode::turn_off_privacy(conn_id, None); - match turn_off_res { - Some(Ok(_)) => { - let msg_out = crate::common::make_privacy_mode_msg( - back_notification::PrivacyModeState::PrvOffByPeer, - impl_key.clone(), - ); - conn.send(msg_out).await; - } - _ => { - let msg_out = Self::turn_off_privacy_result_to_msg( - turn_off_res, - impl_key, - ); - conn.send(msg_out).await; - // Turn-off failed, so revert CM's optimistic toggle - // and keep the previous permission value. - conn.send_to_cm(ipc::Data::SwitchPermission { - name: "privacy_mode".to_owned(), - enabled: conn.privacy_mode, - }); - continue; - } - } - } - } - } - conn.privacy_mode = enabled; - conn.send_permission(Permission::PrivacyMode, enabled).await; } } ipc::Data::RawMessage(bytes) => { @@ -813,8 +717,6 @@ impl Connection { log::error!("Failed to start portable service from cm: {:?}", e); } } - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::Data::SwitchSidesBack => { let mut misc = Misc::new(); misc.set_switch_back(SwitchBack::default()); @@ -1063,7 +965,7 @@ impl Connection { if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { - let _ = Self::turn_off_privacy_to_msg(id, String::new()); + let _ = Self::turn_off_privacy_to_msg(id); } } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] @@ -1531,9 +1433,6 @@ impl Connection { // Keep the connection alive so the client can continue with 2FA. return true; } - if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await { - return keep_alive; - } if !self.connect_port_forward_if_needed().await { return false; } @@ -1988,7 +1887,6 @@ impl Connection { restart: self.restart, recording: self.recording, block_input: self.block_input, - privacy_mode: self.privacy_mode, from_switch: self.from_switch, }); } @@ -2071,135 +1969,34 @@ impl Connection { self.tx_input.send(MessageInput::Key((msg, press))).ok(); } - fn verify_h1(&self, h1: &[u8]) -> bool { - let mut hasher2 = Sha256::new(); - hasher2.update(h1); - hasher2.update(self.hash.challenge.as_bytes()); - // A normal `==` on slices may short-circuit on the first mismatch, which can leak how many leading - // bytes matched via timing. In typical remote scenarios this is difficult to exploit due to network - // jitter, changing challenges, and login attempt throttling, but a constant-time comparison here is - // low-cost defensive programming. - constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..]) - } - - fn validate_password_plain(&self, password: &str) -> bool { - if password.is_empty() { + fn validate_one_password(&self, password: String) -> bool { + if password.len() == 0 { return false; } - let mut hasher = Sha256::new(); - hasher.update(password.as_bytes()); - hasher.update(self.hash.salt.as_bytes()); - let h1_plain = hasher.finalize(); - self.verify_h1(&h1_plain[..]) + hasher.update(password); + hasher.update(&self.hash.salt); + let mut hasher2 = Sha256::new(); + hasher2.update(&hasher.finalize()[..]); + hasher2.update(&self.hash.challenge); + hasher2.finalize()[..] == self.lr.password[..] } - fn validate_password_storage(&self, storage: &str) -> bool { - if storage.is_empty() { - return false; - } - - // Use strict decode success to detect hashed storage. - // If decode fails, treat as legacy plaintext storage for compatibility. - if let Some(h1) = decode_permanent_password_h1_from_storage(storage) { - return self.verify_h1(&h1[..]); - } - - // Legacy plaintext storage path. - self.validate_password_plain(storage) - } - - // This is coarse brute-force protection for the current temporary password value. - // We only care whether the active temporary password itself was presented correctly, - // not whether later authorization steps succeed. A successful temporary-password - // match clears this state immediately, and the counter also resets whenever the - // temporary password changes or is rotated. - fn check_update_temporary_password(&self, temporary_password_success: bool) { - const MAX_CONSECUTIVE_FAILURES: i32 = 10; - #[derive(Default)] - struct State { - password: String, - failures: i32, - } - lazy_static::lazy_static! { - static ref TEMPORARY_PASSWORD_FAILURES: Mutex = - Mutex::new(State::default()); - } - - if !password::temporary_enabled() { - return; - } - - let mut state = TEMPORARY_PASSWORD_FAILURES.lock().unwrap(); - let current_password = password::temporary_password(); - if current_password.is_empty() { - return; - } - if state.password != current_password { - state.password = current_password; - state.failures = 0; - } - - if temporary_password_success { - state.failures = 0; - return; - } - state.failures += 1; - - if state.failures < MAX_CONSECUTIVE_FAILURES { - return; - } - - password::update_temporary_password(); - let new_password = password::temporary_password(); - log::warn!( - "Temporary password rotated after too many consecutive wrong attempts: failures={}, ip={}", - state.failures, - self.ip, - ); - state.password = new_password; - state.failures = 0; - } - - fn validate_password(&mut self, allow_permanent_password: bool) -> bool { + fn validate_password(&mut self) -> bool { if password::temporary_enabled() { let password = password::temporary_password(); - if self.validate_password_plain(&password) { + if self.validate_one_password(password.clone()) { raii::AuthedConnID::update_or_insert_session( self.session_key(), Some(password), Some(false), ); - self.check_update_temporary_password(true); return true; } } - if password::permanent_enabled() || allow_permanent_password { - let print_fallback = || { - if allow_permanent_password && !password::permanent_enabled() { - log::info!("Permanent password accepted via logon-screen fallback"); - } - }; - // Since hashed storage uses a prefix-based encoding, a hard plaintext that - // happens to look like hashed storage could be mis-detected. Validate local storage - // and hard/preset plaintext via separate paths to avoid that ambiguity. - let (local_storage, _) = Config::get_local_permanent_password_storage_and_salt(); - if !local_storage.is_empty() { - if self.validate_password_storage(&local_storage) { - print_fallback(); - return true; - } - } else { - let hard = config::HARD_SETTINGS - .read() - .unwrap() - .get("password") - .cloned() - .unwrap_or_default(); - if !hard.is_empty() && self.validate_password_plain(&hard) { - print_fallback(); - return true; - } + if password::permanent_enabled() { + if self.validate_one_password(Config::get_permanent_password()) { + return true; } } false @@ -2219,7 +2016,7 @@ impl Connection { if let Some(session) = session { if !self.lr.password.is_empty() && (tfa && session.tfa - || !tfa && self.validate_password_plain(&session.random_password)) + || !tfa && self.validate_one_password(session.random_password.clone())) { log::info!("is recent session"); return true; @@ -2264,7 +2061,6 @@ impl Connection { keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart), keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording), keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input), - keys::OPTION_ENABLE_PRIVACY_MODE => Some(Permission::privacy_mode), _ => None, }; if let Some(permission) = permission { @@ -2413,6 +2209,33 @@ impl Connection { o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); } self.terminal_service_id = terminal.service_id; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg) = + self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) + { + self.send_login_error(msg).await; + sleep(1.).await; + return false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(is_user) = + terminal_service::is_service_specified_user(&self.terminal_service_id) + { + if let Some(user_token) = &self.terminal_user_token { + let has_service_token = + user_token.to_terminal_service_token().is_some(); + if is_user != has_service_token { + // This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation. + log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail."); + // No need to translate the following message, because it is in an abnormal case. + self.send_login_error("Terminal service user mismatch detected.") + .await; + sleep(1.).await; + return false; + } + } + } } Some(login_request::Union::PortForward(mut pf)) => { if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) { @@ -2430,43 +2253,8 @@ impl Connection { } } - if !hbb_common::is_ip_str(&lr.username) - && !hbb_common::is_domain_port_str(&lr.username) - && lr.username != Config::get_id() - { - self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) - .await; - return false; - } - - #[cfg(target_os = "windows")] - if self.terminal - && lr.os_login.username.trim().is_empty() - && crate::platform::is_prelogin() - { - self.send_login_error( - "No active console user logged on, please connect and logon first.", - ) - .await; - sleep(1.).await; - return false; - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if !should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { - self.try_start_cm_ipc(); - } - - #[cfg(target_os = "linux")] - if should_check_linux_headless_os_auth_before_desktop_start( - self.linux_headless_handle.is_headless_allowed, - &lr.os_login.username, - ) { - let (_failure, res) = self.check_failure(0).await; - if !res { - return true; - } - } + self.try_start_cm_ipc(); #[cfg(not(target_os = "linux"))] let err_msg = "".to_owned(); @@ -2478,18 +2266,6 @@ impl Connection { // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY { - #[cfg(target_os = "linux")] - if should_record_linux_headless_os_auth_failure( - self.linux_headless_handle.is_headless_allowed, - &lr.os_login.username, - &err_msg, - ) { - let (failure, res) = self.check_failure(0).await; - if !res { - return true; - } - self.update_failure(failure, false, 0); - } self.send_login_error(err_msg).await; return true; } @@ -2514,20 +2290,18 @@ impl Connection { #[cfg(any(target_os = "android", target_os = "ios"))] let is_logon = || crate::platform::is_prelogin(); - let allow_logon_screen_password = - crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" - && is_logon(); - - if (password::approve_mode() == ApproveMode::Click && !allow_logon_screen_password) + if !hbb_common::is_ip_str(&lr.username) + && !hbb_common::is_domain_port_str(&lr.username) + && lr.username != Config::get_id() + { + self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) + .await; + return false; + } else if (password::approve_mode() == ApproveMode::Click + && !(crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" + && is_logon())) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { - if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await - { - return keep_alive; - } - } self.try_start_cm(lr.my_id, lr.my_name, false); if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") @@ -2549,14 +2323,6 @@ impl Connection { } } else if lr.password.is_empty() { if err_msg.is_empty() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { - if let Some(keep_alive) = - self.prepare_terminal_login_for_authorization().await - { - return keep_alive; - } - } self.try_start_cm(lr.my_id, lr.my_name, false); } else { self.send_login_error( @@ -2569,9 +2335,8 @@ impl Connection { if !res { return true; } - if !self.validate_password(allow_logon_screen_password) { - self.update_failure_with_scope(failure, false, 0, FailureScope::Default); - self.check_update_temporary_password(false); + if !self.validate_password() { + self.update_failure(failure, false, 0); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) .await; @@ -2583,7 +2348,7 @@ impl Connection { .await; } } else { - self.update_failure_with_scope(failure, true, 0, FailureScope::Default); + self.update_failure(failure, true, 0); if err_msg.is_empty() { #[cfg(target_os = "linux")] self.linux_headless_handle.wait_desktop_cm_ready().await; @@ -2649,7 +2414,6 @@ impl Connection { } } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(lr) = _s.lr.clone().take() { self.handle_login_request_without_validation(&lr).await; SWITCH_SIDES_UUID @@ -3365,13 +3129,8 @@ impl Connection { } } #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { - crate::server::insert_pending_switch_sides_uuid( - self.lr.my_id.clone(), - uuid.clone(), - ); crate::run_me(vec![ "--connect", &self.lr.my_id, @@ -3548,16 +3307,16 @@ impl Connection { self.terminal_user_token = Some(TerminalUserToken::SelfUser); None } else { - Some(TERMINAL_OS_LOGIN_FAILED_MSG) + Some("The user is not an administrator.") } } Ok(Err(e)) => { log::error!("Failed to check if the user is an administrator: {}", e); - Some(TERMINAL_OS_LOGIN_FAILED_MSG) + Some("Failed to check if the user is an administrator.") } Err(e) => { log::error!("Failed to get logon user token: {}", e); - Some(TERMINAL_OS_LOGIN_FAILED_MSG) + Some("Incorrect username or password.") } } } @@ -3593,146 +3352,6 @@ impl Connection { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - async fn prepare_terminal_login_for_authorization(&mut self) -> Option { - if !self.terminal || self.terminal_user_token.is_some() { - return None; - } - - #[derive(Copy, Clone)] - enum TerminalAuthorizationMode { - OsLogin { - failure: ((i32, i32, i32), i32), - scope: FailureScope, - }, - SessionUser, - } - - let normalized_username = self.lr.os_login.username.trim().to_owned(); - let auth_mode = if should_use_terminal_os_login_scope(self.terminal, &normalized_username) { - // Check failure state - let failure_scope = FailureScope::TerminalOsLogin; - let (failure, res) = self.check_failure_with_scope(0, failure_scope).await; - if !res { - log::warn!( - "OS credential login blocked by failure policy: ip={} conn_id={} scope={:?}", - self.ip, - self.inner.id(), - failure_scope - ); - // Terminal OS login is sensitive. Close this connection instead of keeping it - // alive for retries on the same socket after a rate-limit block. - return Some(false); - } - TerminalAuthorizationMode::OsLogin { - failure, - scope: failure_scope, - } - } else { - TerminalAuthorizationMode::SessionUser - }; - - let is_terminal_os_login = matches!(auth_mode, TerminalAuthorizationMode::OsLogin { .. }); - let failure_scope = match auth_mode { - TerminalAuthorizationMode::OsLogin { scope, .. } => scope, - TerminalAuthorizationMode::SessionUser => FailureScope::Default, - }; - - let username = normalized_username; - let password = self.lr.os_login.password.clone(); - let terminal_login_error = { - #[cfg(target_os = "windows")] - { - // Concurrency gate for terminal OS login with credentials, to prevent brute-force attacks. - let _os_login_concurrency_guard = if is_terminal_os_login { - let guard = try_acquire_os_credential_login_gate(); - if guard.is_err() { - log::warn!( - "OS credential login blocked by concurrency gate: ip={} conn_id={} scope={:?}", - self.ip, - self.inner.id(), - failure_scope - ); - self.send_login_error("Please try 1 minute later").await; - sleep(1.).await; - Self::post_alarm_audit( - AlarmAuditType::TerminalOsLoginConcurrency, - json!({ - "ip": self.ip, - "id": self.lr.my_id.clone(), - "name": self.lr.my_name.clone(), - }), - ); - return Some(false); - } - guard.ok() - } else { - None - }; - self.fill_terminal_user_token(&username, &password) - } - #[cfg(not(target_os = "windows"))] - { - self.fill_terminal_user_token(&username, &password) - } - }; - if let Some(msg) = terminal_login_error { - if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode { - self.update_failure_with_scope(failure, false, 0, scope); - } - let auth_context = if is_terminal_os_login { - "OS credential login verification" - } else { - "Terminal session-user authorization" - }; - log::warn!( - "{} failed: ip={} conn_id={} scope={:?} msg='{}'", - auth_context, - self.ip, - self.inner.id(), - failure_scope, - msg - ); - self.send_login_error(msg).await; - sleep(1.).await; - return Some(false); - } - if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode { - self.update_failure_with_scope(failure, true, 0, scope); - } - - if let Some(is_user) = - terminal_service::is_service_specified_user(&self.terminal_service_id) - { - if let Some(user_token) = &self.terminal_user_token { - let has_service_token = user_token.to_terminal_service_token().is_some(); - if is_user != has_service_token { - log::error!( - "Terminal service user mismatch: ip={} conn_id={} service_is_user={} has_service_token={}. The service ID may have been manually changed in the configuration, causing validation to fail.", - self.ip, - self.inner.id(), - is_user, - has_service_token - ); - // No need to translate the following message, because it is in an abnormal case. - self.send_login_error("Terminal service user mismatch detected.") - .await; - sleep(1.).await; - return Some(false); - } - } - } - if is_terminal_os_login { - self.try_start_cm_ipc(); - } - None - } - - #[cfg(any(target_os = "android", target_os = "ios"))] - async fn prepare_terminal_login_for_authorization(&mut self) -> Option { - None - } - // Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes. // Parsing an IPv4 address just returns None. // note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues @@ -3759,37 +3378,18 @@ impl Connection { Some((p64, p56, p48)) } - fn bump_failure_entry(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { - if cur.0 == time { - cur.1 += 1; - cur.2 += 1; - } else { - cur.0 = time; - cur.1 = 1; - cur.2 += 1; - } - cur - } - - fn update_failure(&self, failure: ((i32, i32, i32), i32), remove: bool, i: usize) { - self.update_failure_with_scope(failure, remove, i, FailureScope::Default); - } - - fn update_failure_with_scope( - &self, - (failure, time): ((i32, i32, i32), i32), - remove: bool, - i: usize, - scope: FailureScope, - ) { - let os_credential_scope = matches!(scope, FailureScope::TerminalOsLogin); - if os_credential_scope { - if !remove { - record_os_credential_failure(scope); + fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { + fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { + if cur.0 == time { + cur.1 += 1; + cur.2 += 1; + } else { + cur.0 = time; + cur.1 = 1; + cur.2 += 1; } - return; + cur } - let map_mutex = &LOGIN_FAILURES[i]; if remove { if failure.0 != 0 { @@ -3810,15 +3410,14 @@ impl Connection { let mut m = map_mutex.lock().unwrap(); for key in [p64, p56, p48] { let cur = m.get(&key).copied().unwrap_or((0, 0, 0)); - m.insert(key, Self::bump_failure_entry(cur, time)); + m.insert(key, bump(cur, time)); } - let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0)); - m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time)); + // Update full IP: bump from the *original* passed-in failure + m.insert(self.ip.clone(), bump(failure, time)); } else { - // Re-read the full IP bucket in case another failed attempt updated it. + // Update full IP: bump from the *original* passed-in failure let mut m = map_mutex.lock().unwrap(); - let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0)); - m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time)); + m.insert(self.ip.clone(), bump(failure, time)); } } @@ -3858,50 +3457,8 @@ impl Connection { } async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) { - self.check_failure_with_scope(i, FailureScope::Default) - .await - } - - async fn check_failure_with_scope( - &mut self, - i: usize, - scope: FailureScope, - ) -> (((i32, i32, i32), i32), bool) { let time = (get_time() / 60_000) as i32; - if matches!(scope, FailureScope::TerminalOsLogin) { - let decision = evaluate_os_credential_policy(scope, get_time()); - let res = if decision.allowed { - true - } else { - log::warn!( - "OS credential login blocked by policy: ip={} conn_id={} i={} msg='{}'", - self.ip, - self.inner.id(), - i, - decision.login_error.as_deref().unwrap_or("") - ); - if let Some(login_error) = decision.login_error { - // Rare branch and currently temporary response copy; translation can be added later if needed. - self.send_login_error(login_error).await; - } - if let Some(audit) = decision.audit { - // For OS blocked/backoff events, we currently emit one alarm report per blocked attempt. - // TODO: Add unified cumulative/aggregation fields across alarm producers. - Self::post_alarm_audit( - audit, - json!({ - "ip": self.ip, - "id": self.lr.my_id.clone(), - "name": self.lr.my_name.clone(), - }), - ); - } - false - }; - return (((0, 0, 0), time), res); - } - // IPv6 addresses are cheap to make so we check prefix/netblock as well if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() { if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await { @@ -4470,15 +4027,6 @@ impl Connection { } async fn turn_on_privacy(&mut self, impl_key: String) { - if !self.is_authed_remote_conn() || !self.privacy_mode { - let msg_out = crate::common::make_privacy_mode_msg( - back_notification::PrivacyModeState::PrvOnFailedDenied, - impl_key, - ); - self.send(msg_out).await; - return; - } - let msg_out = if !privacy_mode::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, @@ -4520,7 +4068,7 @@ impl Connection { "Check privacy mode failed: {}, turn off privacy mode.", &err_msg ); - let _ = Self::turn_off_privacy_to_msg(self.inner.id, String::new()); + let _ = Self::turn_off_privacy_to_msg(self.inner.id); crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOnFailed, err_msg, @@ -4539,7 +4087,6 @@ impl Connection { if privacy_mode::is_in_privacy_mode() { let _ = Self::turn_off_privacy_to_msg( privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, - String::new(), ); } crate::common::make_privacy_mode_msg_with_details( @@ -4567,23 +4114,14 @@ impl Connection { impl_key, ) } else { - Self::turn_off_privacy_to_msg(self.inner.id, impl_key) + Self::turn_off_privacy_to_msg(self.inner.id) }; self.send(msg_out).await; } - pub fn turn_off_privacy_to_msg(_conn_id: i32, impl_key: String) -> Message { - Self::turn_off_privacy_result_to_msg( - privacy_mode::turn_off_privacy(_conn_id, None), - impl_key, - ) - } - - fn turn_off_privacy_result_to_msg( - turn_off_res: Option>, - impl_key: String, - ) -> Message { - match turn_off_res { + pub fn turn_off_privacy_to_msg(_conn_id: i32) -> Message { + let impl_key = "".to_owned(); + match privacy_mode::turn_off_privacy(_conn_id, None) { Some(Ok(_)) => crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, impl_key, @@ -5216,8 +4754,6 @@ impl Connection { } } -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { SWITCH_SIDES_UUID .lock() @@ -5225,31 +4761,7 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { .insert(id, (tokio::time::Instant::now(), uuid)); } -#[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn insert_pending_switch_sides_uuid(id: String, uuid: uuid::Uuid) { - let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap(); - uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10)); - uuids.insert(id, (tokio::time::Instant::now(), uuid)); -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool { - let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap(); - uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10)); - if uuids.get(id).map(|(_, stored_uuid)| stored_uuid == uuid) == Some(true) { - uuids.remove(id); - true - } else { - false - } -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -// IPC bootstrap summary: -// - Resolve target CM socket (headless/non-headless, optional UID-scoped path on Linux). -// - Start CM when missing, then bridge bidirectional messages between this task and CM IPC. async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, @@ -5264,19 +4776,10 @@ async fn start_ipc( } sleep(1.).await; } - #[cfg(target_os = "linux")] - let headless_cm = crate::is_server() - && crate::platform::is_headless_allowed() - && linux_desktop_manager::is_headless(); - #[cfg(not(target_os = "linux"))] - let headless_cm = false; let mut stream = None; - if !headless_cm { - if let Ok(s) = crate::ipc::connect(1000, "_cm").await { - stream = Some(s); - } - } - if stream.is_none() { + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + } else { #[allow(unused_mut)] #[allow(unused_assignments)] let mut args = vec!["--cm"]; @@ -5286,123 +4789,75 @@ async fn start_ipc( // Cm run as user, wait until desktop session is ready. #[cfg(target_os = "linux")] - if headless_cm { + if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() { let mut username = linux_desktop_manager::get_username(); loop { if !username.is_empty() { break; } - // `_rx_desktop_ready` is used as a wake-up signal from desktop/session state changes - // (for example wait_desktop_cm_ready paths). It is not itself a proof of CM readiness. - // TODO: - // When `_rx_desktop_ready` is closed, `recv()` returns - // `None` immediately and this loop may spin if `username` remains empty. - // Keep behavior unchanged for now; if field reports appear, handle `Ok(None)` by - // breaking/returning to avoid hot-looping. let _res = timeout(1_000, _rx_desktop_ready.recv()).await; username = linux_desktop_manager::get_username(); } let uid = { - let username_for_cmd = username.clone(); - let mut uid_cmd = hbb_common::tokio::process::Command::new("id"); - // TODO: - // Keep current behavior for now to minimize change risk. - // If usernames starting with '-' are observed in the field, prefer: - // `id -u -- ` to avoid option-parsing ambiguity. - // Already verified that `id -u -- ` works as expected on macOS and Ubuntu 24.04. - uid_cmd.arg("-u").arg(&username_for_cmd).kill_on_drop(true); - let output = timeout(10_000, uid_cmd.output()) - .await - .map_err(|_| anyhow!("Timed out querying uid for {}", username))? - .map_err(|e| anyhow!("Failed to run `id -u {}`: {}", username, e))?; - if !output.status.success() { - bail!("Failed to query uid for {}", username); - } - let output = String::from_utf8_lossy(&output.stdout); + let output = run_cmds(&format!("id -u {}", &username))?; let output = output.trim(); - if output.parse::().is_err() { - bail!("Invalid uid {}", output); + if output.is_empty() || !output.parse::().is_ok() { + bail!("Invalid username {}", &username); } output.to_string() }; user = Some((uid, username)); args = vec!["--cm-no-ui"]; } - #[cfg(target_os = "linux")] - let cm_uid: Option = match &user { - Some((uid, _)) => Some( - uid.parse::() - .map_err(|_| anyhow!("Invalid uid {}", uid))?, - ), - None => None, - }; - #[cfg(target_os = "linux")] - if let Some(uid) = cm_uid { - if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await { + let run_done; + if crate::platform::is_root() { + let mut res = Ok(None); + for _ in 0..10 { + #[cfg(not(any(target_os = "linux")))] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user(args.clone()); + } + #[cfg(target_os = "linux")] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user( + args.clone(), + user.clone(), + None::<(&str, &str)>, + ); + } + if res.is_ok() { + break; + } + log::error!("Failed to run cm: {res:?}"); + sleep(1.).await; + } + if let Some(task) = res? { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + log::debug!("Start cm"); + super::CHILD_PROCESS + .lock() + .unwrap() + .push(crate::run_me(args)?); + } + for _ in 0..20 { + sleep(0.3).await; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); + break; } } if stream.is_none() { - let run_done; - if crate::platform::is_root() { - let mut res = Ok(None); - for _ in 0..10 { - #[cfg(not(any(target_os = "linux")))] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user(args.clone()); - } - #[cfg(target_os = "linux")] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user( - args.clone(), - user.clone(), - None::<(&str, &str)>, - ); - } - if res.is_ok() { - break; - } - log::error!("Failed to run cm: {res:?}"); - sleep(1.).await; - } - if let Some(task) = res? { - super::CHILD_PROCESS.lock().unwrap().push(task); - } - run_done = true; - } else { - run_done = false; - } - if !run_done { - log::debug!("Start cm"); - super::CHILD_PROCESS - .lock() - .unwrap() - .push(crate::run_me(args)?); - } - for _ in 0..20 { - sleep(0.3).await; - #[cfg(target_os = "linux")] - { - if let Some(uid) = cm_uid { - if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await { - stream = Some(s); - break; - } - continue; - } - } - if let Ok(s) = crate::ipc::connect(1000, "_cm").await { - stream = Some(s); - break; - } - } + bail!("Failed to connect to connection manager"); } } - if stream.is_none() { - bail!("Failed to connect to connection manager"); - } let _res = tx_stream_ready.send(()).await; let mut stream = stream.ok_or(anyhow!("none stream"))?; @@ -5485,8 +4940,6 @@ pub enum AlarmAuditType { // MultipleLoginsAttemptsWithinOneMinute = 4, // MultipleLoginsAttemptsWithinOneHour = 5, ExceedIPv6PrefixAttempts = 6, - TerminalOsLoginBackoff = 7, - TerminalOsLoginConcurrency = 8, } pub enum FileAuditType { diff --git a/src/server/login_failure_check.rs b/src/server/login_failure_check.rs deleted file mode 100644 index 4394213ec..000000000 --- a/src/server/login_failure_check.rs +++ /dev/null @@ -1,231 +0,0 @@ -use crate::AlarmAuditType; -use hbb_common::get_time; -#[cfg(target_os = "windows")] -use hbb_common::tokio::sync::{Mutex as TokioMutex, OwnedMutexGuard}; -use std::sync::Mutex; -#[cfg(target_os = "windows")] -use std::sync::Arc; - -const OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS: i64 = 120 * 60 * 1_000; -const OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS: i64 = 15; -const OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS: i64 = 30 * 60; - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(crate) enum FailureScope { - Default, - TerminalOsLogin, -} - -pub(crate) struct OsCredentialPolicyDecision { - pub allowed: bool, - pub login_error: Option, - pub audit: Option, -} - -#[derive(Copy, Clone, Debug, Default)] -struct OsCredentialFailureState { - total_failures: i32, - backoff_until_ms: Option, - last_failure_ms: Option, -} - -lazy_static::lazy_static! { - static ref OS_CREDENTIAL_LOGIN_FAILURE_STATE: Mutex = - Mutex::new(OsCredentialFailureState::default()); -} - -#[cfg(target_os = "windows")] -lazy_static::lazy_static! { - static ref OS_CREDENTIAL_LOGIN_MUTEX: Arc> = Arc::new(TokioMutex::new(())); -} - -fn is_os_credential_scope(scope: FailureScope) -> bool { - matches!(scope, FailureScope::TerminalOsLogin) -} - -fn state_for_os_credential_scope( - scope: FailureScope, -) -> Option<&'static Mutex> { - if is_os_credential_scope(scope) { - Some(&OS_CREDENTIAL_LOGIN_FAILURE_STATE) - } else { - None - } -} - -fn backoff_audit_type_for_scope(scope: FailureScope) -> Option { - match scope { - FailureScope::TerminalOsLogin => Some(AlarmAuditType::TerminalOsLoginBackoff), - FailureScope::Default => None, - } -} - -fn os_credential_login_backoff_seconds(total_failures: i32) -> i64 { - if total_failures <= 2 { - return 0; - } - let exp = (total_failures - 3).min(7); - let seconds = OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS * (1_i64 << exp); - seconds.min(OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS) -} - -fn normalize_backoff(state: &mut OsCredentialFailureState, now_ms: i64) { - if let Some(until_ms) = state.backoff_until_ms { - if until_ms <= now_ms { - state.backoff_until_ms = None; - } - } -} - -fn reset_totals_on_idle(state: &mut OsCredentialFailureState, now_ms: i64) { - if let Some(last_ms) = state.last_failure_ms { - if now_ms.saturating_sub(last_ms) >= OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS { - state.total_failures = 0; - state.backoff_until_ms = None; - state.last_failure_ms = None; - } - } -} - -fn allow_decision() -> OsCredentialPolicyDecision { - OsCredentialPolicyDecision { - allowed: true, - login_error: None, - audit: None, - } -} - -fn block_decision( - login_error: String, - alarm_type: Option, -) -> OsCredentialPolicyDecision { - OsCredentialPolicyDecision { - allowed: false, - login_error: Some(login_error), - audit: alarm_type, - } -} - -pub(crate) fn evaluate_os_credential_policy( - scope: FailureScope, - now_ms: i64, -) -> OsCredentialPolicyDecision { - if !is_os_credential_scope(scope) { - return allow_decision(); - } - let Some(state_mutex) = state_for_os_credential_scope(scope) else { - return allow_decision(); - }; - let mut state = state_mutex.lock().unwrap(); - reset_totals_on_idle(&mut state, now_ms); - normalize_backoff(&mut state, now_ms); - - if let Some(until_ms) = state.backoff_until_ms { - let remaining_ms = (until_ms - now_ms).max(0); - let remaining_seconds = ((remaining_ms + 999) / 1_000).max(1); - let seconds_label = if remaining_seconds == 1 { - "second" - } else { - "seconds" - }; - block_decision( - format!( - "Please try again in {} {}.", - remaining_seconds, seconds_label - ), - backoff_audit_type_for_scope(scope), - ) - } else { - allow_decision() - } -} - -pub(crate) fn record_os_credential_failure(scope: FailureScope) { - if !is_os_credential_scope(scope) { - return; - } - let Some(state_mutex) = state_for_os_credential_scope(scope) else { - return; - }; - let mut state = state_mutex.lock().unwrap(); - let now_ms = get_time(); - reset_totals_on_idle(&mut state, now_ms); - normalize_backoff(&mut state, now_ms); - state.total_failures = state.total_failures.saturating_add(1); - state.last_failure_ms = Some(now_ms); - let backoff_seconds = os_credential_login_backoff_seconds(state.total_failures); - if backoff_seconds > 0 { - state.backoff_until_ms = Some(now_ms + backoff_seconds * 1_000); - } -} - -#[cfg(target_os = "windows")] -pub(crate) fn try_acquire_os_credential_login_gate() -> Result, ()> { - OS_CREDENTIAL_LOGIN_MUTEX - .clone() - .try_lock_owned() - .map_err(|_| ()) -} - -#[cfg(test)] -mod tests { - use super::*; - - static TEST_MUTEX: Mutex<()> = Mutex::new(()); - - fn clear_os_credential_failure_state(scope: FailureScope) { - if let Some(state_mutex) = state_for_os_credential_scope(scope) { - *state_mutex.lock().unwrap() = OsCredentialFailureState::default(); - } - } - - #[test] - fn os_credential_policy_prioritizes_backoff() { - let _guard = TEST_MUTEX.lock().unwrap(); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - let now_ms = get_time(); - for _ in 0..3 { - record_os_credential_failure(FailureScope::TerminalOsLogin); - } - let decision = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms); - assert!(!decision.allowed); - assert!(decision.login_error.is_some()); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - } - - #[test] - fn os_credential_policy_idle_window_resets_total_counter() { - let _guard = TEST_MUTEX.lock().unwrap(); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - for _ in 0..13 { - record_os_credential_failure(FailureScope::TerminalOsLogin); - } - let blocked = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, get_time()); - assert!(!blocked.allowed); - - let after_failures_ms = get_time(); - let after_idle_ms = after_failures_ms + OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS + 1_000; - let allowed = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, after_idle_ms); - assert!(allowed.allowed); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - } - - #[test] - fn os_credential_policy_audits_every_backoff_block() { - let _guard = TEST_MUTEX.lock().unwrap(); - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - - for _ in 0..3 { - record_os_credential_failure(FailureScope::TerminalOsLogin); - } - let now_ms = get_time(); - let first = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms); - let second = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms + 1_000); - assert!(!first.allowed); - assert!(!second.allowed); - assert!(first.audit.is_some()); - assert!(second.audit.is_some()); - - clear_os_credential_failure_state(FailureScope::TerminalOsLogin); - } -} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 23b69a70c..6f5695046 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -1,11 +1,3 @@ -use crate::{ - ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN}, - platform::{ - set_path_permission, set_path_permission_for_portable_service_shmem_dir, - set_path_permission_for_portable_service_shmem_file, - validate_path_for_portable_service_shmem_dir, - }, -}; use core::slice; use hbb_common::{ allow_err, @@ -23,26 +15,26 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, Mutex, - }, + path::Path, + sync::{Arc, Mutex}, time::Duration, }; use winapi::{ shared::minwindef::{BOOL, FALSE, TRUE}, um::winuser::{self, CURSORINFO, PCURSORINFO}, }; -use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ}; + +use crate::{ + ipc::{self, new_listener, Connection, Data, DataPortableService}, + platform::set_path_permission, +}; use super::video_qos; const SIZE_COUNTER: usize = size_of::() * 2; const FRAME_ALIGN: usize = 64; -const ADDR_IPC_TOKEN: usize = 0; -const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN; +const ADDR_CURSOR_PARA: usize = 0; const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::(); const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER; @@ -52,186 +44,12 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of:: bool { - !name.is_empty() - && name.len() <= SHMEM_NAME_MAX_LEN - && name - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-') -} - -#[inline] -pub fn portable_service_shmem_arg(name: &str) -> String { - format!("{SHMEM_ARG_PREFIX}{name}") -} - -#[inline] -fn is_valid_portable_service_ipc_token(token: &str) -> bool { - token.len() == IPC_TOKEN_LEN - && token - .bytes() - .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) -} - -#[inline] -fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option { - if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN { - log::error!( - "Portable service shared memory too small: len={}, need>={}", - shmem.len(), - ADDR_IPC_TOKEN + IPC_TOKEN_LEN - ); - return None; - } - unsafe { - let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN); - let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN); - let end = bytes - .iter() - .position(|byte| *byte == 0) - .unwrap_or(IPC_TOKEN_LEN); - if end == 0 { - return None; - } - let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned(); - if is_valid_portable_service_ipc_token(&token) { - Some(token) - } else { - None - } - } -} - -#[inline] -fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> { - if shmem.len() < MIN_RUNTIME_SHMEM_LEN { - bail!( - "Portable service shared memory too small for runtime layout: len={}, need>={}", - shmem.len(), - MIN_RUNTIME_SHMEM_LEN - ); - } - Ok(()) -} - -#[inline] -fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool { - let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME); - frame_len > 0 && frame_len <= frame_capacity -} - -#[inline] -fn shared_memory_flink_path_by_name(name: &str) -> ResultType { - let mut dir = crate::platform::user_accessible_folder()?; - dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); - dir = dir.join(SHMEM_PARENT_DIR); - Ok(dir.join(format!("shared_memory{}", name))) -} - -#[inline] -fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool { - let flink = match shared_memory_flink_path_by_name(name) { - Ok(path) => path, - Err(err) => { - if log_on_error { - log::warn!( - "{} failed to resolve portable service shared-memory flink path for '{}': {}", - log_context, - name, - err - ); - } - return false; - } - }; - match std::fs::remove_file(&flink) { - Ok(()) => { - log::info!( - "{} removed portable service shared-memory flink artifact: {:?}", - log_context, - flink - ); - true - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => true, - Err(err) => { - if log_on_error { - log::warn!( - "{} failed to remove portable service shared-memory flink artifact {:?}: {}", - log_context, - flink, - err - ); - } - false - } - } -} - -#[inline] -fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> { - if !is_valid_portable_service_ipc_token(token) { - bail!("Invalid portable service ipc token"); - } - shmem.write(ADDR_IPC_TOKEN, token.as_bytes()); - Ok(()) -} - -#[inline] -fn clear_ipc_token_in_shmem(shmem: &SharedMemory) { - shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]); -} - -#[inline] -fn portable_service_arg_value_candidate_from_arg<'a>( - arg: &'a str, - prefix: &str, -) -> Option<&'a str> { - let mut value = arg.strip_prefix(prefix)?; - value = value.trim_start(); - value = value - .strip_prefix('"') - .or_else(|| value.strip_prefix('\'')) - .unwrap_or(value); - value = value.split_whitespace().next().unwrap_or_default(); - value = value.trim_matches(|c| c == '"' || c == '\''); - Some(value) -} - -#[inline] -pub fn portable_service_shmem_name_from_args() -> Option { - for arg in std::env::args() { - if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) { - if is_valid_portable_service_shmem_name(value) { - return Some(value.to_owned()); - } - log::error!( - "Invalid portable service shared memory name argument: '{}'", - value - ); - return None; - } - } - None -} - -#[inline] -pub fn has_portable_service_shmem_arg() -> bool { - std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX)) -} - pub struct SharedMemory { inner: Shmem, } @@ -274,27 +92,7 @@ impl SharedMemory { } }; log::info!("Create shared memory, size: {}, flink: {}", size, flink); - if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) { - // Release shmem handle first so best-effort flink cleanup has a chance to succeed. - drop(shmem); - match std::fs::remove_file(&flink) { - Ok(()) => { - log::info!( - "Create cleanup removed portable service shared-memory flink artifact: {}", - flink - ); - } - Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {} - Err(remove_err) => { - log::warn!( - "Create cleanup failed to remove portable service shared-memory flink artifact {}: {}", - flink, - remove_err - ); - } - } - return Err(err); - } + set_path_permission(Path::new(&flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } @@ -322,18 +120,9 @@ impl SharedMemory { fn flink(name: String) -> ResultType { let mut dir = crate::platform::user_accessible_folder()?; dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); - dir = dir.join(SHMEM_PARENT_DIR); - let parent_created = !dir.exists(); - if parent_created { - std::fs::create_dir_all(&dir)?; - } - if parent_created || crate::platform::is_root() { - // Harden parent ACL on first provisioning and periodically on SYSTEM path. - set_path_permission_for_portable_service_shmem_dir(&dir)?; - } else { - // Existing parents still need type/reparse validation. Non-SYSTEM callers may lack - // WRITE_DAC on a valid parent, so avoid rebuilding the ACL here. - validate_path_for_portable_service_shmem_dir(&dir)?; + if !dir.exists() { + std::fs::create_dir(&dir)?; + set_path_permission(&dir, "F").ok(); } Ok(dir .join(format!("shared_memory{}", name)) @@ -443,45 +232,16 @@ pub mod server { lazy_static::lazy_static! { static ref EXIT: Arc> = Default::default(); - static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false); } pub fn run_portable_service() { - let shmem_name = match portable_service_shmem_name_from_args() { - Some(name) => name, - None => { - if has_portable_service_shmem_arg() { - log::error!( - "Invalid portable service shared memory argument, aborting startup" - ); - } else { - log::error!( - "Missing portable service shared memory argument, aborting startup" - ); - } - return; - } - }; - let shmem = match SharedMemory::open_existing(&shmem_name) { + let shmem = match SharedMemory::open_existing(SHMEM_NAME) { Ok(shmem) => Arc::new(shmem), Err(e) => { log::error!("Failed to open existing shared memory: {:?}", e); return; } }; - if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) { - log::error!("{}", e); - return; - } - let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) { - Some(token) => token, - None => { - log::error!( - "Missing portable service ipc token in shared memory, aborting startup" - ); - return; - } - }; let shmem1 = shmem.clone(); let shmem2 = shmem.clone(); let mut threads = vec![]; @@ -491,24 +251,17 @@ pub mod server { threads.push(std::thread::spawn(|| { run_capture(shmem2); })); - threads.push(std::thread::spawn(move || { - run_ipc_client(ipc_token); + threads.push(std::thread::spawn(|| { + run_ipc_client(); })); - // Detached shutdown watchdog: - // - gives graceful shutdown/cleanup a short window - // - force-exits the process if workers are still stuck - std::thread::spawn(|| { + threads.push(std::thread::spawn(|| { run_exit_check(); - }); + })); let record_pos_handle = crate::input_service::try_start_record_cursor_pos(); - // Arm forced-exit watchdog only for worker join phase. - // Once join phase completes, cleanup should not be interrupted by forced exit. - FORCE_EXIT_ARMED.store(true, Ordering::SeqCst); for th in threads.drain(..) { th.join().ok(); log::info!("thread joined"); } - FORCE_EXIT_ARMED.store(false, Ordering::SeqCst); crate::input_service::try_stop_record_cursor_pos(); if let Some(handle) = record_pos_handle { @@ -517,47 +270,16 @@ pub mod server { Err(e) => log::error!("record_pos_handle join error {:?}", &e), } } - drop(shmem); - remove_shared_memory_flink_with_retry(&shmem_name); } fn run_exit_check() { - const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3); loop { if EXIT.lock().unwrap().clone() { - break; + std::thread::sleep(Duration::from_millis(50)); + std::process::exit(0); } std::thread::sleep(Duration::from_millis(50)); } - // Fallback only: normal shutdown path should complete and process should exit naturally. - // This forced exit is a last resort when worker threads are stuck and graceful teardown - // does not finish in time. - std::thread::sleep(FORCED_EXIT_DELAY); - if FORCE_EXIT_ARMED.load(Ordering::SeqCst) { - log::warn!( - "Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}", - FORCED_EXIT_DELAY - ); - std::process::exit(0); - } - } - - fn remove_shared_memory_flink_with_retry(name: &str) { - const MAX_RETRY: usize = 20; - const RETRY_INTERVAL: Duration = Duration::from_millis(200); - for attempt in 0..MAX_RETRY { - let is_last_attempt = attempt + 1 == MAX_RETRY; - if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") { - return; - } - if !is_last_attempt { - std::thread::sleep(RETRY_INTERVAL); - } - } - log::warn!( - "SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry", - name - ); } fn run_get_cursor_info(shmem: Arc) { @@ -664,17 +386,6 @@ pub mod server { match c.as_mut().map(|f| f.frame(spf)) { Some(Ok(f)) => match f { Frame::PixelBuffer(f) => { - let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME); - if f.data().len() > frame_capacity { - log::error!( - "Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}", - f.data().len(), - frame_capacity, - shmem.len() - ); - *EXIT.lock().unwrap() = true; - return; - } utils::set_frame_info( &shmem, FrameInfo { @@ -725,33 +436,17 @@ pub mod server { } #[tokio::main(flavor = "current_thread")] - async fn run_ipc_client(ipc_token: String) { + async fn run_ipc_client() { use DataPortableService::*; let postfix = IPC_SUFFIX; match ipc::connect(1000, postfix).await { Ok(mut stream) => { - if let Err(err) = - ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await - { - log::error!("portable service ipc handshake failed: {}", err); - *EXIT.lock().unwrap() = true; - return; - } let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); let mut nack = 0; loop { - if *EXIT.lock().unwrap() { - log::info!("Portable service EXIT signaled, closing ipc client loop"); - stream - .send(&Data::DataPortableService(WillClose)) - .await - .ok(); - break; - } - tokio::select! { res = stream.next() => { match res { @@ -831,11 +526,7 @@ pub mod client { lazy_static::lazy_static! { static ref RUNNING: Arc> = Default::default(); - static ref STARTING: Arc> = Default::default(); - static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0); static ref SHMEM: Arc>> = Default::default(); - static ref SHMEM_RUNTIME_NAME: Arc>> = Default::default(); - static ref IPC_RUNTIME_TOKEN: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); static ref QUICK_SUPPORT: Arc> = Default::default(); } @@ -845,176 +536,12 @@ pub mod client { Logon(String, String), } - fn has_running_portable_service_process() -> bool { - let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase()); - !crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service") - .is_empty() - } - - #[inline] - fn next_portable_service_shmem_name() -> String { - format!( - "{}_{}_{:08x}", - crate::portable_service::SHMEM_NAME, - std::process::id(), - hbb_common::rand::random::() - ) - } - - #[inline] - fn set_runtime_ipc_token(token: String) { - *IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token); - } - - #[inline] - fn schedule_remove_runtime_shmem_flink_retry(name: String) { - std::thread::spawn(move || { - const MAX_RETRY: usize = 20; - const RETRY_INTERVAL: Duration = Duration::from_millis(200); - for _ in 0..MAX_RETRY { - std::thread::sleep(RETRY_INTERVAL); - if remove_shared_memory_flink_once(&name, false, "Client cleanup") { - return; - } - } - log::warn!( - "Failed to remove portable service shared-memory flink artifact '{}' after retry", - name - ); - }); - } - - #[inline] - fn clear_runtime_shmem_state() { - let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap(); - let mut shmem_lock = SHMEM.lock().unwrap(); - if let Some(shmem) = shmem_lock.as_mut() { - clear_ipc_token_in_shmem(shmem); - } - *shmem_lock = None; - let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take(); - *runtime_token = None; - drop(runtime_token); - drop(shmem_lock); - if let Some(name) = runtime_name.as_deref() { - if !remove_shared_memory_flink_once(name, true, "Client cleanup") { - schedule_remove_runtime_shmem_flink_retry(name.to_owned()); - } - } - } - - #[inline] - fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option) { - let mut token = IPC_RUNTIME_TOKEN.lock().unwrap(); - if !token - .as_deref() - .is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate)) - { - return (false, None); - } - let mut shmem_lock = SHMEM.lock().unwrap(); - let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone(); - *token = None; - if let Some(shmem) = shmem_lock.as_mut() { - clear_ipc_token_in_shmem(shmem); - } - (true, matched_shmem_name) - } - - #[inline] - fn restore_runtime_ipc_token_after_failed_handshake( - token: &str, - expected_shmem_name: Option<&str>, - ) { - let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap(); - if let Some(current) = runtime_token.as_deref() { - if current != token { - log::debug!( - "Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value" - ); - return; - } - } - let mut shmem_lock = SHMEM.lock().unwrap(); - let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone(); - if current_shmem_name.as_deref() != expected_shmem_name { - if runtime_token.as_deref() == Some(token) { - *runtime_token = None; - } - log::debug!( - "Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed" - ); - return; - } - let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() { - write_ipc_token_to_shmem(shmem, token) - .err() - .map(|err| err.to_string()) - } else { - Some("shared memory unavailable".to_owned()) - }; - if let Some(err) = shmem_write_error { - if runtime_token.as_deref() == Some(token) { - *runtime_token = None; - } - log::warn!( - "Failed to restore portable service ipc token after handshake failure: {}", - err - ); - return; - } - *runtime_token = Some(token.to_owned()); - } - - #[inline] - fn schedule_starting_timeout_reset(launch_token: u64) { - std::thread::spawn(move || { - std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT); - let should_reset = { - // Guard against stale watchdogs from previous launches: - // only the watchdog that matches the latest STARTING_TOKEN may reset STARTING. - let current_token = STARTING_TOKEN.load(Ordering::SeqCst); - // Keep lock guards in explicit short scopes to make it obvious - // there is no nested lock ordering (and to avoid Copilot false positives). - let starting = { *STARTING.lock().unwrap() }; - let running = { *RUNNING.lock().unwrap() }; - current_token == launch_token && starting && !running - }; - if should_reset { - log::warn!( - "Portable service startup timeout before IPC ready, reset STARTING state" - ); - *STARTING.lock().unwrap() = false; - } - }); - } - - // Launch flow summary: - // 1) Prepare/reset runtime shared memory + IPC token. - // 2) Start helper process (direct or logon) with shmem argument. - // 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it. pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { log::info!("start portable service"); - let launch_token = { - // Keep lock guards in explicit short scopes to make it obvious - // there is no nested lock ordering (and to avoid Copilot false positives). - let running = { *RUNNING.lock().unwrap() }; - let mut starting = STARTING.lock().unwrap(); - if *starting && !running && !has_running_portable_service_process() { - log::warn!( - "Detected stale portable service STARTING state without running process, reset it" - ); - *starting = false; - } - if *starting || running { - bail!("already running"); - } - *starting = true; - STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1 - }; - let start_result = (|| -> ResultType<()> { - clear_runtime_shmem_state(); - let mut shmem_lock = SHMEM.lock().unwrap(); + if RUNNING.lock().unwrap().clone() { + bail!("already running"); + } + if SHMEM.lock().unwrap().is_none() { let displays = scrap::Display::all()?; if displays.is_empty() { bail!("no display available!"); @@ -1031,153 +558,84 @@ pub mod client { } } } - let shmem_size = - utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN); - let shmem_name = next_portable_service_shmem_name(); - if !is_valid_portable_service_shmem_name(&shmem_name) { - bail!("Generated invalid portable service shared memory name"); - } - let ipc_token = ipc::generate_one_time_ipc_token()?; + let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align); // os error 112, no enough space - *shmem_lock = Some(crate::portable_service::SharedMemory::create( - &shmem_name, + *SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create( + crate::portable_service::SHMEM_NAME, shmem_size, )?); - *SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name); shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory); - let shmem_name = SHMEM_RUNTIME_NAME - .lock() - .unwrap() - .clone() - .ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?; - let init_token_result = if let Some(shmem) = shmem_lock.as_mut() { - unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); - } - write_ipc_token_to_shmem(shmem, &ipc_token) - } else { - Ok(()) - }; - if let Err(e) = init_token_result { - drop(shmem_lock); - clear_runtime_shmem_state(); - bail!( - "Failed to initialize portable service ipc token in shared memory: {}", - e - ); - }; - drop(shmem_lock); - set_runtime_ipc_token(ipc_token.clone()); - let portable_service_arg = format!( - "--portable-service {}", - crate::portable_service::portable_service_shmem_arg(&shmem_name) - ); - { - let _sender = SENDER.lock().unwrap(); - } - match para { - StartPara::Direct => { - match crate::platform::run_background( - &std::env::current_exe()?.to_string_lossy().to_string(), - &portable_service_arg, - ) { - Ok(true) => {} - Ok(false) => { - clear_runtime_shmem_state(); - bail!("Failed to run portable service process"); - } - Err(e) => { - clear_runtime_shmem_state(); - bail!("Failed to run portable service process: {}", e); - } - } - } - StartPara::Logon(username, password) => { - #[allow(unused_mut)] - let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); - #[cfg(feature = "flutter")] - { - if let Some(dir) = Path::new(&exe).parent() { - if let Err(err) = set_path_permission( - Path::new(dir), - FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0, - ) { - clear_runtime_shmem_state(); - bail!("Failed to set permission of {:?}: {}", dir, err); - } - } - } - #[cfg(not(feature = "flutter"))] - if let Some((dir, dst)) = - crate::platform::windows::portable_service_logon_helper_paths() - { - let cleanup_helper_artifacts = || { - if Path::new(&exe) != dst { - std::fs::remove_file(&dst).ok(); - } - std::fs::remove_dir(&dir).ok(); - }; - let mut use_logon_helper_exe = false; - if let Err(err) = std::fs::create_dir_all(&dir) { - log::warn!( - "Failed to create portable service logon helper dir {:?}: {}", - dir, - err - ); - } else if let Err(err) = std::fs::copy(&exe, &dst) { - log::warn!( - "Failed to copy portable service logon helper binary from '{}' to {:?}: {}", - exe, - dst, - err - ); - cleanup_helper_artifacts(); - } else if !dst.exists() { - log::warn!( - "Portable service logon helper binary missing after copy: {:?}", - dst - ); - cleanup_helper_artifacts(); - } else if let Err(err) = - set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) - { - log::warn!( - "Failed to set portable service logon helper path permission for {:?}: {}", - dir, - err - ); - cleanup_helper_artifacts(); - } else { - use_logon_helper_exe = true; - } - if use_logon_helper_exe { - exe = dst.to_string_lossy().to_string(); - } - } - if let Err(e) = crate::platform::windows::create_process_with_logon( - username.as_str(), - password.as_str(), - &exe, - &portable_service_arg, - ) { - clear_runtime_shmem_state(); - bail!("Failed to run portable service process: {}", e); - } - } - } - schedule_starting_timeout_reset(launch_token); - Ok(()) - })(); - if start_result.is_err() { - *STARTING.lock().unwrap() = false; } - start_result + if let Some(shmem) = SHMEM.lock().unwrap().as_mut() { + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + } + } + match para { + StartPara::Direct => { + if let Err(e) = crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = Path::new(&exe).parent() { + if set_path_permission(Path::new(dir), "RX").is_err() { + *SHMEM.lock().unwrap() = None; + bail!("Failed to set permission of {:?}", dir); + } + } + } + #[cfg(not(feature = "flutter"))] + match hbb_common::directories_next::UserDirs::new() { + Some(user_dir) => { + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + if std::fs::create_dir_all(&dir).is_ok() { + let dst = dir.join("rustdesk.exe"); + if std::fs::copy(&exe, &dst).is_ok() { + if dst.exists() { + if set_path_permission(&dir, "RX").is_ok() { + exe = dst.to_string_lossy().to_string(); + } + } + } + } + } + None => {} + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + } + let _sender = SENDER.lock().unwrap(); + Ok(()) } pub extern "C" fn drop_portable_service_shared_memory() { // https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout // Please make sure there is no print in the call stack - clear_runtime_shmem_state(); + let mut lock = SHMEM.lock().unwrap(); + if lock.is_some() { + *lock = None; + } } pub fn set_quick_support(v: bool) { @@ -1197,11 +655,7 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); if let Some(shmem) = option.as_mut() { unsafe { - libc::memset( - shmem.as_ptr().add(ADDR_CURSOR_PARA) as _, - 0, - shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _, - ); + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } utils::set_para( shmem, @@ -1248,19 +702,6 @@ pub mod client { if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) { let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO); let frame_info = frame_info_ptr as *const FrameInfo; - let frame_len = (*frame_info).length; - if !is_valid_capture_frame_length(shmem.len(), frame_len) { - log::error!( - "Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}", - frame_len, - shmem.len(), - ADDR_CAPTURE_FRAME - ); - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid portable service frame length".to_string(), - )); - } if (*frame_info).width != self.width || (*frame_info).height != self.height { log::info!( "skip frame, ({},{}) != ({},{})", @@ -1275,7 +716,7 @@ pub mod client { )); } let frame_ptr = base.add(ADDR_CAPTURE_FRAME); - let data = slice::from_raw_parts(frame_ptr, frame_len); + let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( data, self.width, @@ -1337,49 +778,10 @@ pub mod client { Some(result) = incoming.next() => { match result { Ok(stream) => { - let mut stream = Connection::new(stream); - if !ipc::authorize_windows_portable_service_ipc_connection( - &stream, postfix, - ) { - continue; - } - let mut consumed_token: Option = None; - let mut consumed_token_shmem_name: Option = None; - let handshake_result = - ipc::portable_service_ipc_handshake_as_server( - &mut stream, - |token| { - let (matched, matched_shmem_name) = - consume_runtime_ipc_token_if_match(token); - if matched { - consumed_token = Some(token.to_owned()); - consumed_token_shmem_name = matched_shmem_name; - true - } else { - false - } - }, - ) - .await; - if let Err(err) = handshake_result { - if let Some(token) = consumed_token.as_deref() { - restore_runtime_ipc_token_after_failed_handshake( - token, - consumed_token_shmem_name.as_deref(), - ); - *STARTING.lock().unwrap() = false; - } - log::warn!( - "Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}", - postfix, - err - ); - continue; - } log::info!("Got portable service ipc connection"); let rx_clone = rx.clone(); tokio::spawn(async move { - let mut stream = stream; + let mut stream = Connection::new(stream); let postfix = postfix.to_owned(); let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); let mut nack = 0; @@ -1403,7 +805,6 @@ pub mod client { Pong => { nack = 0; *RUNNING.lock().unwrap() = true; - *STARTING.lock().unwrap() = false; }, ConnCount(None) => { if !quick_support { @@ -1440,7 +841,6 @@ pub mod client { } } *RUNNING.lock().unwrap() = false; - *STARTING.lock().unwrap() = false; }); } Err(err) => { @@ -1590,23 +990,3 @@ pub struct FrameInfo { width: usize, height: usize, } - -#[cfg(test)] -mod tests { - use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME}; - - #[test] - fn test_is_valid_capture_frame_length_rejects_zero_length() { - assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0)); - } - - #[test] - fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() { - assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17)); - } - - #[test] - fn test_is_valid_capture_frame_length_accepts_in_bounds_length() { - assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16)); - } -} diff --git a/src/server/terminal_helper.rs b/src/server/terminal_helper.rs index fd85d2a4c..8edf4621b 100644 --- a/src/server/terminal_helper.rs +++ b/src/server/terminal_helper.rs @@ -318,35 +318,6 @@ pub fn get_default_shell() -> String { std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) } -fn utf8_shell_args(shell: &str) -> Vec { - let name = std::path::Path::new(shell) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(shell) - .to_ascii_lowercase(); - - if name == "cmd.exe" || name == "cmd" { - return vec!["/K".to_string(), "chcp 65001 >NUL".to_string()]; - } - - if name == "pwsh.exe" || name == "pwsh" || name == "powershell.exe" { - return vec![ - "-NoLogo".to_string(), - "-NoExit".to_string(), - "-Command".to_string(), - "chcp.com 65001 > $null; [Console]::InputEncoding = [System.Text.Encoding]::UTF8; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8".to_string(), - ]; - } - - Vec::new() -} - -pub fn configure_utf8_shell_command(shell: &str, cmd: &mut CommandBuilder) { - for arg in utf8_shell_args(shell) { - cmd.arg(arg); - } -} - /// Get the SID of the user from a token. /// Returns a Vec containing the SID bytes. pub fn get_user_sid_from_token(user_token: UserToken) -> Result> { @@ -860,8 +831,7 @@ pub fn run_terminal_helper(args: &[String]) -> Result<()> { let shell = get_default_shell(); log::debug!("Using shell: {}", shell); - let mut cmd = CommandBuilder::new(&shell); - configure_utf8_shell_command(&shell, &mut cmd); + let cmd = CommandBuilder::new(&shell); let mut child = pty_pair .slave .spawn_command(cmd) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 52a296b74..fb6b4fd29 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -20,11 +20,10 @@ use std::{ // Windows-specific imports from terminal_helper module #[cfg(target_os = "windows")] use super::terminal_helper::{ - configure_utf8_shell_command, create_named_pipe_server, encode_helper_message, - encode_resize_message, is_helper_process_running, launch_terminal_helper_with_token, - wait_for_pipe_connection, HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, - WinTerminateProcess, WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, - WIN_WAIT_OBJECT_0, + create_named_pipe_server, encode_helper_message, encode_resize_message, + is_helper_process_running, launch_terminal_helper_with_token, wait_for_pipe_connection, + HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, WinTerminateProcess, + WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, WIN_WAIT_OBJECT_0, }; const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal @@ -134,26 +133,6 @@ fn get_default_shell() -> String { } } -#[cfg(target_os = "macos")] -fn locale_value_is_utf8(value: &str) -> bool { - let value = value.to_ascii_uppercase(); - value.contains("UTF-8") || value.contains("UTF8") -} - -#[cfg(target_os = "macos")] -fn should_force_process_utf8_ctype() -> bool { - if let Ok(value) = std::env::var("LC_ALL") { - return !locale_value_is_utf8(&value); - } - if let Ok(value) = std::env::var("LC_CTYPE") { - return !locale_value_is_utf8(&value); - } - if let Ok(value) = std::env::var("LANG") { - return !locale_value_is_utf8(&value); - } - true -} - pub fn is_service_specified_user(service_id: &str) -> Option { get_service(service_id).map(|s| s.lock().unwrap().is_specified_user) } @@ -456,7 +435,6 @@ impl OutputBuffer { // Find first newline in new data if let Some(newline_pos) = data.iter().position(|&b| b == b'\n') { last_line.extend_from_slice(&data[..=newline_pos]); - self.total_size += newline_pos + 1; start = newline_pos + 1; self.last_line_incomplete = false; } else { @@ -495,28 +473,7 @@ impl OutputBuffer { // Trim old data if buffer is too large while self.total_size > MAX_OUTPUT_BUFFER_SIZE || self.lines.len() > MAX_BUFFER_LINES { if let Some(removed) = self.lines.pop_front() { - if removed.len() > self.total_size { - log::error!( - "OutputBuffer total_size underflow avoided: total_size={}, removed_len={}, lines_len={}", - self.total_size, - removed.len(), - self.lines.len() - ); - self.total_size = self.lines.iter().map(|line| line.len()).sum(); - } else { - self.total_size -= removed.len(); - } - if self.lines.is_empty() { - self.last_line_incomplete = false; - } - } else { - log::error!( - "OutputBuffer trim invariant broken: total_size={}, lines_len=0", - self.total_size - ); - self.total_size = 0; - self.last_line_incomplete = false; - break; + self.total_size -= removed.len(); } } } @@ -574,97 +531,6 @@ impl OutputBuffer { } } -/// Find the largest prefix of `buf` that does not end in the middle of a UTF-8 -/// code point. Invalid bytes are treated as complete so they can continue -/// downstream and be rendered with replacement characters if needed. -fn find_utf8_split_point(buf: &[u8]) -> usize { - if buf.is_empty() { - return 0; - } - - let start = buf.len().saturating_sub(3); - for i in (start..buf.len()).rev() { - let b = buf[i]; - if b & 0x80 == 0 { - return buf.len(); - } - if b & 0xC0 == 0x80 { - continue; - } - - let seq_len = if b & 0xE0 == 0xC0 { - 2 - } else if b & 0xF0 == 0xE0 { - 3 - } else if b & 0xF8 == 0xF0 { - 4 - } else { - return buf.len(); - }; - - return if buf.len() - i >= seq_len { - buf.len() - } else { - i - }; - } - - buf.len() -} - -// Terminal output currently follows a UTF-8 text model end to end: the service -// keeps replay buffers on UTF-8 boundaries, and Flutter decodes payload bytes as -// UTF-8 before writing to xterm. This accumulator only prevents splitting a -// trailing UTF-8 code point across PTY reads. Supporting non-UTF-8 terminals -// would need a separate design covering remote encoding detection, Flutter -// decoding, replay truncation, and input transcoding. -#[derive(Default)] -struct Utf8ChunkAccumulator { - remainder: Vec, -} - -impl Utf8ChunkAccumulator { - fn push_chunk(&mut self, mut data: Vec) -> Option> { - if data.is_empty() { - return None; - } - - let had_remainder = !self.remainder.is_empty(); - if had_remainder { - let mut combined = std::mem::take(&mut self.remainder); - combined.extend_from_slice(&data); - data = combined; - } - - let split = find_utf8_split_point(&data); - if split == data.len() { - return Some(data); - } - - // Only hold back a candidate incomplete suffix when we have evidence that - // the bytes before it are already UTF-8 text. If split is 0, the whole - // read may be the start of a UTF-8 character, so keep it for the next read. - if !had_remainder && split > 0 && std::str::from_utf8(&data[..split]).is_err() { - return Some(data); - } - - self.remainder = data.split_off(split); - if data.is_empty() { - None - } else { - Some(data) - } - } - - fn finish(&mut self) -> Option> { - if self.remainder.is_empty() { - None - } else { - Some(std::mem::take(&mut self.remainder)) - } - } -} - /// Try to send data through the output channel with rate-limited drop logging. /// Returns `true` if the caller should break out of the read loop (channel disconnected). fn try_send_output( @@ -704,11 +570,7 @@ fn try_send_output( false } Err(mpsc::TrySendError::Disconnected(_)) => { - log::debug!( - "Terminal {}{} output channel disconnected", - terminal_id, - label - ); + log::debug!("Terminal {}{} output channel disconnected", terminal_id, label); true } } @@ -1075,35 +937,15 @@ impl TerminalServiceProxy { if let Some(session_arc) = service.sessions.get(&open.terminal_id) { // Reconnect to existing terminal let mut session = session_arc.lock().unwrap(); - // Directly enter Active state with pending replay for immediate streaming. - // The replay combines output_buffer history and the channel backlog that was - // already pending at reconnect time so the client can suppress stale xterm - // query answers without requiring a protobuf schema change. - // During disconnect, read_outputs() is not called; channel data can still be lost - // if output_rx fills before reconnect drains it. - let mut buffer = session + // Directly enter Active state with pending buffer for immediate streaming. + // Historical buffer is sent first by read_outputs(), then real-time data follows. + // No overlap: pending_buffer comes from output_buffer (pre-disconnect history), + // while received_data in read_outputs() comes from the channel (post-reconnect). + // During disconnect, the run loop (sp.ok()) exits so read_outputs() stops being + // called; output_buffer is not updated, and channel data may be lost if it fills up. + let buffer = session .output_buffer .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES); - let mut reconnect_backlog = Vec::new(); - if let Some(output_rx) = &session.output_rx { - // Cap reconnect-time drain so a chatty PTY cannot keep OpenTerminal - // inside this loop indefinitely. Remaining output is drained by read_outputs(). - for _ in 0..CHANNEL_BUFFER_SIZE { - let Ok(data) = output_rx.try_recv() else { - break; - }; - reconnect_backlog.push(data); - } - } - let has_reconnect_backlog = !reconnect_backlog.is_empty(); - for data in reconnect_backlog { - session.output_buffer.append(&data); - } - if has_reconnect_backlog { - buffer = session - .output_buffer - .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES); - } let has_pending = !buffer.is_empty(); session.state = SessionState::Active { pending_buffer: if has_pending { Some(buffer) } else { None }, @@ -1117,14 +959,9 @@ impl TerminalServiceProxy { let mut opened = TerminalOpened::new(); opened.terminal_id = open.terminal_id; opened.success = true; - opened.message = if has_pending { - "Reconnected to existing terminal with pending output".to_string() - } else { - "Reconnected to existing terminal".to_string() - }; + opened.message = "Reconnected to existing terminal".to_string(); opened.pid = session.pid; opened.service_id = self.service_id.clone(); - opened.replay_terminal_output = has_pending; if service.needs_session_sync { if service.sessions.len() > 1 { // No need to include the current terminal in the list. @@ -1179,9 +1016,6 @@ impl TerminalServiceProxy { #[allow(unused_mut)] let mut cmd = CommandBuilder::new(&shell); - #[cfg(target_os = "windows")] - configure_utf8_shell_command(&shell, &mut cmd); - // macOS-specific terminal configuration // 1. Use login shell (-l) to load user's shell profile (~/.zprofile, ~/.bash_profile) // This ensures PATH includes Homebrew paths (/opt/homebrew/bin, /usr/local/bin) @@ -1202,12 +1036,6 @@ impl TerminalServiceProxy { }; cmd.env("TERM", term); log::debug!("Set TERM={} for macOS PTY", term); - - if should_force_process_utf8_ctype() { - cmd.env_remove("LC_ALL"); - cmd.env("LC_CTYPE", "en_US.UTF-8"); - log::debug!("Set LC_CTYPE=en_US.UTF-8 for macOS PTY"); - } } // Note: On Windows with user_token, we use helper mode (handle_open_with_helper) @@ -1258,7 +1086,6 @@ impl TerminalServiceProxy { let reader_thread = thread::spawn(move || { let mut reader = reader; let mut buf = vec![0u8; 4096]; - let mut utf8_chunks = Utf8ChunkAccumulator::default(); let mut drop_count: u64 = 0; // Initialize to > 5s ago so the first drop triggers a warning immediately. let mut last_drop_warn = Instant::now() - Duration::from_secs(6); @@ -1268,25 +1095,13 @@ impl TerminalServiceProxy { // EOF // This branch can be reached when the child process exits on macOS. // But not on Linux and Windows in my tests. - if let Some(data) = utf8_chunks.finish() { - let _ = try_send_output( - &output_tx, - data, - terminal_id, - "", - &mut drop_count, - &mut last_drop_warn, - ); - } break; } Ok(n) => { if exiting.load(Ordering::SeqCst) { break; } - let Some(data) = utf8_chunks.push_chunk(buf[..n].to_vec()) else { - continue; - }; + let data = buf[..n].to_vec(); // Use try_send to avoid blocking the reader thread when channel is full. // During disconnect, the run loop (sp.ok()) stops and read_outputs() is // no longer called, so the channel won't be drained. Blocking send would @@ -1493,23 +1308,12 @@ impl TerminalServiceProxy { let terminal_id = open.terminal_id; let reader_thread = thread::spawn(move || { let mut buf = vec![0u8; 4096]; - let mut utf8_chunks = Utf8ChunkAccumulator::default(); let mut drop_count: u64 = 0; // Initialize to > 5s ago so the first drop triggers a warning immediately. let mut last_drop_warn = Instant::now() - Duration::from_secs(6); loop { match output_pipe.read(&mut buf) { Ok(0) => { - if let Some(data) = utf8_chunks.finish() { - let _ = try_send_output( - &output_tx, - data, - terminal_id, - " (helper)", - &mut drop_count, - &mut last_drop_warn, - ); - } // EOF - helper process exited log::debug!("Terminal {} helper output EOF", terminal_id); break; @@ -1518,9 +1322,7 @@ impl TerminalServiceProxy { if exiting.load(Ordering::SeqCst) { break; } - let Some(data) = utf8_chunks.push_chunk(buf[..n].to_vec()) else { - continue; - }; + let data = buf[..n].to_vec(); // Use try_send to avoid blocking the reader thread (same as direct PTY mode) if try_send_output( &output_tx, @@ -1660,28 +1462,20 @@ impl TerminalServiceProxy { data: &TerminalData, ) -> Result> { if let Some(session_arc) = session { - let input = { - let mut session = session_arc.lock().unwrap(); - session.update_activity(); - if let Some(input_tx) = session.input_tx.clone() { - // Encode data for helper mode or send raw for direct PTY mode - #[cfg(target_os = "windows")] - let msg = if session.is_helper_mode { - encode_helper_message(MSG_TYPE_DATA, &data.data) - } else { - data.data.to_vec() - }; - #[cfg(not(target_os = "windows"))] - let msg = data.data.to_vec(); - - Some((input_tx, msg)) + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = &session.input_tx { + // Encode data for helper mode or send raw for direct PTY mode + #[cfg(target_os = "windows")] + let msg = if session.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, &data.data) } else { - None - } - }; + data.data.to_vec() + }; + #[cfg(not(target_os = "windows"))] + let msg = data.data.to_vec(); - if let Some((input_tx, msg)) = input { - // Send outside the session lock; SyncSender::send can block when full. + // Send data to writer thread if let Err(e) = input_tx.send(msg) { log::error!( "Failed to send data to terminal {}: {}", @@ -1889,6 +1683,10 @@ impl TerminalServiceProxy { } } + if has_activity { + session.update_activity(); + } + // Update buffer (always buffer for reconnection support) for data in &received_data { session.output_buffer.append(data); @@ -1898,7 +1696,7 @@ impl TerminalServiceProxy { // Data is already buffered above and will be sent on next reconnection. // Use a scoped block to limit the mutable borrow of session.state, // so we can immutably borrow other session fields afterwards. - let (replay_buffer, sigwinch_action) = { + let sigwinch_action = { let (pending_buffer, sigwinch) = match &mut session.state { SessionState::Active { pending_buffer, @@ -1907,12 +1705,19 @@ impl TerminalServiceProxy { _ => continue, }; - let replay_buffer = pending_buffer.take(); + // Send pending buffer response first (set on reconnection in handle_open). + // This ensures historical buffer is sent before any real-time data. + if let Some(buffer) = pending_buffer.take() { + if !buffer.is_empty() { + responses + .push(Self::create_terminal_data_response(terminal_id, buffer)); + } + } // Two-phase SIGWINCH: see SigwinchPhase doc comments for rationale. // Each phase is a single PTY resize, spaced ~30ms apart by the polling // interval, ensuring the TUI app sees a real size change on each signal. - let sigwinch_action = match sigwinch { + match sigwinch { SigwinchPhase::TempResize { retries } => { if *retries == 0 { log::warn!( @@ -1940,19 +1745,8 @@ impl TerminalServiceProxy { } } SigwinchPhase::Idle => None, - }; - (replay_buffer, sigwinch_action) - }; - - if let Some(buffer) = replay_buffer { - if !buffer.is_empty() { - responses.push(Self::create_terminal_data_response(terminal_id, buffer)); } - } - - if has_activity { - session.update_activity(); - } + }; // Execute SIGWINCH resize outside the mutable borrow scope of session.state. if let Some(action) = sigwinch_action { @@ -2051,116 +1845,3 @@ impl TerminalServiceProxy { } } } - -#[cfg(test)] -mod tests { - use super::{find_utf8_split_point, OutputBuffer, Utf8ChunkAccumulator, MAX_BUFFER_LINES}; - - #[test] - fn utf8_split_point_returns_full_len_for_complete_input() { - assert_eq!(find_utf8_split_point(b"hello"), 5); - assert_eq!(find_utf8_split_point("中文".as_bytes()), "中文".len()); - assert_eq!(find_utf8_split_point("😀".as_bytes()), "😀".len()); - } - - #[test] - fn utf8_split_point_detects_incomplete_trailing_sequence() { - let data = [b'a', 0xE4, 0xB8]; - assert_eq!(find_utf8_split_point(&data), 1); - } - - #[test] - fn utf8_split_point_keeps_malformed_prefix_but_buffers_trailing_lead_byte() { - let data = [0xFF, 0xE4]; - assert_eq!(find_utf8_split_point(&data), 1); - } - - #[test] - fn utf8_split_point_treats_orphan_continuations_as_complete() { - let data = [0x80, 0x81, 0x82]; - assert_eq!(find_utf8_split_point(&data), data.len()); - } - - #[test] - fn utf8_chunk_accumulator_reassembles_split_multibyte_output() { - let full = "你好世界".as_bytes(); - let mut chunker = Utf8ChunkAccumulator::default(); - let mut output = Vec::new(); - - for chunk in full.chunks(5) { - if let Some(data) = chunker.push_chunk(chunk.to_vec()) { - output.extend_from_slice(&data); - } - } - - if let Some(data) = chunker.finish() { - output.extend_from_slice(&data); - } - - assert_eq!(output, full); - } - - #[test] - fn utf8_chunk_accumulator_buffers_leading_split_multibyte_output() { - let mut chunker = Utf8ChunkAccumulator::default(); - - assert!(chunker.push_chunk(vec![0xE4]).is_none()); - assert!(chunker.push_chunk(vec![0xB8]).is_none()); - assert_eq!( - chunker.push_chunk(vec![0xAD]), - Some("中".as_bytes().to_vec()) - ); - assert!(chunker.finish().is_none()); - } - - #[test] - fn utf8_chunk_accumulator_flushes_incomplete_tail_on_finish() { - let mut chunker = Utf8ChunkAccumulator::default(); - assert_eq!(chunker.push_chunk(vec![b'a', 0xE4]), Some(vec![b'a'])); - assert_eq!(chunker.finish(), Some(vec![0xE4])); - assert!(chunker.finish().is_none()); - } - - #[test] - fn utf8_chunk_accumulator_does_not_stall_on_malformed_bytes() { - let mut chunker = Utf8ChunkAccumulator::default(); - assert_eq!(chunker.push_chunk(vec![0xFF]), Some(vec![0xFF])); - assert!(chunker.finish().is_none()); - } - - #[test] - fn utf8_chunk_accumulator_buffers_lone_utf8_lead_bytes() { - let mut chunker = Utf8ChunkAccumulator::default(); - assert!(chunker.push_chunk(vec![0xE4]).is_none()); - assert_eq!(chunker.finish(), Some(vec![0xE4])); - } - - #[test] - fn utf8_chunk_accumulator_does_not_hold_back_non_utf8_prefixes() { - let mut chunker = Utf8ChunkAccumulator::default(); - assert_eq!(chunker.push_chunk(vec![0xFF, 0xE4]), Some(vec![0xFF, 0xE4])); - assert!(chunker.finish().is_none()); - } - - #[test] - fn output_buffer_trim_after_incomplete_merge_does_not_underflow() { - let mut buffer = OutputBuffer::new(); - - // Create an incomplete line first. - buffer.append(b"hello"); - - // Merge a large chunk that contains the first newline at the tail. - // This exercises the "append to last incomplete line" branch. - let mut large = vec![b'a'; 30_000]; - large.push(b'\n'); - buffer.append(&large); - - // Exceed MAX_BUFFER_LINES so trim pops the first large merged line. - for _ in 0..=MAX_BUFFER_LINES { - buffer.append(b"x\n"); - } - - let actual_size: usize = buffer.lines.iter().map(|line| line.len()).sum(); - assert_eq!(buffer.total_size, actual_size); - } -} diff --git a/src/server/uinput.rs b/src/server/uinput.rs index a1947d79f..a808b4aaa 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -185,13 +185,9 @@ pub mod client { pub mod service { use super::*; use hbb_common::lazy_static; - #[cfg(target_os = "linux")] - use parity_tokio_ipc::Connection as RawIpcConnection; use scrap::wayland::{ pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop, }; - #[cfg(target_os = "linux")] - use std::os::unix::io::AsRawFd; use std::{collections::HashMap, sync::Mutex}; lazy_static::lazy_static! { @@ -606,10 +602,7 @@ pub mod service { } DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { if *code < 8 { - log::error!( - "Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", - code - ); + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); } else { let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); allow_err!(keyboard.emit(&[down_event])); @@ -617,10 +610,7 @@ pub mod service { } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { if *code < 8 { - log::error!( - "Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", - code - ); + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); } else { let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); allow_err!(keyboard.emit(&[up_event])); @@ -919,35 +909,6 @@ pub mod service { }); } - #[cfg(target_os = "linux")] - fn authorize_uinput_peer(postfix: &str, stream: &RawIpcConnection) -> bool { - if !hbb_common::config::is_service_ipc_postfix(postfix) { - return true; - } - let peer_uid = ipc::peer_uid_from_fd(stream.as_raw_fd()); - let active_uid = crate::platform::linux::get_active_userid_fresh() - .trim() - .parse::() - .ok(); - let authorized = - peer_uid.is_some_and(|uid| ipc::is_allowed_service_peer_uid(uid, active_uid)); - if !authorized { - crate::ipc::log_rejected_uinput_connection(postfix, peer_uid, active_uid); - return false; - } - if let Err(err) = - ipc::ensure_peer_executable_matches_current_by_fd(stream.as_raw_fd(), postfix) - { - log::warn!( - "Rejected connection on protected uinput ipc channel due to executable mismatch: postfix={}, err={}", - postfix, - err - ); - return false; - } - true - } - /// Start uinput service. async fn start_service(postfix: &str, handler: F) { match new_listener(postfix).await { @@ -955,10 +916,6 @@ pub mod service { while let Some(result) = incoming.next().await { match result { Ok(stream) => { - #[cfg(target_os = "linux")] - if !authorize_uinput_peer(postfix, &stream) { - continue; - } log::debug!("Got new connection of uinput ipc {}", postfix); handler(Connection::new(stream)); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 1e0efc0f4..6eb6a97bf 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -10,8 +10,7 @@ use std::io; use crate::{ client::{ - SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, - SCRAP_X11_REQUIRED, SCRAP_XDP_PORTAL_UNAVAILABLE, + SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, SCRAP_X11_REQUIRED, }, platform::linux::is_x11, }; @@ -57,15 +56,10 @@ fn map_err_scrap(err: String) -> io::Error { } } else { try_log(&err); - let err_lower = err.to_ascii_lowercase(); - if err_lower.contains("org.freedesktop.portal") - || err_lower.contains("dbus") - || err_lower.contains("d-bus") + if err.contains("org.freedesktop.portal") + || err.contains("pipewire") + || err.contains("dbus") { - // The portal D-Bus interface is unreachable. This typically means - // xdg-desktop-portal has crashed... for more info, see: Issue #12897 - io::Error::new(io::ErrorKind::Other, SCRAP_XDP_PORTAL_UNAVAILABLE) - } else if err_lower.contains("pipewire") { io::Error::new(io::ErrorKind::Other, SCRAP_OTHER_VERSION_OR_X11_REQUIRED) } else { io::Error::new(io::ErrorKind::Other, SCRAP_X11_REQUIRED) diff --git a/src/tray.rs b/src/tray.rs index e8db0efc0..8ab4e3ecb 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -54,22 +54,9 @@ fn make_tray() -> hbb_common::ResultType<()> { let mut event_loop = EventLoopBuilder::new().build(); let tray_menu = Menu::new(); - let hide_stop_service = crate::ui_interface::get_builtin_option( - hbb_common::config::keys::OPTION_HIDE_STOP_SERVICE, - ) == "Y"; - // The tray icon is only shown when the service is running, so we don't need to check - // the `stop-service` option here. - let quit_i = if !hide_stop_service { - Some(MenuItem::new(translate("Stop service".to_owned()), true, None)) - } else { - None - }; + let quit_i = MenuItem::new(translate("Stop service".to_owned()), true, None); let open_i = MenuItem::new(translate("Open".to_owned()), true, None); - if let Some(quit_i) = &quit_i { - tray_menu.append_items(&[&open_i, quit_i]).ok(); - } else { - tray_menu.append_items(&[&open_i]).ok(); - } + tray_menu.append_items(&[&open_i, &quit_i]).ok(); let tooltip = |count: usize| { if count == 0 { format!( @@ -168,19 +155,15 @@ fn make_tray() -> hbb_common::ResultType<()> { } if let Ok(event) = menu_channel.try_recv() { - if let Some(quit_i) = &quit_i { - if event.id == quit_i.id() { - /* failed in windows, seems no permission to check system process - if !crate::check_process("--server", false) { - *control_flow = ControlFlow::Exit; - return; - } - */ - if !crate::platform::uninstall_service(false, false) { - *control_flow = ControlFlow::Exit; - } - } else if event.id == open_i.id() { - open_func(); + if event.id == quit_i.id() { + /* failed in windows, seems no permission to check system process + if !crate::check_process("--server", false) { + *control_flow = ControlFlow::Exit; + return; + } + */ + if !crate::platform::uninstall_service(false, false) { + *control_flow = ControlFlow::Exit; } } else if event.id == open_i.id() { open_func(); diff --git a/src/ui.rs b/src/ui.rs index 6d0d0927a..fc59cffd2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -212,16 +212,12 @@ impl UI { update_temporary_password() } + fn permanent_password(&self) -> String { + permanent_password() + } + fn set_permanent_password(&self, password: String) { - let _ = set_permanent_password_with_result(password); - } - - fn is_local_permanent_password_set(&self) -> bool { - is_local_permanent_password_set() - } - - fn is_permanent_password_set(&self) -> bool { - is_permanent_password_set() + set_permanent_password(password); } fn get_remote_id(&mut self) -> String { @@ -372,11 +368,6 @@ impl UI { is_installed() } - fn get_supported_privacy_mode_impls(&self) -> String { - serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) - .unwrap_or_default() - } - fn is_root(&self) -> bool { is_root() } @@ -735,9 +726,8 @@ impl sciter::EventHandler for UI { fn get_id(); fn temporary_password(); fn update_temporary_password(); + fn permanent_password(); fn set_permanent_password(String); - fn is_local_permanent_password_set(); - fn is_permanent_password_set(); fn get_remote_id(); fn set_remote_id(String); fn closing(i32, i32, i32, i32); @@ -757,7 +747,6 @@ impl sciter::EventHandler for UI { fn get_icon(); fn install_me(String, String); fn is_installed(); - fn get_supported_privacy_mode_impls(); fn is_root(); fn is_release(); fn set_socks(String, String, String); diff --git a/src/ui/cm.css b/src/ui/cm.css index 3ac6c7be3..ba6de887b 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -93,13 +93,6 @@ div.permissions > div:active { opacity: 0.5; } -div.permissions.locked, -div.permissions.locked *, -div.permissions.locked > div:active { - cursor: default !important; - opacity: 1; -} - icon.keyboard { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII='); } @@ -128,10 +121,6 @@ icon.block_input { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAjdJREFUWEe1V8tNAzEQfXOHAx2QG0UgQSqBFIIgHdABoQqOhBq4cCMlcMh90FvZq/HEXtvJxlKUZNceP783no+gY6jqNYBHAHcA+JufXTDBb37eRWTbalZqE82mz7W55v0ABMBGRCLA7PJJAKr6AiC3sT11NHyf2SEyQjvtAMKp3wBYo9VTGbYegjxxU65d5tg4YEBVbwF8ALgw2lLX4in80QqyZUEkAMLCb7P5n4hcdWifTA32Pg0bByA8AE4+oL3n9A1s7ERkEeeNAJzD/QC4OVaCAgjrU7wdK86zAHREJSKqyvvORRxVb67JFOT4NfYGpxwAqCo34oYcKxHZhOdzg7D2BhYigHj6RJ+5QbjrPezlqR61sZTOKYfztSUBWPoXpdA5FwjnC2sCGK+eiNRC8yw+oap0RiayLQHEPwf65zx7DibMoXcEEB0wq/85QJQAbEVkWbvP8f0pTFi/65ZgjtuRyJ7QYWL0OZnwTmiLDobH5nLqGDlUlcmON49jQwnsg/Wxma/VJ1zcGQIR7+OYJGyqbJWhhwlDPxh3JpNRL4Ba7nAsJckoYaFUv7UCyslBvQ3TNDWEfVsPJGH2FCkKTPAxD8ox+poFwJfZqqX15H6eYyK+TgJeriidLCJ7wAQHZ4Udy7u9iFxaG7mynEx4EF1leZDANzV7AE8i8joJICz2cvBxbExIYTZYTTQmxTxTzP+VnvC8rZlLOLEj7m5OW6JqtTs2US6247Hvy7XnX0OV05FP/gHde5fLZaGS8AAAAABJRU5ErkJggg=='); } -icon.privacy_mode { - background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAB7UlEQVR4AdyTrVYDMRCFuyjqiiuuOJA46sCVR6jDgQTXN+CgQIJCgkOCA0cduOLAgaOOuuW7czYhyWY5FcXQc28n85O5m9nsUuuPf/9IoCzLLnxd9MTCET3SvNckQnwL7lfcpnYueIGiKNbY8QYjERo+wZK4HuAcK94rVvGSWCO8gCqKjAixTXLPsAl7ldBxriASqAo6lfUnqUTaWAP5FajTYjxGCNXeYSRAwSflToBlKxSZKSCiMoUa6Uh+QNW/B37LC9D8lkTYHNegTf7JqNP8b5RB5AT7AkPoNqqXxUyATT28AUzhRuFFaLpDUYc9V1ihr7+EA/JdxUyAxQTWQDM3CuVSEWugGiUztJ5OIJPPhlKRbFEVXJZ1Anph8iNyTCsieA0dvIgCQY3ckBtyTIBjfuDcwRR2TPJDElkRcrpd6XcyJm7X2ATY3CKwi1UxxkNPeyiP/BAa8LVZObtdBMOPcYbvX7wXYJNE2lidBuNxyhgm0I1LCdcgFXmguXqoxhgJKELBKvYMhljH+ULEwDr8mEIRXWHSP6gJKIXIESxYh3PHzWJK1IuwjpAVcBWIhHPX0x2QE/vkHGofIzUevwr4KhZ003wvsOKYkAcxXfPoxbvk3AJuQ5MNRNwFsNKFCaibRGB0CxcqIJGU3wAAAP//8GtoDAAAAAZJREFUAwCJJuAxFVNbWwAAAABJRU5ErkJggg=='); -} - div.outer_buttons { flow:vertical; border-spacing:8; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 4a68a571d..15b7b9435 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -36,8 +36,7 @@ impl InvokeUiCM for SciterHandler { client.file, client.restart, client.recording, - client.block_input, - client.privacy_mode + client.block_input ), ); } @@ -53,12 +52,12 @@ impl InvokeUiCM for SciterHandler { self.call("newMessage", &make_args!(id, text)); } - fn change_theme(&self, dark: String) { - self.call("changeTheme", &make_args!(dark)); + fn change_theme(&self, _dark: String) { + // TODO } fn change_language(&self) { - self.call("changeLanguage", &make_args!()); + // TODO } fn show_elevation(&self, show: bool) { @@ -158,18 +157,9 @@ impl SciterConnectionManager { crate::ui_interface::get_option(key) } - fn get_builtin_option(&self, key: String) -> String { - crate::ui_interface::get_builtin_option(&key) - } - fn hide_cm(&self) -> bool { *crate::ui::cm::HIDE_CM.lock().unwrap() } - - fn get_supported_privacy_mode_impls(&self) -> String { - serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) - .unwrap_or_default() - } } impl sciter::EventHandler for SciterConnectionManager { @@ -191,8 +181,6 @@ impl sciter::EventHandler for SciterConnectionManager { fn can_elevate(); fn elevate_portable(i32); fn get_option(String); - fn get_builtin_option(String); fn hide_cm(); - fn get_supported_privacy_mode_impls(); } } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index f306e9032..a06fb9ff8 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -4,9 +4,6 @@ var body; var connections = []; var show_chat = false; var show_elevation = true; -var is_privacy_mode_supported = handler.get_supported_privacy_mode_impls() != '[]'; -var allow_perm_change_in_accept_window = - handler.get_builtin_option('enable-perm-change-in-accept-window') != 'N'; var svg_elevate = ; var hide_cm = undefined; @@ -38,7 +35,6 @@ class Body: Reactor.Component me.sendMsg(msg); }; var right_style = show_chat ? "" : "display: none"; - var permissions_locked = !allow_perm_change_in_accept_window; var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; @@ -62,16 +58,15 @@ class Body: Reactor.Component

    {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    {translate('Permissions')}
    } - {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    -
    +
    -
    } {c.is_file_transfer ?
    {translate('Transfer file')}
    : ""} @@ -108,7 +103,6 @@ class Body: Reactor.Component } event click $(icon.keyboard) (e) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.keyboard = !connection.keyboard; @@ -118,7 +112,6 @@ class Body: Reactor.Component } event click $(icon.clipboard) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.clipboard = !connection.clipboard; @@ -128,7 +121,6 @@ class Body: Reactor.Component } event click $(icon.audio) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.audio = !connection.audio; @@ -138,7 +130,6 @@ class Body: Reactor.Component } event click $(icon.file) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.file = !connection.file; @@ -148,7 +139,6 @@ class Body: Reactor.Component } event click $(icon.restart) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.restart = !connection.restart; @@ -158,7 +148,6 @@ class Body: Reactor.Component } event click $(icon.recording) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.recording = !connection.recording; @@ -168,7 +157,6 @@ class Body: Reactor.Component } event click $(icon.block_input) { - if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.block_input = !connection.block_input; @@ -177,16 +165,6 @@ class Body: Reactor.Component }); } - event click $(icon.privacy_mode) { - if (!allow_perm_change_in_accept_window) return; - var { cid, connection } = this; - checkClickTime(function() { - connection.privacy_mode = !connection.privacy_mode; - body.update(); - handler.switch_permission(cid, "privacy_mode", connection.privacy_mode); - }); - } - event click $(button#accept) { var { cid, connection } = this; checkClickTime(function() { @@ -390,7 +368,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -398,7 +376,6 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin }); if (conn) { conn.authorized = authorized; - conn.privacy_mode = privacy_mode; update(); return; } @@ -414,7 +391,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, audio: audio, file: file, restart: restart, recording: recording, - block_input:block_input, privacy_mode:privacy_mode, + block_input:block_input, disconnected: false }; if (idx < 0) { @@ -503,21 +480,15 @@ function getElapsed(time, now) { return out; } -var ui_status_cache = ["", ""]; +var ui_status_cache = [""]; function check_update_ui() { self.timer(1s, function() { var approve_mode = handler.get_option('approve-mode'); - var allow_perm_change = handler.get_builtin_option('enable-perm-change-in-accept-window'); var changed = false; if (ui_status_cache[0] != approve_mode) { ui_status_cache[0] = approve_mode; changed = true; } - if (ui_status_cache[1] != allow_perm_change) { - ui_status_cache[1] = allow_perm_change; - allow_perm_change_in_accept_window = allow_perm_change != 'N'; - changed = true; - } if (changed) update(); check_update_ui(); }); diff --git a/src/ui/common.css b/src/ui/common.css index 16dd6ca9f..3307e0965 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -72,11 +72,6 @@ button.button:hover, button.outline:hover { border-color: color(hover-border); } -button:disabled, -button:disabled:hover { - opacity: 0.3; -} - button.link { background: none !important; border: none; @@ -489,4 +484,4 @@ div.user-session select { background: color(bg); color: color(text); padding-left: 0.5em; -} +} \ No newline at end of file diff --git a/src/ui/header.tis b/src/ui/header.tis index 40ccbcbf2..17efe6982 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -218,7 +218,7 @@ class Header: Reactor.Component { {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} - {(pi.platform == "Windows" || pi.platform == "Mac OS") && (handler.get_toggle_option("privacy-mode") || (keyboard_enabled && privacy_mode_enabled)) ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} {handler.version_cmp(pi.version, '1.2.4') >= 0 ?
  • {svg_checkmark}{translate('True color (4:4:4)')}
  • : ""} @@ -602,13 +602,7 @@ function togglePrivacyMode(privacy_id) { if (!supported) { msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { }); } else { - var privacy_mode_impls = pi.platform_additions?.supported_privacy_mode_impl; - if (privacy_mode_impls == null || privacy_mode_impls == undefined) { - handler.toggle_option(privacy_id); - return; - } - var is_on = handler.get_toggle_option("privacy-mode"); - handler.toggle_privacy_mode("", !is_on); + handler.toggle_option(privacy_id); } } @@ -719,4 +713,4 @@ handler.setConnectionType = function(secured, direct, stream_type) { handler.updateRecordStatus = function(status) { recording = status; header.update(); -} +} \ No newline at end of file diff --git a/src/ui/index.tis b/src/ui/index.tis index a099b95f9..5853fe3e2 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -16,7 +16,6 @@ const disable_ab = handler.is_disable_ab(); const hide_server_settings = handler.get_builtin_option("hide-server-settings") == "Y"; const hide_proxy_settings = handler.get_builtin_option("hide-proxy-settings") == "Y"; const hide_websocket_settings = handler.get_builtin_option("hide-websocket-settings") == "Y"; -const hide_stop_service = handler.get_builtin_option("hide-stop-service") == "Y"; const disable_change_permanent_password = handler.get_builtin_option("disable-change-permanent-password") == "Y"; const disable_change_id = handler.get_builtin_option("disable-change-id") == "Y"; @@ -521,7 +520,6 @@ class MyIdMenu: Reactor.Component { {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote restart')}
  • } {!disable_settings &&
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • } {!disable_settings && is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} - {!disable_settings && (handler.get_supported_privacy_mode_impls() != '[]') &&
  • {svg_checkmark}{translate('Enable privacy mode')}
  • } {!disable_settings &&
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • } @@ -534,7 +532,7 @@ class MyIdMenu: Reactor.Component { {!disable_settings && !using_public_server && !outgoing_only &&
  • {svg_checkmark}{translate('Disable UDP')}
  • } {!disable_settings && !using_public_server &&
  • {svg_checkmark}{translate('Allow insecure TLS fallback')}
  • }
    - {(!hide_stop_service || service_stopped) &&
  • {svg_checkmark}{translate("Enable service")}
  • } +
  • {svg_checkmark}{translate("Enable service")}
  • {!disable_settings && is_win && handler.is_installed() ? : ""} {!disable_settings && } {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } @@ -1073,7 +1071,6 @@ class PasswordArea: Reactor.Component { var method = handler.get_option('verification-method'); var approve_mode= handler.get_option('approve-mode'); var show_password = approve_mode != 'click'; - var has_local_password = handler.is_local_permanent_password_set(); return
  • {svg_checkmark}{translate('Accept sessions via password')}
  • {svg_checkmark}{translate('Accept sessions via click')}
  • @@ -1084,7 +1081,6 @@ class PasswordArea: Reactor.Component { { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } { !show_password ? '' :
    } { !show_password || disable_change_permanent_password ? '' :
  • {translate('Set permanent password')}
  • } - { !show_password || disable_change_permanent_password ? '' :
  • {translate('Clear permanent password')}
  • } { !show_password ? '' : }
  • {svg_checkmark}{translate('enable-2fa-title')}
  • @@ -1117,10 +1113,6 @@ class PasswordArea: Reactor.Component { el.state.disabled = true; } } - if (el.id == "clear-password") { - var has_local_password = handler.is_local_permanent_password_set(); - el.state.disabled = !has_local_password; - } if (el.id == "tfa") el.attributes.toggleClass("selected", has_valid_2fa); } @@ -1136,28 +1128,16 @@ class PasswordArea: Reactor.Component { event click $(li#set-password) { var me = this; - var has_local_password = handler.is_local_permanent_password_set(); - var permanent_password_set = handler.is_permanent_password_set(); - var password_hidden_tip = translate('password-hidden-tip'); - var preset_password_tip = translate('preset-password-in-use-tip'); - var password_tip = ""; - if (has_local_password) { - password_tip = "
    [!] " + password_hidden_tip + "
    "; - } else if (permanent_password_set) { - password_tip = "
    [!] " + preset_password_tip + "
    "; - } + var password = handler.permanent_password(); + var value_field = password.length == 0 ? "" : "value=" + password; msgbox("custom-password", translate("Set Password"), "
    \ -
    " + translate('Password') + ":
    \ -
    " + translate('Confirmation') + ":
    \ - " + password_tip + " \ +
    " + translate('Password') + ":
    \ +
    " + translate('Confirmation') + ":
    \
    \ ", "", function(res=null) { if (!res) return; var p0 = (res.password || "").trim(); var p1 = (res.confirmation || "").trim(); - if (p0.length == 0 && p1.length == 0) { - return " "; - } if (p0.length < 6 && p0.length != 0) { return translate("Too short, at least 6 characters."); } @@ -1167,15 +1147,6 @@ class PasswordArea: Reactor.Component { handler.set_permanent_password(p0); me.update(); }, msgbox_default_height, get_msgbox_width()); - self.timer(30ms, function() { - updateSetPasswordSubmitState(); - }); - } - - event click $(li#clear-password) { - if (this.$(li#clear-password).state.disabled) return; - handler.set_permanent_password(""); - this.update(); } event click $(menu#edit-password-context>li) (_, me) { @@ -1255,18 +1226,6 @@ function updatePasswordArea() { } if (!outgoing_only) updatePasswordArea(); -function updateSetPasswordSubmitState() { - var dialog = $(#msgbox); - if (!dialog) return; - var password = dialog.$(input[name='password']); - var confirmation = dialog.$(input[name='confirmation']); - var submit = dialog.$(button#submit); - if (!password || !confirmation || !submit) return; - var can_submit = (password.value || "").trim().length > 0 || - (confirmation.value || "").trim().length > 0; - submit.state.disabled = !can_submit; -} - class ID: Reactor.Component { function render() { return
    ); event click $(#powered-by) { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 6e6b6a62f..542691f5f 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -193,10 +193,8 @@ class MsgboxComponent: Reactor.Component { } function submit() { - var submit_btn = this.$(button#submit); - if (submit_btn) { - if (submit_btn.state.disabled) return; - submit_btn.sendEvent("click"); + if (this.$(button#submit)) { + this.$(button#submit).sendEvent("click"); } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 8b6f01ae0..a575cf397 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -85,22 +85,6 @@ impl SciterHandler { serde_json::Value::Bool(b) => { value.set_item(k, b); } - serde_json::Value::Array(arr) if k == "supported_privacy_mode_impl" => { - let mut impls = Value::array(0); - for item in arr { - if let serde_json::Value::Array(entry) = item { - let impl_key = entry.get(0).and_then(|v| v.as_str()); - let impl_name = entry.get(1).and_then(|v| v.as_str()); - if let (Some(impl_key), Some(impl_name)) = (impl_key, impl_name) { - let mut impl_item = Value::array(0); - impl_item.push(impl_key); - impl_item.push(impl_name); - impls.push(impl_item); - } - } - } - value.set_item(k, impls); - } _ => { // ignore for now } @@ -566,7 +550,6 @@ impl sciter::EventHandler for SciterSession { fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); - fn toggle_privacy_mode(String, bool); fn get_remember(); fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 28fbc3763..7602432fe 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -17,7 +17,6 @@ var audio_enabled = true; // server side var file_enabled = true; // server side var restart_enabled = true; // server side var recording_enabled = true; // server side -var privacy_mode_enabled = true; // server side var scroll_body = $(body); var peer_platform = ""; @@ -589,7 +588,6 @@ handler.setPermission = function(name, enabled) { if (name == "clipboard") clipboard_enabled = enabled; if (name == "restart") restart_enabled = enabled; if (name == "recording") recording_enabled = enabled; - if (name == "privacy_mode") privacy_mode_enabled = enabled; input_blocked = false; header.update(); }); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index cab0d7f1c..75e724007 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -12,10 +12,7 @@ use hbb_common::fs::serialize_transfer_job; use hbb_common::tokio::sync::mpsc::unbounded_channel; use hbb_common::{ allow_err, bail, - config::{ - keys::{OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, OPTION_FILE_TRANSFER_MAX_FILES}, - option2bool, Config, - }, + config::{keys::OPTION_FILE_TRANSFER_MAX_FILES, Config}, fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult}, log, message_proto::*, @@ -28,7 +25,10 @@ use hbb_common::{ ResultType, }; #[cfg(target_os = "windows")] -use hbb_common::{config::keys::*, tokio::sync::Mutex as TokioMutex}; +use hbb_common::{ + config::{keys::*, option2bool}, + tokio::sync::Mutex as TokioMutex, +}; use serde_derive::Serialize; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use std::iter::FromIterator; @@ -143,7 +143,6 @@ pub struct Client { pub restart: bool, pub recording: bool, pub block_input: bool, - pub privacy_mode: bool, pub from_switch: bool, pub in_voice_call: bool, pub incoming_voice_call: bool, @@ -231,7 +230,6 @@ impl ConnectionManager { restart: bool, recording: bool, block_input: bool, - privacy_mode: bool, from_switch: bool, #[cfg(not(any(target_os = "ios")))] tx: mpsc::UnboundedSender, ) { @@ -253,7 +251,6 @@ impl ConnectionManager { restart, recording, block_input, - privacy_mode, from_switch, #[cfg(not(any(target_os = "ios")))] tx, @@ -395,23 +392,6 @@ pub fn send_chat(id: i32, text: String) { #[inline] #[cfg(not(any(target_os = "ios")))] pub fn switch_permission(id: i32, name: String, enabled: bool) { - #[cfg(target_os = "android")] - let is_keyboard_permission = name == "keyboard"; - #[cfg(not(target_os = "android"))] - let is_keyboard_permission = false; - if !option2bool( - OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - &crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), - ) && !is_keyboard_permission - { - log::info!( - "blocked cm switch_permission by policy, conn_id={}, permission={}, enabled={}", - id, - name, - enabled - ); - return; - } if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); }; @@ -420,19 +400,6 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { #[inline] #[cfg(target_os = "android")] pub fn switch_permission_all(name: String, enabled: bool) { - if name != "keyboard" - && !option2bool( - OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, - &crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), - ) - { - log::info!( - "blocked cm switch_permission_all by policy, permission={}, enabled={}", - name, - enabled - ); - return; - } for (_, client) in CLIENTS.read().unwrap().iter() { allow_err!(client.tx.send(Data::SwitchPermission { name: name.clone(), @@ -455,16 +422,9 @@ pub fn get_clients_length() -> usize { clients.len() } -#[inline] -#[cfg(target_os = "android")] -pub fn has_active_clients() -> bool { - let clients = CLIENTS.read().unwrap(); - clients.values().any(|c| !c.disconnected) -} - #[inline] #[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(not(any(target_os = "ios")))] pub fn switch_back(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchSidesBack)); @@ -543,9 +503,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, privacy_mode, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -573,26 +533,6 @@ impl IpcTaskRunner { Data::ChatMessage { text } => { self.cm.new_message(self.conn_id, text); } - Data::SwitchPermission { name, enabled } => { - // Keep this branch scoped to privacy mode rollback. - // Other CM permission toggles are updated optimistically by the UI itself. - // The backend currently sends SwitchPermission back to CM only when - // privacy-mode turn-off fails and the UI state must be restored. - if name == "privacy_mode" { - let client = { - let mut clients = CLIENTS.write().unwrap(); - clients.get_mut(&self.conn_id).map(|c| { - c.privacy_mode = enabled; - c.clone() - }) - }; - if let Some(client) = client { - // This reuses add_connection(), and cm.tis only selectively updates - // existing rows (authorized/privacy_mode) for this fallback path. - self.cm.ui_handler.add_connection(&client); - } - } - } Data::FS(mut fs) => { if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs { if let Ok(bytes) = self.stream.next_raw().await { @@ -895,7 +835,6 @@ pub async fn start_listen( restart, recording, block_input, - privacy_mode, from_switch, .. }) => { @@ -917,7 +856,6 @@ pub async fn start_listen( restart, recording, block_input, - privacy_mode, from_switch, tx.clone(), ); @@ -1003,6 +941,15 @@ async fn handle_fs( total_size, conn_id, } => { + // Validate file names to prevent path traversal attacks. + // This must be done BEFORE any path operations to ensure attackers cannot + // escape the target directory using names like "../../malicious.txt" + if let Err(e) = validate_transfer_file_names(&files) { + log::warn!("Path traversal attempt detected for {}: {}", path, e); + send_raw(fs::new_error(id, e, file_num), tx); + return; + } + // Convert files to FileEntry let file_entries: Vec = files .drain(..) @@ -1023,13 +970,9 @@ async fn handle_fs( file_num, false, false, + file_entries, overwrite_detection, ); - if let Err(e) = job.set_files(file_entries) { - log::warn!("Reject unsafe transfer file list for {}: {}", path, e); - send_raw(fs::new_error(id, e, file_num), tx); - return; - } job.total_size = total_size; job.conn_id = conn_id; write_jobs.push(job); @@ -1217,6 +1160,73 @@ async fn handle_fs( } } +/// Validates that a file name does not contain path traversal sequences. +/// This prevents attackers from escaping the base directory by using names like +/// "../../../etc/passwd" or "..\\..\\Windows\\System32\\malicious.dll". +#[cfg(not(any(target_os = "ios")))] +fn validate_file_name_no_traversal(name: &str) -> ResultType<()> { + // Check for null bytes which could cause path truncation in some APIs + if name.bytes().any(|b| b == 0) { + bail!("file name contains null bytes"); + } + + // Check for path traversal patterns + // We check for both Unix and Windows path separators + if name + .split(|c| c == '/' || c == '\\') + .filter(|s| !s.is_empty()) + .any(|component| component == "..") + { + bail!("path traversal detected in file name"); + } + + // On Windows, also check for drive letters (e.g., "C:") + #[cfg(windows)] + { + if name.len() >= 2 { + let bytes = name.as_bytes(); + if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + bail!("absolute path detected in file name"); + } + } + } + + // Check for names starting with path separator: + // - Unix absolute paths (e.g., "/etc/passwd") + // - Windows UNC paths (e.g., "\\server\share") + if name.starts_with('/') || name.starts_with('\\') { + bail!("absolute path detected in file name"); + } + + Ok(()) +} + +#[inline] +fn is_single_file_with_empty_name(files: &[(String, u64)]) -> bool { + files.len() == 1 && files.first().map_or(false, |f| f.0.is_empty()) +} + +/// Validates all file names in a transfer request to prevent path traversal attacks. +/// Returns an error if any file name contains dangerous path components. +#[cfg(not(any(target_os = "ios")))] +fn validate_transfer_file_names(files: &[(String, u64)]) -> ResultType<()> { + if is_single_file_with_empty_name(files) { + // Allow empty name for single file. + // The full path is provided in the `path` parameter for single file transfers. + return Ok(()); + } + + for (name, _) in files { + // In multi-file transfers, empty names are not allowed. + // Each file must have a valid name to construct the destination path. + if name.is_empty() { + bail!("empty file name in multi-file transfer"); + } + validate_file_name_no_traversal(name)?; + } + Ok(()) +} + /// Start a read job in CM for file transfer from server to client (Windows only). /// /// This creates a `TransferJob` using `new_read()`, validates it, and sends the @@ -1591,7 +1601,16 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { #[cfg(not(any(target_os = "ios")))] async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender) { handle_result( - spawn_blocking(move || fs::rename_file(&path, &new_name)).await, + spawn_blocking(move || { + // Rename target must not be empty + if new_name.is_empty() { + bail!("new file name cannot be empty"); + } + // Validate that new_name doesn't contain path traversal + validate_file_name_no_traversal(&new_name)?; + fs::rename_file(&path, &new_name) + }) + .await, id, 0, tx, @@ -1754,6 +1773,42 @@ mod tests { }); } + #[test] + #[cfg(not(any(target_os = "ios")))] + fn validate_file_name_security() { + // Null byte injection + assert!(super::validate_file_name_no_traversal("file\0.txt").is_err()); + assert!(super::validate_file_name_no_traversal("test\0").is_err()); + + // Path traversal + assert!(super::validate_file_name_no_traversal("../etc/passwd").is_err()); + assert!(super::validate_file_name_no_traversal("foo/../bar").is_err()); + assert!(super::validate_file_name_no_traversal("..").is_err()); + + // Absolute paths + assert!(super::validate_file_name_no_traversal("/etc/passwd").is_err()); + assert!(super::validate_file_name_no_traversal("\\Windows").is_err()); + #[cfg(windows)] + assert!(super::validate_file_name_no_traversal("C:\\Windows").is_err()); + + // Valid paths + assert!(super::validate_file_name_no_traversal("file.txt").is_ok()); + assert!(super::validate_file_name_no_traversal("subdir/file.txt").is_ok()); + assert!(super::validate_file_name_no_traversal("").is_ok()); + } + + #[test] + #[cfg(not(any(target_os = "ios")))] + fn validate_transfer_file_names_security() { + assert!(super::validate_transfer_file_names(&[("file.txt".into(), 100)]).is_ok()); + assert!(super::validate_transfer_file_names(&[("".into(), 100)]).is_ok()); + assert!( + super::validate_transfer_file_names(&[("".into(), 100), ("file.txt".into(), 100)]) + .is_err() + ); + assert!(super::validate_transfer_file_names(&[("../passwd".into(), 100)]).is_err()); + } + /// Tests that symlink creation works on this platform. /// This is a helper to verify the test environment supports symlinks. #[test] diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 1645b242d..49098f2db 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -609,57 +609,19 @@ pub fn update_temporary_password() { } #[inline] -pub fn is_permanent_password_set() -> bool { +pub fn permanent_password() -> String { #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::has_permanent_password(); + return Config::get_permanent_password(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let daemon_is_set = ipc::is_permanent_password_set(); - // `daemon_is_set` is authoritative for the return value. Local storage is only used to - // decide whether we should attempt a sync to clear stale user-side state. - let local_storage_is_empty = if daemon_is_set { - true - } else { - let (storage, _) = Config::get_local_permanent_password_storage_and_salt(); - storage.is_empty() - }; - if daemon_is_set || !local_storage_is_empty { - allow_err!(ipc::sync_permanent_password_storage_from_daemon()); - } - daemon_is_set - } + return ipc::get_permanent_password(); } #[inline] -pub fn is_local_permanent_password_set() -> bool { +pub fn set_permanent_password(password: String) { #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::has_local_permanent_password(); + Config::set_permanent_password(&password); #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - allow_err!(ipc::sync_permanent_password_storage_from_daemon()); - Config::has_local_permanent_password() - } -} - -pub fn set_permanent_password_with_result(password: String) -> bool { - if config::Config::is_disable_change_permanent_password() { - return false; - } - #[cfg(any(target_os = "android", target_os = "ios"))] - { - config::Config::set_permanent_password(&password); - return true; - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - match crate::ipc::set_permanent_password_with_ack(password) { - Ok(ok) => ok, - Err(err) => { - log::warn!("Failed to set permanent password via IPC: {err}"); - false - } - } - } + allow_err!(ipc::set_permanent_password(password)); } #[inline] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e6c8ac6a2..be1895e64 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -870,14 +870,12 @@ impl Session { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn enter(&self, keyboard_mode: String) { - let session_id = self.lc.read().unwrap().session_id as u128; - keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode, session_id); + keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode); } #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn leave(&self, keyboard_mode: String) { - let session_id = self.lc.read().unwrap().session_id as u128; - keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode, session_id); + keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode); } // flutter only TODO new input @@ -1464,11 +1462,10 @@ impl Session { self.send(Data::ElevateWithLogon(username, password)); } - #[cfg(any(target_os = "android", target_os = "ios", not(feature = "flutter")))] + #[cfg(any(target_os = "ios"))] pub fn switch_sides(&self) {} - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(any(target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn switch_sides(&self) { match crate::ipc::connect(1000, "").await {