95 Commits

Author SHA1 Message Date
RustDesk
7bbed69ad2 Merge pull request #232 from n-connect/master
U 20.04 for binary build & release
2023-03-30 00:43:49 +08:00
n-connect
d4b6e6ee28 U 20.04 for binary build & release
Build and GitHub release to 20.04
2023-03-29 18:40:33 +02:00
RustDesk
b3fbb9e179 Merge pull request #231 from n-connect/master
Move from Ubuntu 18.04 VM (Github Actions)
2023-03-29 22:45:38 +08:00
n-connect
b44eb1bbc3 Linux toolchain to 1.67.1
Changes  of - Ubuntu boxes rolled back to 18.04 where were applicable.

Linux toolchain from 1.62 -> 1.67.1, Windows toolchain untouched (1.62)
2023-03-29 16:43:02 +02:00
n-connect
d88286642e Move from Ubuntu 18.04
Base on https://github.com/nextcloud/notify_push/releases, the 18.04 will be deprecated. Also moving toolchain to 1.68 to hopefully fix FreeBSD build core dump issue.
2023-03-29 14:28:51 +02:00
RustDesk
74ff886900 Merge pull request #225 from n-connect/master
Adding log capability over syslog
2023-03-28 18:38:24 +08:00
n-connect
9e716b3b7b Minor update 2023-03-27 15:59:06 +02:00
n-connect
bea99ae315 Adding log capability over syslog
Logging over syslog added via a semi-duplicate line commented out by default. Instructions in the line above.
2023-03-25 22:30:59 +01:00
n-connect
6068b5941c Adding log capability over syslog
Logging over syslog added via a semi-duplicate line commented out by default. Instructions in the line above.
2023-03-25 22:29:02 +01:00
rustdesk
675bf3c1f5 fix command line buffer and test addr 2023-03-16 00:53:58 +08:00
RustDesk
dc81956d42 Merge pull request #219 from FastAct/master
Create README-NL.md
2023-03-15 19:33:42 +08:00
FastAct
ffe736be17 Create README-NL.md
Add Dutch translation
2023-03-15 12:21:01 +01:00
RustDesk
b83dae4cc4 Merge pull request #212 from n-connect/patch-1
Update build.yaml - adding FreeBSD build
2023-03-06 08:36:49 +08:00
n-connect
8d9203ecdb Update build.yaml - adding FreeBSD build 2023-03-05 19:44:56 +01:00
RustDesk
13321b5a90 Merge pull request #210 from n-connect/master
Log files enables
2023-03-03 10:32:00 +08:00
n-connect
0a8c39c11f hbbs logging to file
Logging enabled via file redirection (not syslog, as it can't tell/pass the logger program's name)
2023-03-03 01:11:26 +01:00
n-connect
7dd812c79a hbbr logging to file
Logging enabled via file redirection (not syslog, as it can't tell/pass the logger program's name)
2023-03-03 01:10:09 +01:00
RustDesk
9d524443ec Merge pull request #208 from n-connect/master
FreeBSD rcd scripts for hbbs & hbbr
2023-03-02 21:39:32 +08:00
n-connect
35a192a478 Create rustdesk-hbbs
FreeBSD rcd script running hbbs as service. Service user, group, pid, running directory handled. IP address of the -r option need to be changed manually.
2023-03-02 13:47:07 +01:00
n-connect
2f4235a968 Create rustdesk-hbbr
FreeBSD rcd script running hbbr as service. Service user, group, pid, running directory handled.
2023-03-02 13:44:13 +01:00
RustDesk
12b57238d2 Merge pull request #207 from n-connect/master
Update Cargo.toml for FreeBSD build
2023-03-02 20:31:30 +08:00
n-connect
fe805e8554 Update Cargo.toml for FreeBSD build
Crate.io package local-ip-address from v0.5+ is Freebsd compatible, eg. it compiles and works. Simply changing the version to v0.5.1 in the Cargo.toml was possible to make a successful release build on FreeBSD 13 with prepackaged Rust 1.67.1.
2023-03-02 13:19:10 +01:00
rustdesk
4d6d439b1a 1.1.7-1 2023-02-18 13:44:25 +08:00
rustdesk
ec202209f3 fix ID_EXISTS not sent out due to ipv6 change 2023-02-18 13:41:45 +08:00
RustDesk
49f10a288d Merge pull request #197 from elilchen/master
vite build
2023-02-16 23:05:27 +08:00
elilchen
986d16eb2d vite build 2023-02-16 22:28:34 +08:00
RustDesk
4fd83deaf1 Merge pull request #196 from elilchen/master
vite build
2023-02-16 22:23:16 +08:00
elilchen
85150127bb vite build 2023-02-16 22:18:27 +08:00
RustDesk
26d8c13fe4 Merge pull request #195 from elilchen/master
crt-static
2023-02-16 15:29:10 +08:00
elilchen
388ae586ec crt-static 2023-02-16 15:23:31 +08:00
RustDesk
6ad923d519 Merge pull request #194 from elilchen/master
fix issues #192
2023-02-16 14:04:48 +08:00
elilchen
fe661fe067 merge 2023-02-16 13:50:08 +08:00
elilchen
ad40d65070 issues #192 add MicrosoftEdgeWebview2Setup and fix the "VCRUNTIME140.dll Is Missing" error on windows server 2022 2023-02-16 13:39:08 +08:00
RustDesk
10bb0530ae Merge pull request #190 from elilchen/master
change icons
2023-02-14 22:52:42 +08:00
elilchen
7c3be2d9fb change icons 2023-02-14 22:48:59 +08:00
rustdesk
14301a7d5f sign all exe 2023-02-14 19:56:27 +08:00
rustdesk
d0841f7558 more lang in setup.nsi 2023-02-14 19:19:38 +08:00
rustdesk
467298efa7 fix sign 2023-02-14 19:02:46 +08:00
rustdesk
75203d2e4e sign 2023-02-14 18:20:05 +08:00
RustDesk
27d8f9cbb4 Merge pull request #188 from elilchen/master
UI
2023-02-12 09:23:42 +08:00
elilchen
7a0e300ff9 UI 2023-02-12 00:48:38 +08:00
rustdesk
b2f381913d sync 2023-02-11 00:25:44 +08:00
rustdesk
6ec46cb95f CI 2023-02-08 17:07:27 +08:00
rustdesk
e2f4962ba8 clippy 2023-02-08 16:45:30 +08:00
rustdesk
7e307a5a1c CI 2023-02-08 16:00:12 +08:00
rustdesk
33f54ba5aa sync with rustdesk 2023-02-08 15:45:51 +08:00
RustDesk
6a83ffea62 Merge pull request #187 from attie-argentum/encrypted_only
add '-k _' to hbbr if ENCRYPTED_ONLY is set
2023-02-05 12:26:12 +08:00
Attie Grande
af848f96df add '-k _' to hbbr if ENCRYPTED_ONLY is set 2023-02-03 22:40:56 +00:00
rustdesk
d88e4b5151 make hbbr / hbbs share the PORT value of .env 2023-02-01 23:31:00 +08:00
rustdesk
fe3b42809a run gen_version no matter debug or release 2023-02-01 10:49:16 +08:00
rustdesk
2830be95a7 opt 2023-01-27 11:37:43 +08:00
rustdesk
a974906fdc Merge branch 'master' into tmp 2023-01-27 11:37:15 +08:00
rustdesk
17ddc89bd0 sync rustdesk's hbb_common here 2023-01-27 11:00:59 +08:00
RustDesk
088a009078 Merge pull request #180 from paspo/deb_logdir_creation
fix logdir creation
2023-01-19 09:24:47 +08:00
Paolo Asperti
be2ce5c93b fix logdir creation 2023-01-18 18:19:12 +01:00
rustdesk
f8936eff93 change date 2023-01-11 11:28:36 +08:00
rustdesk
accd96f1d8 add 1.1.7 to debian/changelog 2023-01-11 11:20:09 +08:00
rustdesk
32ee474813 fmt 2023-01-10 23:03:44 +08:00
rustdesk
46a7c025a0 update version 2023-01-10 22:56:28 +08:00
rustdesk
3ca4035d0c wrong image name 2023-01-10 22:10:15 +08:00
rustdesk
86a75451d8 centos7 -> ubuntu18.04 2023-01-10 17:15:56 +08:00
rustdesk
8cdfe0fec6 22.04 -> 7 2023-01-10 16:44:36 +08:00
rustdesk
5aaad36729 1.1.7 2023-01-10 16:26:06 +08:00
rustdesk
fc83fa0a04 try_into_v4 2023-01-10 16:09:25 +08:00
RustDesk
338af1af9d Merge pull request #176 from botanicvelious/master
Add logging to the service files
2023-01-10 11:06:00 +08:00
botanicvelious
6538023a11 Update rustdesk-hbbr.service 2023-01-09 20:00:55 -07:00
botanicvelious
f139ad69a1 add logging to the .service file 2023-01-09 20:00:30 -07:00
rustdesk
55fcf241c6 try to_v4 in mangle encode 2023-01-09 14:52:29 +08:00
RustDesk
ebd73e1a09 remove RMEM and fix RUST_LOG 2023-01-08 11:31:13 +08:00
RustDesk
00e6a016f8 remove some env vars which normal users do not care 2023-01-08 11:29:43 +08:00
RustDesk
78d7e9437e Update test.yml 2023-01-07 12:50:24 +08:00
rustdesk
6bd5621fb0 fix ci 2023-01-07 12:38:21 +08:00
rustdesk
ee794d2e40 no gen_version if debug 2023-01-07 12:31:12 +08:00
rustdesk
605d0dd6c1 fix clippy 2023-01-07 11:59:53 +08:00
RustDesk
ebbe5d5297 Update test.yml 2023-01-07 11:51:14 +08:00
rustdesk
cd1a9885db test.yml 2023-01-07 00:58:51 +08:00
rustdesk
81d4fb6d6a fmt 2023-01-07 00:50:48 +08:00
rustdesk
a766aaf165 update .gitignore 2023-01-07 00:37:41 +08:00
rustdesk
55b841afb5 one more clippy 2023-01-07 00:37:12 +08:00
rustdesk
d48913d7b5 fix clippy 2023-01-07 00:32:10 +08:00
RustDesk
1557203912 Merge pull request #82 from dlhxzb/fix-clippy-warning
Fix: clippy warning in rust 1.62.1
2023-01-07 00:28:29 +08:00
RustDesk
0e01cfcd3a Merge branch 'master' into fix-clippy-warning 2023-01-07 00:28:18 +08:00
rustdesk
e70d82b30f ipv6 support draft 2023-01-06 20:31:15 +08:00
Bo Zhang
60a6d672c5 Fix: clippy warning in rust 1.66.0 2023-01-06 18:48:18 +09:00
RustDesk
d7b2060a5b Merge pull request #86 from dlhxzb/listern-for-unix-signal
Feat: listen for unix signal
2023-01-06 11:11:19 +08:00
RustDesk
75a40412b4 Merge branch 'master' into listern-for-unix-signal 2023-01-06 11:09:52 +08:00
rustdesk
93a89b8ea3 modify LOCAL_IP desc 2023-01-06 11:04:57 +08:00
RustDesk
8f5ce48939 Merge pull request #112 from paspo/envvars
Env vars
2023-01-06 11:02:21 +08:00
Huabing Zhou
2314783d42 sync rustdesk's hbb_common here 2023-01-06 10:40:26 +08:00
Paolo Asperti
e732599941 Merge remote-tracking branch 'origin/envvars' into envvars 2022-11-24 22:53:22 +01:00
Paolo Asperti
29b45dddb4 env variables doc 2022-11-24 22:52:56 +01:00
Paolo Asperti
650f2410ed hbbr can use ENV from docker 2022-11-24 22:52:56 +01:00
Paolo Asperti
c16101a44c env variables doc 2022-09-05 20:30:50 +02:00
Paolo Asperti
4baab96183 hbbr can use ENV from docker 2022-09-05 11:54:39 +02:00
dlhxzb
ca2bc99a38 Feat: listen for unix signal 2022-08-04 18:02:10 +09:00
87 changed files with 8159 additions and 725 deletions

8
.cargo/config.toml Normal file
View File

@@ -0,0 +1,8 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.'cfg(target_os="macos")']
rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
]

View File

@@ -26,7 +26,7 @@ jobs:
build:
name: Build - ${{ matrix.job.name }}
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
@@ -35,6 +35,7 @@ jobs:
- { name: "arm64v8", target: "aarch64-unknown-linux-musl" }
- { name: "armv7", target: "armv7-unknown-linux-musleabihf" }
- { name: "i386", target: "i686-unknown-linux-musl" }
- { name: "amd64fb", target: "x86_64-unknown-freebsd" }
steps:
@@ -44,7 +45,7 @@ jobs:
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: "1.62"
toolchain: "1.67.1"
override: true
default: true
components: rustfmt
@@ -95,16 +96,67 @@ jobs:
with:
command: build
args: --release --all-features --target=x86_64-pc-windows-msvc
use-cross: true
use-cross: true
- name: Install NSIS
run: |
iwr -useb get.scoop.sh -outfile 'install.ps1'
.\install.ps1 -RunAsAdmin
scoop update
scoop bucket add extras
scoop install nsis
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Sign exe files
uses: GermanBluefox/code-sign-action@v7
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
folder: 'target\x86_64-pc-windows-msvc\release'
recursive: false
- name: Build UI browser file
run: |
npm i
npm run build
working-directory: ./ui/html
- name: Build UI setup file
run: |
rustup default nightly
cargo build --release
xcopy /y ..\target\x86_64-pc-windows-msvc\release\*.exe setup\bin\
xcopy /y target\release\*.exe setup\
mkdir setup\logs
makensis /V1 setup.nsi
mkdir SignOutput
mv RustDeskServer.Setup.exe SignOutput\
mv ..\target\x86_64-pc-windows-msvc\release\*.exe SignOutput\
working-directory: ./ui
- name: Sign UI setup file
uses: GermanBluefox/code-sign-action@v7
with:
certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}'
password: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}'
folder: './ui/SignOutput'
recursive: false
- name: Publish Artifacts
uses: actions/upload-artifact@v3
with:
name: binaries-windows-x86_64
path: |
target\x86_64-pc-windows-msvc\release\hbbr.exe
target\x86_64-pc-windows-msvc\release\hbbs.exe
target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe
ui\SignOutput\hbbr.exe
ui\SignOutput\hbbs.exe
ui\SignOutput\rustdesk-utils.exe
ui\SignOutput\RustDeskServer.Setup.exe
if-no-files-found: error
# github (draft) release with all binaries
@@ -114,7 +166,7 @@ jobs:
needs:
- build
- build-win
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
@@ -123,8 +175,9 @@ jobs:
- { os: "linux", name: "arm64v8" }
- { os: "linux", name: "armv7" }
- { os: "linux", name: "i386" }
- { os: "linux", name: "amd64fb" }
- { os: "windows", name: "x86_64" }
steps:
- name: Download binaries (${{ matrix.job.os }} - ${{ matrix.job.name }})
@@ -153,7 +206,7 @@ jobs:
name: Docker push - ${{ matrix.job.name }}
needs: build
runs-on: ubuntu-22.04
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
@@ -223,7 +276,7 @@ jobs:
name: Docker manifest
needs: docker
runs-on: ubuntu-22.04
runs-on: ubuntu-18.04
steps:
@@ -275,7 +328,7 @@ jobs:
name: Docker push classic - ${{ matrix.job.name }}
needs: build
runs-on: ubuntu-22.04
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:

72
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: test
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: test
args: --all
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: build
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: clippy
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all -- -D warnings

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ debian/.debhelper
debian/debhelper-build-stamp
.DS_Store
.vscode
src/version.rs
db_v2.sqlite3
test.*

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"rust.checkWith": "clippy",
"rust.formatOnSave": true,
"rust.checkOnSave": true,
"rust.useNewErrorFormat": true
}

32
Cargo.lock generated
View File

@@ -197,6 +197,9 @@ name = "bytes"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e"
dependencies = [
"serde",
]
[[package]]
name = "cc"
@@ -458,6 +461,18 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "dns-lookup"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872"
dependencies = [
"cfg-if",
"libc",
"socket2 0.4.4",
"winapi",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@@ -738,6 +753,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"chrono",
"confy",
"directories-next",
"dirs-next",
@@ -748,6 +764,7 @@ dependencies = [
"lazy_static",
"log",
"mac_address",
"machine-uid",
"protobuf",
"protobuf-codegen",
"quinn",
@@ -768,7 +785,7 @@ dependencies = [
[[package]]
name = "hbbs"
version = "1.1.6"
version = "1.1.7-1"
dependencies = [
"async-speed-limit",
"async-trait",
@@ -778,6 +795,7 @@ dependencies = [
"chrono",
"clap",
"deadpool",
"dns-lookup",
"flexi_logger",
"hbb_common",
"headers",
@@ -790,6 +808,7 @@ dependencies = [
"machine-uid",
"minreq",
"once_cell",
"ping",
"regex",
"rust-ini",
"serde",
@@ -1440,6 +1459,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ping"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69044d1c00894fc1f43d9485aadb6ab6e68df90608fa52cf1074cda6420c6b76"
dependencies = [
"rand",
"socket2 0.4.4",
"thiserror",
]
[[package]]
name = "pkg-config"
version = "0.3.25"

View File

@@ -1,7 +1,7 @@
[package]
name = "hbbs"
version = "1.1.6"
authors = ["open-trade <info@rustdesk.com>"]
version = "1.1.7-1"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build = "build.rs"
default-run = "hbbs"
@@ -48,7 +48,7 @@ tower-http = { version = "0.3", features = ["fs", "trace", "cors"] }
http = "0.2"
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
ipnetwork = "0.20"
local-ip-address = "0.4"
local-ip-address = "0.5.1"
dns-lookup = "1.0.8"
ping = "0.4.0"
@@ -57,3 +57,4 @@ hbb_common = { path = "libs/hbb_common" }
[workspace]
members = ["libs/hbb_common"]
exclude = ["ui"]

335
README-NL.md Normal file
View File

@@ -0,0 +1,335 @@
# RustDesk Server Programa
[![build](https://github.com/rustdesk/rustdesk-server/actions/workflows/build.yaml/badge.svg)](https://github.com/rustdesk/rustdesk-server/actions/workflows/build.yaml)
[**Download**](https://github.com/rustdesk/rustdesk-server/releases)
[**Handleiding**](https://rustdesk.com/docs/nl/self-host/)
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
Zelf uw eigen RustDesk server hosten, het is gratis en open source.
## Hoe handmatig opbouwen
```bash
cargo build --release
```
In target/release worden drie uitvoerbare bestanden gegenereerd.
- hbbs - RustDesk ID/Rendezvous server
- hbbr - RustDesk relay server
- rustdesk-utils - RustDesk CLI hulpprogramma's
U kunt bijgewerkte binaries vinden op [releases](https://github.com/rustdesk/rustdesk-server/releases) pagina.
Als u uw eigen server wilt ontwikkelen, is [rustdesk-server-demo](https://github.com/rustdesk/rustdesk-server-demo) misschien een betere en eenvoudigere start voor u dan deze repo.
## Docker bestanden (images)
Docker bestanden (images) worden automatisch gegenereerd en gepubliceerd bij elke github release. We hebben 2 soorten bestanden (images).
### Klassiek bestand (image)
Deze bestanden (images) zijn gebouwd voor `ubuntu-20.04` met als enige toevoeging de belangrijkste binaries (`hbbr` en `hbbs`). Ze zijn beschikbaar op [Docker hub](https://hub.docker.com/r/rustdesk/rustdesk-server/) met deze tags:
| architectuur | image:tag |
| --- | --- |
| amd64 | `rustdesk/rustdesk-server:latest` |
| arm64v8 | `rustdesk/rustdesk-server:latest-arm64v8` |
U kunt deze bestanden (images) direct starten via `docker run` met deze commando's:
```bash
docker run --name hbbs --net=host -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbs -r <relay-server-ip[:port]>
docker run --name hbbr --net=host -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbr
```
of zonder --net=host, maar een directe P2P verbinding zal niet werken.
Voor systemen die SELinux gebruiken is het vervangen van `/root` door `/root:z` nodig om de containers correct te laten draaien. Als alternatief kan SELinux containerscheiding volledig worden uitgeschakeld door de optie `--security-opt label=disable` toe te voegen.
```bash
docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbs -r <relay-server-ip[:port]>
docker run --name hbbr -p 21117:21117 -p 21119:21119 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbr
```
De `relay-server-ip` parameter is het IP adres (of dns naam) van de server waarop deze containers draaien. De **optionele** `port` parameter moet gebruikt worden als je een andere poort dan **21117** gebruikt voor `hbbr`.
U kunt ook docker-compose gebruiken, met deze configuratie als sjabloon:
```yaml
version: '3'
networks:
rustdesk-net:
external: false
services:
hbbs:
container_name: hbbs
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21118:21118
image: rustdesk/rustdesk-server:latest
command: hbbs -r rustdesk.example.com:21117
volumes:
- ./data:/root
networks:
- rustdesk-net
depends_on:
- hbbr
restart: unless-stopped
hbbr:
container_name: hbbr
ports:
- 21117:21117
- 21119:21119
image: rustdesk/rustdesk-server:latest
command: hbbr
volumes:
- ./data:/root
networks:
- rustdesk-net
restart: unless-stopped
```
Bewerk regel 16 om te verwijzen naar uw relais-server (degene die luistert op poort 21117). U kunt ook de inhoudsregels (L18 en L33) bewerken indien nodig.
(docker-compose erkenning gaat naar @lukebarone en @QuiGonLeong)
## S6-overlay gebaseerde bestanden
Deze bestanden (images) zijn gebouwd tegen `busybox:stable` met toevoeging van de binaries (zowel hbbr als hbbs) en [S6-overlay](https://github.com/just-containers/s6-overlay). Ze zijn beschikbaar op [Docker hub](https://hub.docker.com/r/rustdesk/rustdesk-server-s6/) met deze tags:
| architectuur | versie | image:tag |
| --- | --- | --- |
| multiarch | latest | `rustdesk/rustdesk-server-s6:latest` |
| amd64 | latest | `rustdesk/rustdesk-server-s6:latest-amd64` |
| i386 | latest | `rustdesk/rustdesk-server-s6:latest-i386` |
| arm64v8 | latest | `rustdesk/rustdesk-server-s6:latest-arm64v8` |
| armv7 | latest | `rustdesk/rustdesk-server-s6:latest-armv7` |
| multiarch | 2 | `rustdesk/rustdesk-server-s6:2` |
| amd64 | 2 | `rustdesk/rustdesk-server-s6:2-amd64` |
| i386 | 2 | `rustdesk/rustdesk-server-s6:2-i386` |
| arm64v8 | 2 | `rustdesk/rustdesk-server-s6:2-arm64v8` |
| armv7 | 2 | `rustdesk/rustdesk-server-s6:2-armv7` |
| multiarch | 2.0.0 | `rustdesk/rustdesk-server-s6:2.0.0` |
| amd64 | 2.0.0 | `rustdesk/rustdesk-server-s6:2.0.0-amd64` |
| i386 | 2.0.0 | `rustdesk/rustdesk-server-s6:2.0.0-i386` |
| arm64v8 | 2.0.0 | `rustdesk/rustdesk-server-s6:2.0.0-arm64v8` |
| armv7 | 2.0.0 | `rustdesk/rustdesk-server-s6:2.0.0-armv7` |
Je wordt sterk aangeraden om het `multiarch` bestand (image) te gebruiken met de `major version` of `latest` tag.
De S6-overlay fungeert als supervisor en houdt beide processen draaiende, dus met dit bestand (image) is het niet nodig om twee aparte draaiende containers te hebben.
U kunt deze bestanden (images) direct starten via `docker run` met dit commando:
```bash
docker run --name rustdesk-server \
--net=host \
-e "RELAY=rustdeskrelay.example.com" \
-e "ENCRYPTED_ONLY=1" \
-v "$PWD/data:/data" -d rustdesk/rustdesk-server-s6:latest
```
of zonder --net=host, maar een directe P2P verbinding zal niet werken.
```bash
docker run --name rustdesk-server \
-p 21115:21115 -p 21116:21116 -p 21116:21116/udp \
-p 21117:21117 -p 21118:21118 -p 21119:21119 \
-e "RELAY=rustdeskrelay.example.com" \
-e "ENCRYPTED_ONLY=1" \
-v "$PWD/data:/data" -d rustdesk/rustdesk-server-s6:latest
```
Of u kunt een docker-compose bestand gebruiken:
```yaml
version: '3'
services:
rustdesk-server:
container_name: rustdesk-server
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: rustdesk/rustdesk-server-s6:latest
environment:
- "RELAY=rustdesk.example.com:21117"
- "ENCRYPTED_ONLY=1"
volumes:
- ./data:/data
restart: unless-stopped
```
Voor dit container bestand (image) kunt u deze omgevingsvariabelen gebruiken, **naast** de variabelen in de volgende **ENV-variabelen** sectie:
| variabele | optioneel | beschrijving |
| --- | --- | --- |
| RELAY | no | het IP-adres/DNS-naam van de machine waarop deze container draait |
| ENCRYPTED_ONLY | yes | indien ingesteld op **"1"** wordt een niet-versleutelde verbinding niet geaccepteerd |
| KEY_PUB | yes | het openbare deel van het key paar |
| KEY_PRIV | yes | het private deel van het key paar |
### Geheim beheer in S6-overlay gebaseerde bestanden (images)
U kunt uiteraard het key paar bewaren in een docker volume, maar de optimale werkwijzen vertellen u om de keys niet op het bestandssysteem te schrijven; dus bieden we een paar opties.
Bij het opstarten van de container wordt de aanwezigheid van het key paar gecontroleerd (`/data/id_ed25519.pub` en `/data/id_ed25519`) en als een van deze keys niet bestaat, wordt deze opnieuw aangemaakt vanuit ENV variabelen of docker secrets.
Vervolgens wordt de geldigheid van het key paar gecontroleerd: indien publieke en private keys niet overeenkomen, stopt de container.
Als je geen keys opgeeft, zal `hbbs` er een voor je genereren en op de standaard locatie plaatsen.
#### Gebruik ENV om het key paar op te slaan
U kunt docker omgevingsvariabelen gebruiken om de keys op te slaan. Volg gewoon deze voorbeelden:
```bash
docker run --name rustdesk-server \
--net=host \
-e "RELAY=rustdeskrelay.example.com" \
-e "ENCRYPTED_ONLY=1" \
-e "DB_URL=/db/db_v2.sqlite3" \
-e "KEY_PRIV=FR2j78IxfwJNR+HjLluQ2Nh7eEryEeIZCwiQDPVe+PaITKyShphHAsPLn7So0OqRs92nGvSRdFJnE2MSyrKTIQ==" \
-e "KEY_PUB=iEyskoaYRwLDy5+0qNDqkbPdpxr0kXRSZxNjEsqykyE=" \
-v "$PWD/db:/db" -d rustdesk/rustdesk-server-s6:latest
```
```yaml
version: '3'
services:
rustdesk-server:
container_name: rustdesk-server
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: rustdesk/rustdesk-server-s6:latest
environment:
- "RELAY=rustdesk.example.com:21117"
- "ENCRYPTED_ONLY=1"
- "DB_URL=/db/db_v2.sqlite3"
- "KEY_PRIV=FR2j78IxfwJNR+HjLluQ2Nh7eEryEeIZCwiQDPVe+PaITKyShphHAsPLn7So0OqRs92nGvSRdFJnE2MSyrKTIQ=="
- "KEY_PUB=iEyskoaYRwLDy5+0qNDqkbPdpxr0kXRSZxNjEsqykyE="
volumes:
- ./db:/db
restart: unless-stopped
```
#### Gebruik Docker secrets om het key paar op te slaan
U kunt ook docker secrets gebruiken om de keys op te slaan.
Dit is handig als je **docker-compose** of **docker swarm** gebruikt.
Volg deze voorbeelden:
```bash
cat secrets/id_ed25519.pub | docker secret create key_pub -
cat secrets/id_ed25519 | docker secret create key_priv -
docker service create --name rustdesk-server \
--secret key_priv --secret key_pub \
--net=host \
-e "RELAY=rustdeskrelay.example.com" \
-e "ENCRYPTED_ONLY=1" \
-e "DB_URL=/db/db_v2.sqlite3" \
--mount "type=bind,source=$PWD/db,destination=/db" \
rustdesk/rustdesk-server-s6:latest
```
```yaml
version: '3'
services:
rustdesk-server:
container_name: rustdesk-server
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: rustdesk/rustdesk-server-s6:latest
environment:
- "RELAY=rustdesk.example.com:21117"
- "ENCRYPTED_ONLY=1"
- "DB_URL=/db/db_v2.sqlite3"
volumes:
- ./db:/db
restart: unless-stopped
secrets:
- key_pub
- key_priv
secrets:
key_pub:
file: secrets/id_ed25519.pub
key_priv:
file: secrets/id_ed25519
```
## Hoe maak je een key paar
Een key paar is nodig voor encryptie; u kunt het verstrekken, zoals eerder uitgelegd, maar u heeft een manier nodig om er een te maken.
U kunt dit commando gebruiken om een key paar te genereren:
```bash
/usr/bin/rustdesk-utils genkeypair
```
Als u het pakket `rustdesk-utils` niet op uw systeem hebt staan (of wilt), kunt u hetzelfde commando met docker uitvoeren:
```bash
docker run --rm --entrypoint /usr/bin/rustdesk-utils rustdesk/rustdesk-server-s6:latest genkeypair
```
De uitvoer ziet er ongeveer zo uit:
```text
Public Key: 8BLLhtzUBU/XKAH4mep3p+IX4DSApe7qbAwNH9nv4yA=
Secret Key: egAVd44u33ZEUIDTtksGcHeVeAwywarEdHmf99KM5ajwEsuG3NQFT9coAfiZ6nen4hfgNICl7upsDA0f2e/jIA==
```
## .deb pakketten
Voor elke binary zijn aparte .deb-pakketten beschikbaar, u kunt ze vinden in de [releases](https://github.com/rustdesk/rustdesk-server/releases).
Deze pakketten zijn bedoeld voor de volgende distributies:
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 11 bullseye
- Debian 10 buster
## ENV variabelen
hbbs en hbbr kunnen worden geconfigureerd met deze ENV-variabelen.
U kunt de variabelen zoals gebruikelijk opgeven of een `.env` bestand gebruiken.
| variabele | binary | beschrijving |
| --- | --- | --- |
| ALWAYS_USE_RELAY | hbbs | indien ingesteld op **"Y"** wordt directe peer-verbinding niet toegestaan |
| DB_URL | hbbs | path voor database bestand |
| DOWNGRADE_START_CHECK | hbbr | vertraging (in seconden) voor downgrade-controle |
| DOWNGRADE_THRESHOLD | hbbr | drempel van downgrade controle (bit/ms) |
| KEY | hbbs/hbbr | indien ingesteld forceert dit het gebruik van een specifieke toets, indien ingesteld op **"_"** forceert dit het gebruik van een willekeurige toets |
| LIMIT_SPEED | hbbr | snelheidslimiet (in Mb/s) |
| PORT | hbbs/hbbr | luister-poort (21116 voor hbbs - 21117 voor hbbr) |
| RELAY_SERVERS | hbbs | IP-adres/DNS-naam van de machines waarop hbbr draait (gescheiden door komma) |
| RUST_LOG | all | debug-niveau instellen (error|warn|info|debug|trace) |
| SINGLE_BANDWIDTH | hbbr | maximale bandbreedte voor een enkele verbinding (in Mb/s) |
| TOTAL_BANDWIDTH | hbbr | maximale totale bandbreedte (in Mb/s) |

View File

@@ -173,16 +173,14 @@ services:
restart: unless-stopped
```
We use these environment variables:
For this container image, you can use these environment variables, **in addition** to the ones specified in the following **ENV variables** section:
| variable | optional | description |
| --- | --- | --- |
| RELAY | no | the IP address/DNS name of the machine running this container |
| ENCRYPTED_ONLY | yes | if set to **"1"** unencrypted connection will not be accepted |
| DB_URL | yes | path for database file |
| KEY_PUB | yes | public part of the key pair |
| KEY_PRIV | yes | private part of the key pair |
| RUST_LOG | yes | set debug level (error|warn|info|debug|trace) |
### Secret management in S6-overlay based images
@@ -316,3 +314,22 @@ These packages are meant for the following distributions:
- Ubuntu 18.04 LTS
- Debian 11 bullseye
- Debian 10 buster
## ENV variables
hbbs and hbbr can be configured using these ENV variables.
You can specify the variables as usual or use an `.env` file.
| variable | binary | description |
| --- | --- | --- |
| ALWAYS_USE_RELAY | hbbs | if set to **"Y"** disallows direct peer connection |
| DB_URL | hbbs | path for database file |
| DOWNGRADE_START_CHECK | hbbr | delay (in seconds) before downgrade check |
| DOWNGRADE_THRESHOLD | hbbr | threshold of downgrade check (bit/ms) |
| KEY | hbbs/hbbr | if set force the use of a specific key, if set to **"_"** force the use of any key |
| LIMIT_SPEED | hbbr | speed limit (in Mb/s) |
| PORT | hbbs/hbbr | listening port (21116 for hbbs - 21117 for hbbr) |
| RELAY_SERVERS | hbbs | IP address/DNS name of the machines running hbbr (separated by comma) |
| RUST_LOG | all | set debug level (error\|warn\|info\|debug\|trace) |
| SINGLE_BANDWIDTH | hbbr | max bandwidth for a single connection (in Mb/s) |
| TOTAL_BANDWIDTH | hbbr | max total bandwidth (in Mb/s) |

Binary file not shown.

6
debian/changelog vendored
View File

@@ -1,3 +1,9 @@
rustdesk-server (1.1.7) UNRELEASED; urgency=medium
* ipv6 support
-- rustdesk <info@rustdesk.com> Wed, 11 Jan 2023 11:27:00 +0800
rustdesk-server (1.1.6) UNRELEASED; urgency=medium
* Initial release

View File

@@ -3,6 +3,10 @@ set -e
SERVICE=rustdesk-hbbr.service
if [ "$1" = "configure" ]; then
mkdir -p /var/log/rustdesk
fi
case "$1" in
configure|abort-upgrade|abort-deconfigure|abort-remove)
mkdir -p /var/lib/rustdesk-server/

View File

@@ -6,7 +6,7 @@ SERVICE=rustdesk-hbbr.service
systemctl --system daemon-reload >/dev/null || true
if [ "$1" = "purge" ]; then
rm -rf /var/lib/rustdesk-server/
rm -rf /var/lib/rustdesk-server/ /var/log/rustdesk/rustdesk-hbbr.* /var/log/rustdesk/rustdesk-hbbs.*
deb-systemd-helper purge "${SERVICE}" >/dev/null || true
deb-systemd-helper unmask "${SERVICE}" >/dev/null || true
fi

View File

@@ -3,6 +3,10 @@ set -e
SERVICE=rustdesk-hbbs.service
if [ "$1" = "configure" ]; then
mkdir -p /var/log/rustdesk
fi
case "$1" in
configure|abort-upgrade|abort-deconfigure|abort-remove)
mkdir -p /var/lib/rustdesk-server/

View File

@@ -1,3 +1,5 @@
#!/command/execlineb -P
posix-cd /data
/usr/bin/hbbr
#!/command/with-contenv sh
cd /data
PARAMS=
[ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k _"
/usr/bin/hbbr $PARAMS

View File

@@ -11,7 +11,7 @@ protobuf = { version = "3.1", features = ["with-bytes"] }
tokio = { version = "1.20", features = ["full"] }
tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
bytes = "1.2"
bytes = { version = "1.2", features = ["serde"] }
log = "0.4"
env_logger = "0.9"
socket2 = { version = "0.3", features = ["reuseport"] }
@@ -30,15 +30,18 @@ filetime = "0.2"
sodiumoxide = "0.2"
regex = "1.4"
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
chrono = "0.4"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
machine-uid = "0.2"
[features]
quic = []
flatpak = []
[build-dependencies]
protobuf-codegen = "3.1"
protobuf-codegen = { version = "3.1" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }

View File

@@ -1,14 +1,14 @@
fn main() {
std::fs::create_dir_all("src/protos").unwrap();
let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap());
std::fs::create_dir_all(&out_dir).unwrap();
protobuf_codegen::Codegen::new()
.pure()
.out_dir("src/protos")
.inputs(&["protos/rendezvous.proto", "protos/message.proto"])
.out_dir(out_dir)
.inputs(["protos/rendezvous.proto", "protos/message.proto"])
.include("protos")
.customize(
protobuf_codegen::Customize::default()
.tokio_bytes(true)
)
.customize(protobuf_codegen::Customize::default().tokio_bytes(true))
.run()
.expect("Codegen failed.");
}

View File

@@ -1,13 +1,13 @@
syntax = "proto3";
package hbb;
message VP9 {
message EncodedVideoFrame {
bytes data = 1;
bool key = 2;
int64 pts = 3;
}
message VP9s { repeated VP9 frames = 1; }
message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
message RGB { bool compress = 1; }
@@ -19,9 +19,11 @@ message YUV {
message VideoFrame {
oneof union {
VP9s vp9s = 6;
EncodedVideoFrames vp9s = 6;
RGB rgb = 7;
YUV yuv = 8;
EncodedVideoFrames h264s = 10;
EncodedVideoFrames h265s = 11;
}
int64 timestamp = 9;
}
@@ -38,6 +40,7 @@ message DisplayInfo {
int32 height = 4;
string name = 5;
bool online = 6;
bool cursor_embedded = 7;
}
message PortForward {
@@ -61,10 +64,21 @@ message LoginRequest {
PortForward port_forward = 8;
}
bool video_ack_required = 9;
uint64 session_id = 10;
string version = 11;
}
message ChatMessage { string text = 1; }
message Features {
bool privacy_mode = 1;
}
message SupportedEncoding {
bool h264 = 1;
bool h265 = 2;
}
message PeerInfo {
string username = 1;
string hostname = 2;
@@ -74,6 +88,8 @@ message PeerInfo {
bool sas_enabled = 6;
string version = 7;
int32 conn_id = 8;
Features features = 9;
SupportedEncoding encoding = 10;
}
message LoginResponse {
@@ -90,6 +106,13 @@ message MouseEvent {
repeated ControlKey modifiers = 4;
}
enum KeyboardMode{
Legacy = 0;
Map = 1;
Translate = 2;
Auto = 3;
}
enum ControlKey {
Unknown = 0;
Alt = 1;
@@ -183,6 +206,7 @@ message KeyEvent {
string seq = 6;
}
repeated ControlKey modifiers = 8;
KeyboardMode mode = 9;
}
message CursorData {
@@ -252,6 +276,7 @@ message FileAction {
FileRemoveFile remove_file = 6;
ReadAllFiles all_files = 7;
FileTransferCancel cancel = 8;
FileTransferSendConfirmRequest send_confirm = 9;
}
}
@@ -263,14 +288,24 @@ message FileResponse {
FileTransferBlock block = 2;
FileTransferError error = 3;
FileTransferDone done = 4;
FileTransferDigest digest = 5;
}
}
message FileTransferDigest {
int32 id = 1;
sint32 file_num = 2;
uint64 last_modified = 3;
uint64 file_size = 4;
bool is_upload = 5;
}
message FileTransferBlock {
int32 id = 1;
sint32 file_num = 2;
bytes data = 3;
bool compressed = 4;
uint32 blk_id = 5;
}
message FileTransferError {
@@ -283,6 +318,16 @@ message FileTransferSendRequest {
int32 id = 1;
string path = 2;
bool include_hidden = 3;
int32 file_num = 4;
}
message FileTransferSendConfirmRequest {
int32 id = 1;
sint32 file_num = 2;
oneof union {
bool skip = 3;
uint32 offset_blk = 4;
}
}
message FileTransferDone {
@@ -294,6 +339,7 @@ message FileTransferReceiveRequest {
int32 id = 1;
string path = 2; // path written to
repeated FileEntry files = 3;
int32 file_num = 4;
}
message FileRemoveDir {
@@ -315,38 +361,31 @@ message FileDirCreate {
// main logic from freeRDP
message CliprdrMonitorReady {
int32 conn_id = 1;
}
message CliprdrFormat {
int32 conn_id = 1;
int32 id = 2;
string format = 3;
}
message CliprdrServerFormatList {
int32 conn_id = 1;
repeated CliprdrFormat formats = 2;
}
message CliprdrServerFormatListResponse {
int32 conn_id = 1;
int32 msg_flags = 2;
}
message CliprdrServerFormatDataRequest {
int32 conn_id = 1;
int32 requested_format_id = 2;
}
message CliprdrServerFormatDataResponse {
int32 conn_id = 1;
int32 msg_flags = 2;
bytes format_data = 3;
}
message CliprdrFileContentsRequest {
int32 conn_id = 1;
int32 stream_id = 2;
int32 list_index = 3;
int32 dw_flags = 4;
@@ -358,7 +397,6 @@ message CliprdrFileContentsRequest {
}
message CliprdrFileContentsResponse {
int32 conn_id = 1;
int32 msg_flags = 3;
int32 stream_id = 4;
bytes requested_data = 5;
@@ -382,6 +420,7 @@ message SwitchDisplay {
sint32 y = 3;
int32 width = 4;
int32 height = 5;
bool cursor_embedded = 6;
}
message PermissionInfo {
@@ -390,6 +429,8 @@ message PermissionInfo {
Clipboard = 2;
Audio = 3;
File = 4;
Restart = 5;
Recording = 6;
}
Permission permission = 1;
@@ -403,6 +444,20 @@ enum ImageQuality {
Best = 4;
}
message VideoCodecState {
enum PreferCodec {
Auto = 0;
VPX = 1;
H264 = 2;
H265 = 3;
}
int32 score_vpx = 1;
int32 score_h264 = 2;
int32 score_h265 = 3;
PreferCodec prefer = 4;
}
message OptionMessage {
enum BoolOption {
NotSet = 0;
@@ -418,16 +473,15 @@ message OptionMessage {
BoolOption disable_audio = 7;
BoolOption disable_clipboard = 8;
BoolOption enable_file_transfer = 9;
}
message OptionResponse {
OptionMessage opt = 1;
string error = 2;
VideoCodecState video_codec_state = 10;
int32 custom_fps = 11;
}
message TestDelay {
int64 time = 1;
bool from_client = 2;
uint32 last_delay = 3;
uint32 target_bitrate = 4;
}
message PublicKey {
@@ -447,6 +501,80 @@ message AudioFrame {
int64 timestamp = 2;
}
// Notify peer to show message box.
message MessageBox {
// Message type. Refer to flutter/lib/common.dart/msgBox().
string msgtype = 1;
string title = 2;
// English
string text = 3;
// If not empty, msgbox provides a button to following the link.
// The link here can't be directly http url.
// It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
string link = 4;
}
message BackNotification {
// no need to consider block input by someone else
enum BlockInputState {
BlkStateUnknown = 0;
BlkOnSucceeded = 2;
BlkOnFailed = 3;
BlkOffSucceeded = 4;
BlkOffFailed = 5;
}
enum PrivacyModeState {
PrvStateUnknown = 0;
// Privacy mode on by someone else
PrvOnByOther = 2;
// Privacy mode is not supported on the remote side
PrvNotSupported = 3;
// Privacy mode on by self
PrvOnSucceeded = 4;
// Privacy mode on by self, but denied
PrvOnFailedDenied = 5;
// Some plugins are not found
PrvOnFailedPlugin = 6;
// Privacy mode on by self, but failed
PrvOnFailed = 7;
// Privacy mode off by self
PrvOffSucceeded = 8;
// Ctrl + P
PrvOffByPeer = 9;
// Privacy mode off by self, but failed
PrvOffFailed = 10;
PrvOffUnknown = 11;
}
oneof union {
PrivacyModeState privacy_mode_state = 1;
BlockInputState block_input_state = 2;
}
}
message ElevationRequestWithLogon {
string username = 1;
string password = 2;
}
message ElevationRequest {
oneof union {
bool direct = 1;
ElevationRequestWithLogon logon = 2;
}
}
message SwitchSidesRequest {
bytes uuid = 1;
}
message SwitchSidesResponse {
bytes uuid = 1;
LoginRequest lr = 2;
}
message SwitchBack {}
message Misc {
oneof union {
ChatMessage chat_message = 4;
@@ -456,11 +584,32 @@ message Misc {
AudioFormat audio_format = 8;
string close_reason = 9;
bool refresh_video = 10;
OptionResponse option_response = 11;
bool video_received = 12;
BackNotification back_notification = 13;
bool restart_remote_device = 14;
bool uac = 15;
bool foreground_window_elevated = 16;
bool stop_service = 17;
ElevationRequest elevation_request = 18;
string elevation_response = 19;
bool portable_service_running = 20;
SwitchSidesRequest switch_sides_request = 21;
SwitchBack switch_back = 22;
}
}
message VoiceCallRequest {
int64 req_timestamp = 1;
// Indicates whether the request is a connect action or a disconnect action.
bool is_connect = 2;
}
message VoiceCallResponse {
bool accepted = 1;
int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
int64 ack_timestamp = 3;
}
message Message {
oneof union {
SignedId signed_id = 3;
@@ -481,5 +630,9 @@ message Message {
FileResponse file_response = 18;
Misc misc = 19;
Cliprdr cliprdr = 20;
MessageBox message_box = 21;
SwitchSidesResponse switch_sides_response = 22;
VoiceCallRequest voice_call_request = 23;
VoiceCallResponse voice_call_response = 24;
}
}

View File

@@ -15,6 +15,12 @@ enum DecodeState {
Data(usize),
}
impl Default for BytesCodec {
fn default() -> Self {
Self::new()
}
}
impl BytesCodec {
pub fn new() -> Self {
Self {
@@ -56,7 +62,7 @@ impl BytesCodec {
}
src.advance(head_len);
src.reserve(n);
return Ok(Some(n));
Ok(Some(n))
}
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
@@ -137,32 +143,32 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3F, 1);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3F + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
assert!(false);
panic!();
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
panic!();
}
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
panic!();
}
buf2.extend(&buf_saved[1..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3F);
assert_eq!(res[0], 1);
} else {
assert!(false);
panic!();
}
}
@@ -171,21 +177,21 @@ mod tests {
let mut codec = BytesCodec::new();
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
assert!(!codec.encode("".into(), &mut buf).is_err());
assert!(codec.encode("".into(), &mut buf).is_ok());
assert_eq!(buf.len(), 1);
bytes.resize(0x3F + 1, 2);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3F + 2 + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0);
} else {
assert!(false);
panic!();
}
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F + 1);
assert_eq!(res[0], 2);
} else {
assert!(false);
panic!();
}
}
@@ -195,13 +201,13 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3F - 1, 3);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3F + 1 - 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3F - 1);
assert_eq!(res[0], 3);
} else {
assert!(false);
panic!();
}
}
#[test]
@@ -210,13 +216,13 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFF, 4);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3FFF + 2);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFF);
assert_eq!(res[0], 4);
} else {
assert!(false);
panic!();
}
}
@@ -226,13 +232,13 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFFFF, 5);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
assert_eq!(buf.len(), 0x3FFFFF + 3);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF);
assert_eq!(res[0], 5);
} else {
assert!(false);
panic!();
}
}
@@ -242,33 +248,33 @@ mod tests {
let mut buf = BytesMut::new();
let mut bytes: Vec<u8> = Vec::new();
bytes.resize(0x3FFFFF + 1, 6);
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
let buf_saved = buf.clone();
assert_eq!(buf.len(), 0x3FFFFF + 4 + 1);
if let Ok(Some(res)) = codec.decode(&mut buf) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
assert!(false);
panic!();
}
let mut codec2 = BytesCodec::new();
let mut buf2 = BytesMut::new();
buf2.extend(&buf_saved[0..1]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
panic!();
}
buf2.extend(&buf_saved[1..6]);
if let Ok(None) = codec2.decode(&mut buf2) {
} else {
assert!(false);
panic!();
}
buf2.extend(&buf_saved[6..]);
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
assert_eq!(res.len(), 0x3FFFFF + 1);
assert_eq!(res[0], 6);
} else {
assert!(false);
panic!();
}
}
}

View File

@@ -32,12 +32,7 @@ pub fn decompress(data: &[u8]) -> Vec<u8> {
const MAX: usize = 1024 * 1024 * 64;
const MIN: usize = 1024 * 1024;
let mut n = 30 * data.len();
if n > MAX {
n = MAX;
}
if n < MIN {
n = MIN;
}
n = n.clamp(MIN, MAX);
match d.decompress(data, n) {
Ok(res) => out = res,
Err(err) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,25 @@
use crate::{bail, message_proto::*, ResultType};
#[cfg(windows)]
use std::os::windows::prelude::*;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_derive::{Deserialize, Serialize};
use tokio::{fs::File, io::*};
use crate::{bail, get_version_number, message_proto::*, ResultType, Stream};
// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
use crate::{
compress::{compress, decompress},
config::{Config, COMPRESS_LEVEL},
};
#[cfg(windows)]
use std::os::windows::prelude::*;
use tokio::{fs::File, io::*};
pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType<FileDirectory> {
pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType<FileDirectory> {
let mut dir = FileDirectory {
path: get_string(&path),
path: get_string(path),
..Default::default()
};
#[cfg(windows)]
if "/" == &get_string(&path) {
if "/" == &get_string(path) {
let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() };
for i in 0..32 {
if drives & (1 << i) != 0 {
@@ -32,74 +36,70 @@ pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType<FileDirector
}
return Ok(dir);
}
for entry in path.read_dir()? {
if let Ok(entry) = entry {
let p = entry.path();
let name = p
.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned();
if name.is_empty() {
continue;
}
let mut is_hidden = false;
let meta;
if let Ok(tmp) = std::fs::symlink_metadata(&p) {
meta = tmp;
} else {
continue;
}
// docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
#[cfg(windows)]
if meta.file_attributes() & 0x2 != 0 {
is_hidden = true;
}
#[cfg(not(windows))]
if name.find('.').unwrap_or(usize::MAX) == 0 {
is_hidden = true;
}
if is_hidden && !include_hidden {
continue;
}
let (entry_type, size) = {
if p.is_dir() {
if meta.file_type().is_symlink() {
(FileType::DirLink.into(), 0)
} else {
(FileType::Dir.into(), 0)
}
} else {
if meta.file_type().is_symlink() {
(FileType::FileLink.into(), 0)
} else {
(FileType::File.into(), meta.len())
}
}
};
let modified_time = meta
.modified()
.map(|x| {
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0) as u64;
dir.entries.push(FileEntry {
name: get_file_name(&p),
entry_type,
is_hidden,
size,
modified_time,
..Default::default()
});
for entry in path.read_dir()?.flatten() {
let p = entry.path();
let name = p
.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
.to_owned();
if name.is_empty() {
continue;
}
let mut is_hidden = false;
let meta;
if let Ok(tmp) = std::fs::symlink_metadata(&p) {
meta = tmp;
} else {
continue;
}
// docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
#[cfg(windows)]
if meta.file_attributes() & 0x2 != 0 {
is_hidden = true;
}
#[cfg(not(windows))]
if name.find('.').unwrap_or(usize::MAX) == 0 {
is_hidden = true;
}
if is_hidden && !include_hidden {
continue;
}
let (entry_type, size) = {
if p.is_dir() {
if meta.file_type().is_symlink() {
(FileType::DirLink.into(), 0)
} else {
(FileType::Dir.into(), 0)
}
} else if meta.file_type().is_symlink() {
(FileType::FileLink.into(), 0)
} else {
(FileType::File.into(), meta.len())
}
};
let modified_time = meta
.modified()
.map(|x| {
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0);
dir.entries.push(FileEntry {
name: get_file_name(&p),
entry_type,
is_hidden,
size,
modified_time,
..Default::default()
});
}
Ok(dir)
}
#[inline]
pub fn get_file_name(p: &PathBuf) -> String {
pub fn get_file_name(p: &Path) -> String {
p.file_name()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("")
@@ -107,7 +107,7 @@ pub fn get_file_name(p: &PathBuf) -> String {
}
#[inline]
pub fn get_string(path: &PathBuf) -> String {
pub fn get_string(path: &Path) -> String {
path.to_str().unwrap_or("").to_owned()
}
@@ -123,14 +123,14 @@ pub fn get_home_as_string() -> String {
fn read_dir_recursive(
path: &PathBuf,
prefix: &PathBuf,
prefix: &Path,
include_hidden: bool,
) -> ResultType<Vec<FileEntry>> {
let mut files = Vec::new();
if path.is_dir() {
// to-do: symbol link handling, cp the link rather than the content
// to-do: file mode, for unix
let fd = read_dir(&path, include_hidden)?;
let fd = read_dir(path, include_hidden)?;
for entry in fd.entries.iter() {
match entry.entry_type.enum_value() {
Ok(FileType::File) => {
@@ -154,7 +154,7 @@ fn read_dir_recursive(
}
Ok(files)
} else if path.is_file() {
let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) {
let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) {
(
meta.len(),
meta.modified()
@@ -163,7 +163,7 @@ fn read_dir_recursive(
.map(|x| x.as_secs())
.unwrap_or(0)
})
.unwrap_or(0) as u64,
.unwrap_or(0),
)
} else {
(0, 0)
@@ -184,21 +184,68 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<F
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
}
#[inline]
pub fn is_file_exists(file_path: &str) -> bool {
return Path::new(file_path).exists();
}
#[inline]
pub fn can_enable_overwrite_detection(version: i64) -> bool {
version >= get_version_number("1.1.10")
}
#[derive(Default)]
pub struct TransferJob {
id: i32,
path: PathBuf,
files: Vec<FileEntry>,
file_num: i32,
pub id: i32,
pub remote: String,
pub path: PathBuf,
pub show_hidden: bool,
pub is_remote: bool,
pub is_last_job: bool,
pub file_num: i32,
pub files: Vec<FileEntry>,
file: Option<File>,
total_size: u64,
finished_size: u64,
transferred: u64,
enable_overwrite_detection: bool,
file_confirmed: bool,
// indicating the last file is skipped
file_skipped: bool,
file_is_waiting: bool,
default_overwrite_strategy: Option<bool>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct TransferJobMeta {
#[serde(default)]
pub id: i32,
#[serde(default)]
pub remote: String,
#[serde(default)]
pub to: String,
#[serde(default)]
pub show_hidden: bool,
#[serde(default)]
pub file_num: i32,
#[serde(default)]
pub is_remote: bool,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct RemoveJobMeta {
#[serde(default)]
pub path: String,
#[serde(default)]
pub is_remote: bool,
#[serde(default)]
pub no_confirm: bool,
}
#[inline]
fn get_ext(name: &str) -> &str {
if let Some(i) = name.rfind(".") {
if let Some(i) = name.rfind('.') {
return &name[i + 1..];
}
""
@@ -219,25 +266,55 @@ fn is_compressed_file(name: &str) -> bool {
}
impl TransferJob {
pub fn new_write(id: i32, path: String, files: Vec<FileEntry>) -> Self {
let total_size = files.iter().map(|x| x.size as u64).sum();
#[allow(clippy::too_many_arguments)]
pub fn new_write(
id: i32,
remote: String,
path: String,
file_num: i32,
show_hidden: bool,
is_remote: bool,
files: Vec<FileEntry>,
enable_overwrite_detection: bool,
) -> Self {
log::info!("new write {}", path);
let total_size = files.iter().map(|x| x.size).sum();
Self {
id,
remote,
path: get_path(&path),
file_num,
show_hidden,
is_remote,
files,
total_size,
enable_overwrite_detection,
..Default::default()
}
}
pub fn new_read(id: i32, path: String, include_hidden: bool) -> ResultType<Self> {
let files = get_recursive_files(&path, include_hidden)?;
let total_size = files.iter().map(|x| x.size as u64).sum();
pub fn new_read(
id: i32,
remote: String,
path: String,
file_num: i32,
show_hidden: bool,
is_remote: bool,
enable_overwrite_detection: bool,
) -> ResultType<Self> {
log::info!("new read {}", path);
let files = get_recursive_files(&path, show_hidden)?;
let total_size = files.iter().map(|x| x.size).sum();
Ok(Self {
id,
remote,
path: get_path(&path),
file_num,
show_hidden,
is_remote,
files,
total_size,
enable_overwrite_detection,
..Default::default()
})
}
@@ -283,7 +360,7 @@ impl TransferJob {
let entry = &self.files[file_num];
let path = self.join(&entry.name);
let download_path = format!("{}.download", get_string(&path));
std::fs::rename(&download_path, &path).ok();
std::fs::rename(download_path, &path).ok();
filetime::set_file_mtime(
&path,
filetime::FileTime::from_unix_time(entry.modified_time as _, 0),
@@ -298,11 +375,11 @@ impl TransferJob {
let entry = &self.files[file_num];
let path = self.join(&entry.name);
let download_path = format!("{}.download", get_string(&path));
std::fs::remove_file(&download_path).ok();
std::fs::remove_file(download_path).ok();
}
}
pub async fn write(&mut self, block: FileTransferBlock, raw: Option<&[u8]>) -> ResultType<()> {
pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> {
if block.id != self.id {
bail!("Wrong id");
}
@@ -324,25 +401,20 @@ impl TransferJob {
let path = format!("{}.download", get_string(&path));
self.file = Some(File::create(&path).await?);
}
let data = if let Some(data) = raw {
data
} else {
&block.data
};
if block.compressed {
let tmp = decompress(data);
let tmp = decompress(&block.data);
self.file.as_mut().unwrap().write_all(&tmp).await?;
self.finished_size += tmp.len() as u64;
} else {
self.file.as_mut().unwrap().write_all(data).await?;
self.finished_size += data.len() as u64;
self.file.as_mut().unwrap().write_all(&block.data).await?;
self.finished_size += block.data.len() as u64;
}
self.transferred += data.len() as u64;
self.transferred += block.data.len() as u64;
Ok(())
}
#[inline]
fn join(&self, name: &str) -> PathBuf {
pub fn join(&self, name: &str) -> PathBuf {
if name.is_empty() {
self.path.clone()
} else {
@@ -350,7 +422,7 @@ impl TransferJob {
}
}
pub async fn read(&mut self) -> ResultType<Option<FileTransferBlock>> {
pub async fn read(&mut self, stream: &mut Stream) -> ResultType<Option<FileTransferBlock>> {
let file_num = self.file_num as usize;
if file_num >= self.files.len() {
self.file.take();
@@ -358,21 +430,29 @@ impl TransferJob {
}
let name = &self.files[file_num].name;
if self.file.is_none() {
match File::open(self.join(&name)).await {
match File::open(self.join(name)).await {
Ok(file) => {
self.file = Some(file);
self.file_confirmed = false;
self.file_is_waiting = false;
}
Err(err) => {
self.file_num += 1;
self.file_confirmed = false;
self.file_is_waiting = false;
return Err(err.into());
}
}
}
const BUF_SIZE: usize = 128 * 1024;
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
unsafe {
buf.set_len(BUF_SIZE);
if self.enable_overwrite_detection && !self.file_confirmed() {
if !self.file_is_waiting() {
self.send_current_digest(stream).await?;
self.set_file_is_waiting(true);
}
return Ok(None);
}
const BUF_SIZE: usize = 128 * 1024;
let mut buf: Vec<u8> = vec![0; BUF_SIZE];
let mut compressed = false;
let mut offset: usize = 0;
loop {
@@ -380,6 +460,8 @@ impl TransferJob {
Err(err) => {
self.file_num += 1;
self.file = None;
self.file_confirmed = false;
self.file_is_waiting = false;
return Err(err.into());
}
Ok(n) => {
@@ -394,6 +476,8 @@ impl TransferJob {
if offset == 0 {
self.file_num += 1;
self.file = None;
self.file_confirmed = false;
self.file_is_waiting = false;
} else {
self.finished_size += offset as u64;
if !is_compressed_file(name) {
@@ -413,6 +497,136 @@ impl TransferJob {
..Default::default()
}))
}
async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> {
let mut msg = Message::new();
let mut resp = FileResponse::new();
let meta = self.file.as_ref().unwrap().metadata().await?;
let last_modified = meta
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs();
resp.set_digest(FileTransferDigest {
id: self.id,
file_num: self.file_num,
last_modified,
file_size: meta.len(),
..Default::default()
});
msg.set_file_response(resp);
stream.send(&msg).await?;
log::info!(
"id: {}, file_num:{}, digest message is sent. waiting for confirm. msg: {:?}",
self.id,
self.file_num,
msg
);
Ok(())
}
pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option<bool>) {
self.default_overwrite_strategy = overwrite_strategy;
}
pub fn default_overwrite_strategy(&self) -> Option<bool> {
self.default_overwrite_strategy
}
pub fn set_file_confirmed(&mut self, file_confirmed: bool) {
log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed);
self.file_confirmed = file_confirmed;
self.file_skipped = false;
}
pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) {
self.file_is_waiting = file_is_waiting;
}
#[inline]
pub fn file_is_waiting(&self) -> bool {
self.file_is_waiting
}
#[inline]
pub fn file_confirmed(&self) -> bool {
self.file_confirmed
}
/// Indicating whether the last file is skipped
#[inline]
pub fn file_skipped(&self) -> bool {
self.file_skipped
}
/// Indicating whether the whole task is skipped
#[inline]
pub fn job_skipped(&self) -> bool {
self.file_skipped() && self.files.len() == 1
}
/// Check whether the job is completed after `read` returns `None`
/// This is a helper function which gives additional lifecycle when the job reads `None`.
/// If returns `true`, it means we can delete the job automatically. `False` otherwise.
///
/// [`Note`]
/// Conditions:
/// 1. Files are not waiting for confirmation by peers.
#[inline]
pub fn job_completed(&self) -> bool {
// has no error, Condition 2
!self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting)
}
/// Get job error message, useful for getting status when job had finished
pub fn job_error(&self) -> Option<String> {
if self.job_skipped() {
return Some("skipped".to_string());
}
None
}
pub fn set_file_skipped(&mut self) -> bool {
log::debug!("skip file {} in job {}", self.file_num, self.id);
self.file.take();
self.set_file_confirmed(false);
self.set_file_is_waiting(false);
self.file_num += 1;
self.file_skipped = true;
true
}
pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool {
if self.file_num() != r.file_num {
log::info!("file num truncated, ignoring");
} else {
match r.union {
Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
if s {
self.set_file_skipped();
} else {
self.set_file_confirmed(true);
}
}
Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => {
self.set_file_confirmed(true);
}
_ => {}
}
}
true
}
#[inline]
pub fn gen_meta(&self) -> TransferJobMeta {
TransferJobMeta {
id: self.id,
remote: self.remote.to_string(),
to: self.path.to_string_lossy().to_string(),
file_num: self.file_num,
show_hidden: self.show_hidden,
is_remote: self.is_remote,
}
}
}
#[inline]
@@ -435,7 +649,7 @@ pub fn new_dir(id: i32, path: String, files: Vec<FileEntry>) -> Message {
resp.set_dir(FileDirectory {
id,
path,
entries: files.into(),
entries: files,
..Default::default()
});
let mut msg_out = Message::new();
@@ -453,12 +667,22 @@ pub fn new_block(block: FileTransferBlock) -> Message {
}
#[inline]
pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
let mut msg_out = Message::new();
let mut action = FileAction::new();
action.set_send_confirm(r);
msg_out.set_file_action(action);
msg_out
}
#[inline]
pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message {
let mut action = FileAction::new();
action.set_receive(FileTransferReceiveRequest {
id,
path,
files: files.into(),
files,
file_num,
..Default::default()
});
let mut msg_out = Message::new();
@@ -467,12 +691,14 @@ pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
}
#[inline]
pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message {
pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message {
log::info!("new send: {},id : {}", path, id);
let mut action = FileAction::new();
action.set_send(FileTransferSendRequest {
id,
path,
include_hidden,
file_num,
..Default::default()
});
let mut msg_out = Message::new();
@@ -499,8 +725,8 @@ pub fn remove_job(id: i32, jobs: &mut Vec<TransferJob>) {
}
#[inline]
pub fn get_job(id: i32, jobs: &mut Vec<TransferJob>) -> Option<&mut TransferJob> {
jobs.iter_mut().filter(|x| x.id() == id).next()
pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
jobs.iter_mut().find(|x| x.id() == id)
}
pub async fn handle_read_jobs(
@@ -509,7 +735,10 @@ pub async fn handle_read_jobs(
) -> ResultType<()> {
let mut finished = Vec::new();
for job in jobs.iter_mut() {
match job.read().await {
if job.is_last_job {
continue;
}
match job.read(stream).await {
Err(err) => {
stream
.send(&new_error(job.id(), err, job.file_num()))
@@ -519,8 +748,19 @@ pub async fn handle_read_jobs(
stream.send(&new_block(block)).await?;
}
Ok(None) => {
finished.push(job.id());
stream.send(&new_done(job.id(), job.file_num())).await?;
if job.job_completed() {
finished.push(job.id());
let err = job.job_error();
if err.is_some() {
stream
.send(&new_error(job.id(), err.unwrap(), job.file_num()))
.await?;
} else {
stream.send(&new_done(job.id(), job.file_num())).await?;
}
} else {
// waiting confirmation.
}
}
}
}
@@ -538,7 +778,7 @@ pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
remove_all_empty_dir(&path.join(&entry.name)).ok();
}
Ok(FileType::DirLink) | Ok(FileType::FileLink) => {
std::fs::remove_file(&path.join(&entry.name)).ok();
std::fs::remove_file(path.join(&entry.name)).ok();
}
_ => {}
}
@@ -562,7 +802,38 @@ pub fn create_dir(dir: &str) -> ResultType<()> {
#[inline]
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
for entry in entries {
entry.name = entry.name.replace("\\", "/");
entry.name = entry.name.replace('\\', "/");
}
}
pub enum DigestCheckResult {
IsSame,
NeedConfirm(FileTransferDigest),
NoSuchFile,
}
#[inline]
pub fn is_write_need_confirmation(
file_path: &str,
digest: &FileTransferDigest,
) -> ResultType<DigestCheckResult> {
let path = Path::new(file_path);
if path.exists() && path.is_file() {
let metadata = std::fs::metadata(path)?;
let modified_time = metadata.modified()?;
let remote_mt = Duration::from_secs(digest.last_modified);
let local_mt = modified_time.duration_since(UNIX_EPOCH)?;
if remote_mt == local_mt && digest.file_size == metadata.len() {
return Ok(DigestCheckResult::IsSame);
}
Ok(DigestCheckResult::NeedConfirm(FileTransferDigest {
id: digest.id,
file_num: digest.file_num,
last_modified: local_mt.as_secs(),
file_size: metadata.len(),
..Default::default()
}))
} else {
Ok(DigestCheckResult::NoSuchFile)
}
}

View File

@@ -0,0 +1,39 @@
use std::{fmt, slice::Iter, str::FromStr};
use crate::protos::message::KeyboardMode;
impl fmt::Display for KeyboardMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
KeyboardMode::Legacy => write!(f, "legacy"),
KeyboardMode::Map => write!(f, "map"),
KeyboardMode::Translate => write!(f, "translate"),
KeyboardMode::Auto => write!(f, "auto"),
}
}
}
impl FromStr for KeyboardMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"legacy" => Ok(KeyboardMode::Legacy),
"map" => Ok(KeyboardMode::Map),
"translate" => Ok(KeyboardMode::Translate),
"auto" => Ok(KeyboardMode::Auto),
_ => Err(()),
}
}
}
impl KeyboardMode {
pub fn iter() -> Iter<'static, KeyboardMode> {
static KEYBOARD_MODES: [KeyboardMode; 4] = [
KeyboardMode::Legacy,
KeyboardMode::Map,
KeyboardMode::Translate,
KeyboardMode::Auto,
];
KEYBOARD_MODES.iter()
}
}

View File

@@ -1,15 +1,16 @@
pub mod compress;
#[path = "./protos/message.rs"]
pub mod message_proto;
#[path = "./protos/rendezvous.rs"]
pub mod rendezvous_proto;
pub mod platform;
pub mod protos;
pub use bytes;
use config::Config;
pub use futures;
pub use protobuf;
pub use protos::message as message_proto;
pub use protos::rendezvous as rendezvous_proto;
use std::{
fs::File,
io::{self, BufRead},
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
path::Path,
time::{self, SystemTime, UNIX_EPOCH},
};
@@ -27,6 +28,7 @@ pub use anyhow::{self, bail};
pub use futures_util;
pub mod config;
pub mod fs;
pub use lazy_static;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use mac_address;
pub use rand;
@@ -35,6 +37,10 @@ pub use sodiumoxide;
pub use tokio_socks;
pub use tokio_socks::IntoTargetAddr;
pub use tokio_socks::TargetAddr;
pub mod password_security;
pub use chrono;
pub use directories_next;
pub mod keyboard;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;
@@ -61,6 +67,21 @@ macro_rules! allow_err {
} else {
}
};
($e:expr, $($arg:tt)*) => {
if let Err(err) = $e {
log::debug!(
"{:?}, {}, {}:{}:{}:{}",
err,
format_args!($($arg)*),
module_path!(),
file!(),
line!(),
column!()
);
} else {
}
};
}
#[inline]
@@ -75,8 +96,24 @@ pub type ResultType<F, E = anyhow::Error> = anyhow::Result<F, E>;
pub struct AddrMangle();
#[inline]
pub fn try_into_v4(addr: SocketAddr) -> SocketAddr {
match addr {
SocketAddr::V6(v6) if !addr.ip().is_loopback() => {
if let Some(v4) = v6.ip().to_ipv4() {
SocketAddr::new(IpAddr::V4(v4), addr.port())
} else {
addr
}
}
_ => addr,
}
}
impl AddrMangle {
pub fn encode(addr: SocketAddr) -> Vec<u8> {
// not work with [:1]:<port>
let addr = try_into_v4(addr);
match addr {
SocketAddr::V4(addr_v4) => {
let tm = (SystemTime::now()
@@ -97,15 +134,31 @@ impl AddrMangle {
}
bytes[..(16 - n_padding)].to_vec()
}
_ => {
panic!("Only support ipv4");
SocketAddr::V6(addr_v6) => {
let mut x = addr_v6.ip().octets().to_vec();
let port: [u8; 2] = addr_v6.port().to_le_bytes();
x.push(port[0]);
x.push(port[1]);
x
}
}
}
pub fn decode(bytes: &[u8]) -> SocketAddr {
use std::convert::TryInto;
if bytes.len() > 16 {
if bytes.len() != 18 {
return Config::get_any_listen_addr(false);
}
let tmp: [u8; 2] = bytes[16..].try_into().unwrap();
let port = u16::from_le_bytes(tmp);
let tmp: [u8; 16] = bytes[..16].try_into().unwrap();
let ip = std::net::Ipv6Addr::from(tmp);
return SocketAddr::new(IpAddr::V6(ip), port);
}
let mut padded = [0u8; 16];
padded[..bytes.len()].copy_from_slice(&bytes);
padded[..bytes.len()].copy_from_slice(bytes);
let number = u128::from_le_bytes(padded);
let tm = (number >> 17) & (u32::max_value() as u128);
let ip = (((number >> 49) - tm) as u32).to_le_bytes();
@@ -119,21 +172,9 @@ impl AddrMangle {
pub fn get_version_from_url(url: &str) -> String {
let n = url.chars().count();
let a = url
.chars()
.rev()
.enumerate()
.filter(|(_, x)| x == &'-')
.next()
.map(|(i, _)| i);
let a = url.chars().rev().position(|x| x == '-');
if let Some(a) = a {
let b = url
.chars()
.rev()
.enumerate()
.filter(|(_, x)| x == &'.')
.next()
.map(|(i, _)| i);
let b = url.chars().rev().position(|x| x == '.');
if let Some(b) = b {
if a > b {
if url
@@ -156,19 +197,24 @@ pub fn get_version_from_url(url: &str) -> String {
}
pub fn gen_version() {
println!("cargo:rerun-if-changed=Cargo.toml");
use std::io::prelude::*;
let mut file = File::create("./src/version.rs").unwrap();
for line in read_lines("Cargo.toml").unwrap() {
if let Ok(line) = line {
let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect();
if ab.len() == 2 && ab[0] == "version" {
use std::io::prelude::*;
file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes())
.ok();
file.sync_all().ok();
break;
}
for line in read_lines("Cargo.toml").unwrap().flatten() {
let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect();
if ab.len() == 2 && ab[0] == "version" {
file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes())
.ok();
break;
}
}
// generate build date
let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
file.write_all(
format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(),
)
.ok();
file.sync_all().ok();
}
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
@@ -187,24 +233,159 @@ pub fn is_valid_custom_id(id: &str) -> bool {
pub fn get_version_number(v: &str) -> i64 {
let mut n = 0;
for x in v.split(".") {
for x in v.split('.') {
n = n * 1000 + x.parse::<i64>().unwrap_or(0);
}
n
}
pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
std::fs::metadata(&path)
std::fs::metadata(path)
.map(|m| m.modified().unwrap_or(UNIX_EPOCH))
.unwrap_or(UNIX_EPOCH)
}
pub fn get_created_time(path: &std::path::Path) -> SystemTime {
std::fs::metadata(path)
.map(|m| m.created().unwrap_or(UNIX_EPOCH))
.unwrap_or(UNIX_EPOCH)
}
pub fn get_exe_time() -> SystemTime {
std::env::current_exe().map_or(UNIX_EPOCH, |path| {
let m = get_modified_time(&path);
let c = get_created_time(&path);
if m > c {
m
} else {
c
}
})
}
pub fn get_uuid() -> Vec<u8> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Ok(id) = machine_uid::get() {
return id.into();
}
Config::get_key_pair().1
}
#[inline]
pub fn get_time() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0) as _
}
#[inline]
pub fn is_ipv4_str(id: &str) -> bool {
regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$")
.unwrap()
.is_match(id)
}
#[inline]
pub fn is_ipv6_str(id: &str) -> bool {
regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$")
.unwrap()
.is_match(id)
}
#[inline]
pub fn is_ip_str(id: &str) -> bool {
is_ipv4_str(id) || is_ipv6_str(id)
}
#[inline]
pub fn is_domain_port_str(id: &str) -> bool {
// modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname.
// according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700,
// there is no digits in TLD, and length is 2~63.
regex::Regex::new(
r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$",
)
.unwrap()
.is_match(id)
}
#[cfg(test)]
mod tests {
mod test {
use super::*;
#[test]
fn test_mangle() {
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116));
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
let addr = "[2001:db8::1]:8080".parse::<SocketAddr>().unwrap();
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
let addr = "[2001:db8:ff::1111]:80".parse::<SocketAddr>().unwrap();
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
}
#[test]
fn test_allow_err() {
allow_err!(Err("test err") as Result<(), &str>);
allow_err!(
Err("test err with msg") as Result<(), &str>,
"prompt {}",
"failed"
);
}
#[test]
fn test_ipv6() {
assert!(is_ipv6_str("1:2:3"));
assert!(is_ipv6_str("[ab:2:3]:12"));
assert!(is_ipv6_str("[ABEF:2a:3]:12"));
assert!(!is_ipv6_str("[ABEG:2a:3]:12"));
assert!(!is_ipv6_str("1[ab:2:3]:12"));
assert!(!is_ipv6_str("1.1.1.1"));
assert!(is_ip_str("1.1.1.1"));
assert!(!is_ipv6_str("1:2:"));
assert!(is_ipv6_str("1:2::0"));
assert!(is_ipv6_str("[1:2::0]:1"));
assert!(!is_ipv6_str("[1:2::0]:"));
assert!(!is_ipv6_str("1:2::0]:1"));
}
#[test]
fn test_hostname_port() {
assert!(!is_domain_port_str("a:12"));
assert!(!is_domain_port_str("a.b.c:12"));
assert!(is_domain_port_str("test.com:12"));
assert!(is_domain_port_str("test-UPPER.com:12"));
assert!(is_domain_port_str("some-other.domain.com:12"));
assert!(!is_domain_port_str("under_score:12"));
assert!(!is_domain_port_str("a@bc:12"));
assert!(!is_domain_port_str("1.1.1.1:12"));
assert!(!is_domain_port_str("1.2.3:12"));
assert!(!is_domain_port_str("1.2.3.45:12"));
assert!(!is_domain_port_str("a.b.c:123456"));
assert!(!is_domain_port_str("---:12"));
assert!(!is_domain_port_str(".:12"));
// todo: should we also check for these edge cases?
// out-of-range port
assert!(is_domain_port_str("test.com:0"));
assert!(is_domain_port_str("test.com:98989"));
}
#[test]
fn test_mangle2() {
let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap();
let addr_v4 = "127.0.0.1:8080".parse().unwrap();
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4);
assert_eq!(
AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())),
addr_v4
);
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4);
let addr_v6 = "[ef::fe]:8080".parse().unwrap();
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
let addr_v6 = "[::1]:8080".parse().unwrap();
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
}
}

View File

@@ -0,0 +1,242 @@
use crate::config::Config;
use sodiumoxide::base64;
use std::sync::{Arc, RwLock};
lazy_static::lazy_static! {
pub static ref TEMPORARY_PASSWORD:Arc<RwLock<String>> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length())));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VerificationMethod {
OnlyUseTemporaryPassword,
OnlyUsePermanentPassword,
UseBothPasswords,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApproveMode {
Both,
Password,
Click,
}
// Should only be called in server
pub fn update_temporary_password() {
*TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length());
}
// Should only be called in server
pub fn temporary_password() -> String {
TEMPORARY_PASSWORD.read().unwrap().clone()
}
fn verification_method() -> VerificationMethod {
let method = Config::get_option("verification-method");
if method == "use-temporary-password" {
VerificationMethod::OnlyUseTemporaryPassword
} else if method == "use-permanent-password" {
VerificationMethod::OnlyUsePermanentPassword
} else {
VerificationMethod::UseBothPasswords // default
}
}
pub fn temporary_password_length() -> usize {
let length = Config::get_option("temporary-password-length");
if length == "8" {
8
} else if length == "10" {
10
} else {
6 // default
}
}
pub fn temporary_enabled() -> bool {
verification_method() != VerificationMethod::OnlyUsePermanentPassword
}
pub fn permanent_enabled() -> bool {
verification_method() != VerificationMethod::OnlyUseTemporaryPassword
}
pub fn has_valid_password() -> bool {
temporary_enabled() && !temporary_password().is_empty()
|| permanent_enabled() && !Config::get_permanent_password().is_empty()
}
pub fn approve_mode() -> ApproveMode {
let mode = Config::get_option("approve-mode");
if mode == "password" {
ApproveMode::Password
} else if mode == "click" {
ApproveMode::Click
} else {
ApproveMode::Both
}
}
pub fn hide_cm() -> bool {
approve_mode() == ApproveMode::Password
&& verification_method() == VerificationMethod::OnlyUsePermanentPassword
&& !Config::get_option("allow-hide-cm").is_empty()
}
const VERSION_LEN: usize = 2;
pub fn encrypt_str_or_original(s: &str, version: &str) -> String {
if decrypt_str_or_original(s, version).1 {
log::error!("Duplicate encryption!");
return s.to_owned();
}
if version == "00" {
if let Ok(s) = encrypt(s.as_bytes()) {
return version.to_owned() + &s;
}
}
s.to_owned()
}
// String: password
// bool: whether decryption is successful
// bool: whether should store to re-encrypt when load
pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
if s.len() > VERSION_LEN {
let version = &s[..VERSION_LEN];
if version == "00" {
if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
return (
String::from_utf8_lossy(&v).to_string(),
true,
version != current_version,
);
}
}
}
(s.to_owned(), false, !s.is_empty())
}
pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec<u8> {
if decrypt_vec_or_original(v, version).1 {
log::error!("Duplicate encryption!");
return v.to_owned();
}
if version == "00" {
if let Ok(s) = encrypt(v) {
let mut version = version.to_owned().into_bytes();
version.append(&mut s.into_bytes());
return version;
}
}
v.to_owned()
}
// Vec<u8>: password
// bool: whether decryption is successful
// bool: whether should store to re-encrypt when load
pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, bool, bool) {
if v.len() > VERSION_LEN {
let version = String::from_utf8_lossy(&v[..VERSION_LEN]);
if version == "00" {
if let Ok(v) = decrypt(&v[VERSION_LEN..]) {
return (v, true, version != current_version);
}
}
}
(v.to_owned(), false, !v.is_empty())
}
fn encrypt(v: &[u8]) -> Result<String, ()> {
if !v.is_empty() {
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
} else {
Err(())
}
}
fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
if !v.is_empty() {
base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
} else {
Err(())
}
}
fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
use sodiumoxide::crypto::secretbox;
use std::convert::TryInto;
let mut keybuf = crate::get_uuid();
keybuf.resize(secretbox::KEYBYTES, 0);
let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
if encrypt {
Ok(secretbox::seal(data, &nonce, &key))
} else {
secretbox::open(data, &nonce, &key)
}
}
mod test {
#[test]
fn test() {
use super::*;
let version = "00";
println!("test str");
let data = "Hello World";
let encrypted = encrypt_str_or_original(data, version);
let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
println!("data: {data}");
println!("encrypted: {encrypted}");
println!("decrypted: {decrypted}");
assert_eq!(data, decrypted);
assert_eq!(version, &encrypted[..2]);
assert!(succ);
assert!(!store);
let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
assert!(store);
assert!(!decrypt_str_or_original(&decrypted, version).1);
assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted);
println!("test vec");
let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6];
let encrypted = encrypt_vec_or_original(&data, version);
let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
println!("data: {data:?}");
println!("encrypted: {encrypted:?}");
println!("decrypted: {decrypted:?}");
assert_eq!(data, decrypted);
assert_eq!(version.as_bytes(), &encrypted[..2]);
assert!(!store);
assert!(succ);
let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
assert!(store);
assert!(!decrypt_vec_or_original(&decrypted, version).1);
assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted);
println!("test original");
let data = version.to_string() + "Hello World";
let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
assert_eq!(data, decrypted);
assert!(store);
assert!(!succ);
let verbytes = version.as_bytes();
let data: Vec<u8> = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6];
let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
assert_eq!(data, decrypted);
assert!(store);
assert!(!succ);
let (_, succ, store) = decrypt_str_or_original("", version);
assert!(!store);
assert!(!succ);
let (_, succ, store) = decrypt_vec_or_original(&[], version);
assert!(!store);
assert!(!succ);
}
}

View File

@@ -0,0 +1,157 @@
use crate::ResultType;
lazy_static::lazy_static! {
pub static ref DISTRO: Distro = Distro::new();
}
pub struct Distro {
pub name: String,
pub version_id: String,
}
impl Distro {
fn new() -> Self {
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned())
.unwrap_or_default()
.trim()
.trim_matches('"')
.to_string();
let version_id =
run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned())
.unwrap_or_default()
.trim()
.trim_matches('"')
.to_string();
Self { name, version_id }
}
}
pub fn get_display_server() -> String {
let mut session = get_values_of_seat0([0].to_vec())[0].clone();
if session.is_empty() {
// loginctl has not given the expected output. try something else.
if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
// could also execute "cat /proc/self/sessionid"
session = sid;
}
if session.is_empty() {
session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default();
}
}
get_display_server_of_session(&session)
}
fn get_display_server_of_session(session: &str) -> String {
let mut display_server = if let Ok(output) =
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
// Check session type of the session
{
let display_server = String::from_utf8_lossy(&output.stdout)
.replace("Type=", "")
.trim_end()
.into();
if display_server == "tty" {
// If the type is tty...
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
// Get the tty number
{
let tty: String = String::from_utf8_lossy(&output.stdout)
.replace("TTY=", "")
.trim_end()
.into();
if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\""))
// And check if Xorg is running on that tty
{
if xorg_results.trim_end() != "" {
// If it is, manually return "x11", otherwise return tty
return "x11".to_owned();
}
}
}
}
display_server
} else {
"".to_owned()
};
if display_server.is_empty() || display_server == "tty" {
// loginctl has not given the expected output. try something else.
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
display_server = sestype;
}
}
// If the session is not a tty, then just return the type as usual
display_server
}
pub fn get_values_of_seat0(indices: Vec<usize>) -> Vec<String> {
if let Ok(output) = run_loginctl(None) {
for line in String::from_utf8_lossy(&output.stdout).lines() {
if line.contains("seat0") {
if let Some(sid) = line.split_whitespace().next() {
if is_active(sid) {
return indices
.into_iter()
.map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
.collect::<Vec<String>>();
}
}
}
}
}
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
if let Ok(output) = run_loginctl(None) {
for line in String::from_utf8_lossy(&output.stdout).lines() {
if let Some(sid) = line.split_whitespace().next() {
let d = get_display_server_of_session(sid);
if is_active(sid) && d != "tty" {
return indices
.into_iter()
.map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
.collect::<Vec<String>>();
}
}
}
}
return indices
.iter()
.map(|_x| "".to_owned())
.collect::<Vec<String>>();
}
fn is_active(sid: &str) -> bool {
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
String::from_utf8_lossy(&output.stdout).contains("active")
} else {
false
}
}
pub fn run_cmds(cmds: String) -> ResultType<String> {
let output = std::process::Command::new("sh")
.args(vec!["-c", &cmds])
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(not(feature = "flatpak"))]
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
let mut cmd = std::process::Command::new("loginctl");
if let Some(a) = args {
return cmd.args(a).output();
}
cmd.output()
}
#[cfg(feature = "flatpak")]
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
let mut l_args = String::from("loginctl");
if let Some(a) = args {
l_args = format!("{} {}", l_args, a.join(" "));
}
std::process::Command::new("flatpak-spawn")
.args(vec![String::from("--host"), l_args])
.output()
}

View File

@@ -0,0 +1,2 @@
#[cfg(target_os = "linux")]
pub mod linux;

View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));

View File

@@ -9,31 +9,52 @@ use std::net::SocketAddr;
use tokio::net::ToSocketAddrs;
use tokio_socks::{IntoTargetAddr, TargetAddr};
fn to_socket_addr(host: &str) -> ResultType<SocketAddr> {
use std::net::ToSocketAddrs;
host.to_socket_addrs()?
.filter(|x| x.is_ipv4())
.next()
.context("Failed to solve")
#[inline]
pub fn check_port<T: std::string::ToString>(host: T, port: i32) -> String {
let host = host.to_string();
if crate::is_ipv6_str(&host) {
if host.starts_with('[') {
return host;
}
return format!("[{host}]:{port}");
}
if !host.contains(':') {
return format!("{host}:{port}");
}
host
}
pub fn get_target_addr(host: &str) -> ResultType<TargetAddr<'static>> {
let addr = match Config::get_network_type() {
NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?,
NetworkType::ProxySocks => host.into_target_addr()?,
#[inline]
pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
let host = host.to_string();
if crate::is_ipv6_str(&host) {
if host.starts_with('[') {
let tmp: Vec<&str> = host.split("]:").collect();
if tmp.len() == 2 {
let port: i32 = tmp[1].parse().unwrap_or(0);
if port > 0 {
return format!("{}]:{}", tmp[0], port + offset);
}
}
}
} else if host.contains(':') {
let tmp: Vec<&str> = host.split(':').collect();
if tmp.len() == 2 {
let port: i32 = tmp[1].parse().unwrap_or(0);
if port > 0 {
return format!("{}:{}", tmp[0], port + offset);
}
}
}
.to_owned();
Ok(addr)
host
}
pub fn test_if_valid_server(host: &str) -> String {
let mut host = host.to_owned();
if !host.contains(":") {
host = format!("{}:{}", host, 0);
}
let host = check_port(host, 0);
use std::net::ToSocketAddrs;
match Config::get_network_type() {
NetworkType::Direct => match to_socket_addr(&host) {
NetworkType::Direct => match host.to_socket_addrs() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
},
@@ -44,33 +65,125 @@ pub fn test_if_valid_server(host: &str) -> String {
}
}
pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>(
pub trait IsResolvedSocketAddr {
fn resolve(&self) -> Option<&SocketAddr>;
}
impl IsResolvedSocketAddr for SocketAddr {
fn resolve(&self) -> Option<&SocketAddr> {
Some(self)
}
}
impl IsResolvedSocketAddr for String {
fn resolve(&self) -> Option<&SocketAddr> {
None
}
}
impl IsResolvedSocketAddr for &str {
fn resolve(&self) -> Option<&SocketAddr> {
None
}
}
#[inline]
pub async fn connect_tcp<
't,
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
>(
target: T,
local: SocketAddr,
ms_timeout: u64,
) -> ResultType<FramedStream> {
let target_addr = target.into_target_addr()?;
connect_tcp_local(target, None, ms_timeout).await
}
pub async fn connect_tcp_local<
't,
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
>(
target: T,
local: Option<SocketAddr>,
ms_timeout: u64,
) -> ResultType<FramedStream> {
if let Some(conf) = Config::get_socks() {
FramedStream::connect(
return FramedStream::connect(
conf.proxy.as_str(),
target_addr,
target,
local,
conf.username.as_str(),
conf.password.as_str(),
ms_timeout,
)
.await
} else {
let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)?
.filter(|x| x.is_ipv4())
.next()
.context("Invalid target addr, no valid ipv4 address can be resolved.")?;
Ok(FramedStream::new(addr, local, ms_timeout).await?)
.await;
}
if let Some(target) = target.resolve() {
if let Some(local) = local {
if local.is_ipv6() && target.is_ipv4() {
let target = query_nip_io(target).await?;
return FramedStream::new(target, Some(local), ms_timeout).await;
}
}
}
FramedStream::new(target, local, ms_timeout).await
}
#[inline]
pub fn is_ipv4(target: &TargetAddr<'_>) -> bool {
match target {
TargetAddr::Ip(addr) => addr.is_ipv4(),
_ => true,
}
}
pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
#[inline]
pub async fn query_nip_io(addr: &SocketAddr) -> ResultType<SocketAddr> {
tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port()))
.await?
.find(|x| x.is_ipv6())
.context("Failed to get ipv6 from nip.io")
}
#[inline]
pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String {
if !ipv4 && crate::is_ipv4_str(&addr) {
if let Some(ip) = addr.split(':').next() {
return addr.replace(ip, &format!("{ip}.nip.io"));
}
}
addr
}
async fn test_target(target: &str) -> ResultType<SocketAddr> {
if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await {
if let Ok(addr) = s.peer_addr() {
return Ok(addr);
}
}
tokio::net::lookup_host(target)
.await?
.next()
.context(format!("Failed to look up host for {target}"))
}
#[inline]
pub async fn new_udp_for(
target: &str,
ms_timeout: u64,
) -> ResultType<(FramedSocket, TargetAddr<'static>)> {
let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() {
let addr = test_target(target).await?;
(addr.is_ipv4(), addr.into_target_addr()?)
} else {
(true, target.into_target_addr()?)
};
Ok((
new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?,
target.to_owned(),
))
}
async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
match Config::get_socks() {
None => Ok(FramedSocket::new(local).await?),
Some(conf) => {
@@ -87,9 +200,82 @@ pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<
}
}
pub async fn rebind_udp<T: ToSocketAddrs>(local: T) -> ResultType<Option<FramedSocket>> {
match Config::get_network_type() {
NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)),
_ => Ok(None),
pub async fn rebind_udp_for(
target: &str,
) -> ResultType<Option<(FramedSocket, TargetAddr<'static>)>> {
if Config::get_network_type() != NetworkType::Direct {
return Ok(None);
}
let addr = test_target(target).await?;
let v4 = addr.is_ipv4();
Ok(Some((
FramedSocket::new(Config::get_any_listen_addr(v4)).await?,
addr.into_target_addr()?.to_owned(),
)))
}
#[cfg(test)]
mod tests {
use std::net::ToSocketAddrs;
use super::*;
#[test]
fn test_nat64() {
test_nat64_async();
}
#[tokio::main(flavor = "current_thread")]
async fn test_nat64_async() {
assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1");
assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io");
assert_eq!(
ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false),
"1.1.1.1.nip.io:8080"
);
assert_eq!(
ipv4_to_ipv6("rustdesk.com".to_owned(), false),
"rustdesk.com"
);
if ("rustdesk.com:80")
.to_socket_addrs()
.unwrap()
.next()
.unwrap()
.is_ipv6()
{
assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap())
.await
.unwrap()
.is_ipv6());
return;
}
assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err());
}
#[test]
fn test_test_if_valid_server() {
assert!(!test_if_valid_server("a").is_empty());
// on Linux, "1" is resolved to "0.0.0.1"
assert!(test_if_valid_server("1.1.1.1").is_empty());
assert!(test_if_valid_server("1.1.1.1:1").is_empty());
}
#[test]
fn test_check_port() {
assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12");
assert_eq!(check_port("1:2", 32), "[1:2]:32");
assert_eq!(check_port("z1:2", 32), "z1:2");
assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32");
assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32");
assert_eq!(check_port("test.com:32", 0), "test.com:32");
assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13");
assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13");
assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4");
assert_eq!(increase_port("test.com", 1), "test.com");
assert_eq!(increase_port("test.com:13", 4), "test.com:17");
assert_eq!(increase_port("1:13", 4), "1:13");
assert_eq!(increase_port("22:1:13", 4), "22:1:13");
assert_eq!(increase_port("z1:2", 1), "z1:3");
}
}

View File

@@ -1,11 +1,12 @@
use crate::{bail, bytes_codec::BytesCodec, ResultType};
use anyhow::Context as AnyhowCtx;
use bytes::{BufMut, Bytes, BytesMut};
use futures::{SinkExt, StreamExt};
use protobuf::Message;
use sodiumoxide::crypto::secretbox::{self, Key, Nonce};
use std::{
io::{self, Error, ErrorKind},
net::SocketAddr,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
ops::{Deref, DerefMut},
pin::Pin,
task::{Context, Poll},
@@ -73,73 +74,79 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std:
}
impl FramedStream {
pub async fn new<T1: ToSocketAddrs, T2: ToSocketAddrs>(
remote_addr: T1,
local_addr: T2,
pub async fn new<T: ToSocketAddrs + std::fmt::Display>(
remote_addr: T,
local_addr: Option<SocketAddr>,
ms_timeout: u64,
) -> ResultType<Self> {
for local_addr in lookup_host(&local_addr).await? {
for remote_addr in lookup_host(&remote_addr).await? {
let stream = super::timeout(
ms_timeout,
new_socket(local_addr, true)?.connect(remote_addr),
)
.await??;
stream.set_nodelay(true).ok();
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
for remote_addr in lookup_host(&remote_addr).await? {
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4())
};
if let Ok(socket) = new_socket(local, true) {
if let Ok(Ok(stream)) =
super::timeout(ms_timeout, socket.connect(remote_addr)).await
{
stream.set_nodelay(true).ok();
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
}
}
}
bail!("could not resolve to any address");
bail!(format!("Failed to connect to {remote_addr}"));
}
pub async fn connect<'a, 't, P, T1, T2>(
pub async fn connect<'a, 't, P, T>(
proxy: P,
target: T1,
local: T2,
target: T,
local_addr: Option<SocketAddr>,
username: &'a str,
password: &'a str,
ms_timeout: u64,
) -> ResultType<Self>
where
P: ToProxyAddrs,
T1: IntoTargetAddr<'t>,
T2: ToSocketAddrs,
T: IntoTargetAddr<'t>,
{
if let Some(local) = lookup_host(&local).await?.next() {
if let Some(proxy) = proxy.to_proxy_addrs().next().await {
let stream =
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??;
stream.set_nodelay(true).ok();
let stream = if username.trim().is_empty() {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
} else {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream, target, username, password,
),
)
.await??
};
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
};
};
let stream =
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
stream.set_nodelay(true).ok();
let stream = if username.trim().is_empty() {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
} else {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream, target, username, password,
),
)
.await??
};
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
}
bail!("could not resolve to any address");
}
@@ -203,7 +210,7 @@ impl FramedStream {
if let Some(Ok(bytes)) = res.as_mut() {
key.2 += 1;
let nonce = Self::get_nonce(key.2);
match secretbox::open(&bytes, &nonce, &key.0) {
match secretbox::open(bytes, &nonce, &key.0) {
Ok(res) => {
bytes.clear();
bytes.put_slice(&res);
@@ -239,19 +246,52 @@ impl FramedStream {
const DEFAULT_BACKLOG: u32 = 128;
#[allow(clippy::never_loop)]
pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<TcpListener> {
if !reuse {
Ok(TcpListener::bind(addr).await?)
} else {
for addr in lookup_host(&addr).await? {
let socket = new_socket(addr, true)?;
return Ok(socket.listen(DEFAULT_BACKLOG)?);
}
bail!("could not resolve to any address");
let addr = lookup_host(&addr)
.await?
.next()
.context("could not resolve to any address")?;
new_socket(addr, true)?
.listen(DEFAULT_BACKLOG)
.map_err(anyhow::Error::msg)
}
}
pub async fn listen_any(port: u16) -> ResultType<TcpListener> {
if let Ok(mut socket) = TcpSocket::new_v6() {
#[cfg(unix)]
{
use std::os::unix::io::{FromRawFd, IntoRawFd};
let raw_fd = socket.into_raw_fd();
let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) };
sock2.set_only_v6(false).ok();
socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) };
}
#[cfg(windows)]
{
use std::os::windows::prelude::{FromRawSocket, IntoRawSocket};
let raw_socket = socket.into_raw_socket();
let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) };
sock2.set_only_v6(false).ok();
socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) };
}
if socket
.bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port))
.is_ok()
{
if let Ok(l) = socket.listen(DEFAULT_BACKLOG) {
return Ok(l);
}
}
}
let s = TcpSocket::new_v4()?;
s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
Ok(s.listen(DEFAULT_BACKLOG)?)
}
impl Unpin for DynTcpStream {}
impl AsyncRead for DynTcpStream {

View File

@@ -1,11 +1,11 @@
use crate::{bail, ResultType};
use anyhow::anyhow;
use crate::ResultType;
use anyhow::{anyhow, Context};
use bytes::{Bytes, BytesMut};
use futures::{SinkExt, StreamExt};
use protobuf::Message;
use socket2::{Domain, Socket, Type};
use std::net::SocketAddr;
use tokio::net::{ToSocketAddrs, UdpSocket};
use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket};
use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs};
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
@@ -37,39 +37,31 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket,
addr,
socket.recv_buffer_size()
);
if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 {
socket.set_only_v6(false).ok();
}
socket.bind(&addr.into())?;
Ok(socket)
}
impl FramedSocket {
pub async fn new<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
let socket = UdpSocket::bind(addr).await?;
Ok(Self::Direct(UdpFramed::new(socket, BytesCodec::new())))
Self::new_reuse(addr, false, 0).await
}
#[allow(clippy::never_loop)]
pub async fn new_reuse<T: std::net::ToSocketAddrs>(addr: T) -> ResultType<Self> {
for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) {
let socket = new_socket(addr, true, 0)?.into_udp_socket();
return Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(socket)?,
BytesCodec::new(),
)));
}
bail!("could not resolve to any address");
}
pub async fn new_with_buf_size<T: std::net::ToSocketAddrs>(
pub async fn new_reuse<T: ToSocketAddrs>(
addr: T,
reuse: bool,
buf_size: usize,
) -> ResultType<Self> {
for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) {
return Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?,
BytesCodec::new(),
)));
}
bail!("could not resolve to any address");
let addr = lookup_host(&addr)
.await?
.next()
.context("could not resolve to any address")?;
Ok(Self::Direct(UdpFramed::new(
UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?,
BytesCodec::new(),
)))
}
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
@@ -104,11 +96,12 @@ impl FramedSocket {
) -> ResultType<()> {
let addr = addr.into_target_addr()?.to_owned();
let send_data = Bytes::from(msg.write_to_bytes()?);
let _ = match self {
Self::Direct(f) => match addr {
TargetAddr::Ip(addr) => f.send((send_data, addr)).await?,
_ => {}
},
match self {
Self::Direct(f) => {
if let TargetAddr::Ip(addr) = addr {
f.send((send_data, addr)).await?
}
}
Self::ProxySocks(f) => f.send((send_data, addr)).await?,
};
Ok(())
@@ -123,11 +116,12 @@ impl FramedSocket {
) -> ResultType<()> {
let addr = addr.into_target_addr()?.to_owned();
let _ = match self {
Self::Direct(f) => match addr {
TargetAddr::Ip(addr) => f.send((Bytes::from(msg), addr)).await?,
_ => {}
},
match self {
Self::Direct(f) => {
if let TargetAddr::Ip(addr) = addr {
f.send((Bytes::from(msg), addr)).await?
}
}
Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?,
};
Ok(())
@@ -164,4 +158,13 @@ impl FramedSocket {
None
}
}
pub fn local_addr(&self) -> Option<SocketAddr> {
if let FramedSocket::Direct(x) = self {
if let Ok(v) = x.get_ref().local_addr() {
return Some(v);
}
}
None
}
}

66
rcd/rustdesk-hbbr Normal file
View File

@@ -0,0 +1,66 @@
#!/bin/sh
# PROVIDE: rustdesk_hbbr
# REQUIRE: LOGIN
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# rustdesk_hbbr_enable (bool): Set to NO by default.
# Set it to YES to enable nfs-exporter.
# rustdesk_hbbr_args (string): Set extra arguments to pass to nfs-exporter
# Default is "".
# rustdesk_hbbr_user (string): Set user that rustdesk_hbbr will run under
# Default is "root".
# rustdesk_hbbr_group (string): Set group that rustdesk_hbbr will run under
# Default is "wheel".
. /etc/rc.subr
name=rustdesk_hbbr
desc="Rustdesk Relay Server"
rcvar=rustdesk_hbbr_enable
load_rc_config $name
: ${rustdesk_hbbr_enable:=NO}
: ${rustdesk_hbbr_args:=""}
: ${rustdesk_hbbr_user:=rustdesk}
: ${rustdesk_hbbr_group:=rustdesk}
pidfile=/var/run/rustdesk_hbbr.pid
command=/usr/sbin/daemon
procname=/usr/local/sbin/hbbr
rustdesk_hbbr_chdir="/var/lib/rustdesk-server/"
rustdesk_hbbr_args="-k _"
command_args="-p ${pidfile} -o /var/log/rustdesk-hbbr.log ${procname} ${rustdesk_hbbr_args}"
## If you want the daemon do its log over syslog comment out the above line and remove the comment from the below replacement
#command_args="-p ${pidfile} -T ${name} ${procname} ${rustdesk_hbbr_args}"
start_precmd=rustdesk_hbbr_startprecmd
rustdesk_hbbr_startprecmd()
{
if [ -e ${pidfile} ]; then
chown ${rustdesk_hbbr_user}:${rustdesk_hbbr_group} ${pidfile};
else
install -o ${rustdesk_hbbr_user} -g ${rustdesk_hbbr_group} /dev/null ${pidfile};
fi
if [ -e ${rustdesk_hbbr_chdir} ]; then
chown -R ${rustdesk_hbbr_user}:${rustdesk_hbbr_group} ${rustdesk_hbbr_chdir};
chmod -R 770 ${rustdesk_hbbr_chdir};
else
mkdir -m 770 ${rustdesk_hbbr_chdir};
chown ${rustdesk_hbbr_user}:${rustdesk_hbbr_group} ${rustdesk_hbbr_chdir};
fi
if [ -e /var/log/rustdesk-hbbr.log ]; then
chown -R ${rustdesk_hbbr_user}:${rustdesk_hbbr_group} /var/log/rustdesk-hbbr.log;
chmod 660 /var/log/rustdesk-hbbr.log;
else
install -o ${rustdesk_hbbr_user} -g ${rustdesk_hbbr_group} /dev/null /var/log/rustdesk-hbbr.log;
chmod 660 /var/log/rustdesk-hbbr.log;
fi
}
run_rc_command "$1"

66
rcd/rustdesk-hbbs Normal file
View File

@@ -0,0 +1,66 @@
#!/bin/sh
# PROVIDE: rustdesk_hbbs
# REQUIRE: LOGIN
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# rustdesk_hbbs_enable (bool): Set to NO by default.
# Set it to YES to enable nfs-exporter.
# rustdesk_hbbs_args (string): Set extra arguments to pass to nfs-exporter
# Default is "".
# rustdesk_hbbs_user (string): Set user that rustdesk_hbbs will run under
# Default is "root".
# rustdesk_hbbs_group (string): Set group that rustdesk_hbbs will run under
# Default is "wheel".
. /etc/rc.subr
name=rustdesk_hbbs
desc="Rustdesk ID/Rendezvous Server"
rcvar=rustdesk_hbbs_enable
load_rc_config $name
: ${rustdesk_hbbs_enable:=NO}
: ${rustdesk_hbbs_args:=""}
: ${rustdesk_hbbs_user:=rustdesk}
: ${rustdesk_hbbs_group:=rustdesk}
pidfile=/var/run/rustdesk_hbbs.pid
command=/usr/sbin/daemon
procname=/usr/local/sbin/hbbs
rustdesk_hbbs_chdir="/var/lib/rustdesk-server/"
rustdesk_hbbs_args="-r your.ip.add.ress -k _"
command_args="-p ${pidfile} -o /var/log/rustdesk-hbbs.log ${procname} ${rustdesk_hbbs_args}"
## If you want the daemon do its log over syslog comment out the above line and remove the comment from the below replacement
#command_args="-p ${pidfile} -T ${name} ${procname} ${rustdesk_hbbs_args}"
start_precmd=rustdesk_hbbs_startprecmd
rustdesk_hbbs_startprecmd()
{
if [ -e ${pidfile} ]; then
chown ${rustdesk_hbbs_user}:${rustdesk_hbbs_group} ${pidfile};
else
install -o ${rustdesk_hbbs_user} -g ${rustdesk_hbbs_group} /dev/null ${pidfile};
fi
if [ -e ${rustdesk_hbbs_chdir} ]; then
chown -R ${rustdesk_hbbs_user}:${rustdesk_hbbs_group} ${rustdesk_hbbs_chdir};
chmod -R 770 ${rustdesk_hbbs_chdir};
else
mkdir -m 770 ${rustdesk_hbbs_chdir};
chown ${rustdesk_hbbs_user}:${rustdesk_hbbs_group} ${rustdesk_hbbs_chdir};
fi
if [ -e /var/log/rustdesk-hbbs.log ]; then
chown -R ${rustdesk_hbbs_user}:${rustdesk_hbbs_group} /var/log/rustdesk-hbbs.log;
chmod 660 /var/log/rustdesk-hbbs.log;
else
install -o ${rustdesk_hbbs_user} -g ${rustdesk_hbbs_group} /dev/null /var/log/rustdesk-hbbs.log;
chmod 660 /var/log/rustdesk-hbbs.log;
fi
}
run_rc_command "$1"

View File

@@ -1,5 +1,8 @@
use clap::App;
use hbb_common::{anyhow::Context, log, ResultType};
use hbb_common::{
anyhow::{Context, Result},
log, ResultType,
};
use ini::Ini;
use sodiumoxide::crypto::sign;
use std::{
@@ -9,15 +12,17 @@ use std::{
time::{Instant, SystemTime},
};
#[allow(dead_code)]
pub(crate) fn get_expired_time() -> Instant {
let now = Instant::now();
now.checked_sub(std::time::Duration::from_secs(3600))
.unwrap_or(now)
}
#[allow(dead_code)]
pub(crate) fn test_if_valid_server(host: &str, name: &str) -> ResultType<SocketAddr> {
use std::net::ToSocketAddrs;
let res = if host.contains(":") {
let res = if host.contains(':') {
host.to_socket_addrs()?.next().context("")
} else {
format!("{}:{}", host, 0)
@@ -31,9 +36,10 @@ pub(crate) fn test_if_valid_server(host: &str, name: &str) -> ResultType<SocketA
res
}
#[allow(dead_code)]
pub(crate) fn get_servers(s: &str, tag: &str) -> Vec<String> {
let servers: Vec<String> = s
.split(",")
.split(',')
.filter(|x| !x.is_empty() && test_if_valid_server(x, tag).is_ok())
.map(|x| x.to_owned())
.collect();
@@ -41,17 +47,19 @@ pub(crate) fn get_servers(s: &str, tag: &str) -> Vec<String> {
servers
}
#[allow(dead_code)]
#[inline]
fn arg_name(name: &str) -> String {
name.to_uppercase().replace("_", "-")
name.to_uppercase().replace('_', "-")
}
#[allow(dead_code)]
pub fn init_args(args: &str, name: &str, about: &str) {
let matches = App::new(name)
.version(crate::version::VERSION)
.author("Purslane Ltd. <info@rustdesk.com>")
.about(about)
.args_from_usage(&args)
.args_from_usage(args)
.get_matches();
if let Ok(v) = Ini::load_from_file(".env") {
if let Some(section) = v.section(None::<String>) {
@@ -76,16 +84,19 @@ pub fn init_args(args: &str, name: &str, about: &str) {
}
}
#[allow(dead_code)]
#[inline]
pub fn get_arg(name: &str) -> String {
get_arg_or(name, "".to_owned())
}
#[allow(dead_code)]
#[inline]
pub fn get_arg_or(name: &str, default: String) -> String {
std::env::var(arg_name(name)).unwrap_or(default)
}
#[allow(dead_code)]
#[inline]
pub fn now() -> u64 {
SystemTime::now()
@@ -118,12 +129,12 @@ pub fn gen_sk(wait: u64) -> (String, Option<sign::SecretKey>) {
};
let (mut pk, mut sk) = gen_func();
for _ in 0..300 {
if !pk.contains("/") && !pk.contains(":") {
if !pk.contains('/') && !pk.contains(':') {
break;
}
(pk, sk) = gen_func();
}
let pub_file = format!("{}.pub", sk_file);
let pub_file = format!("{sk_file}.pub");
if let Ok(mut f) = std::fs::File::create(&pub_file) {
f.write_all(pk.as_bytes()).ok();
if let Ok(mut f) = std::fs::File::create(sk_file) {
@@ -138,3 +149,43 @@ pub fn gen_sk(wait: u64) -> (String, Option<sign::SecretKey>) {
}
("".to_owned(), None)
}
#[cfg(unix)]
pub async fn listen_signal() -> Result<()> {
use hbb_common::tokio;
use hbb_common::tokio::signal::unix::{signal, SignalKind};
tokio::spawn(async {
let mut s = signal(SignalKind::hangup())?;
let hangup = s.recv();
let mut s = signal(SignalKind::terminate())?;
let terminate = s.recv();
let mut s = signal(SignalKind::interrupt())?;
let interrupt = s.recv();
let mut s = signal(SignalKind::quit())?;
let quit = s.recv();
tokio::select! {
_ = hangup => {
log::info!("signal hangup");
}
_ = terminate => {
log::info!("signal terminate");
}
_ = interrupt => {
log::info!("signal interrupt");
}
_ = quit => {
log::info!("signal quit");
}
}
Ok(())
})
.await?
}
#[cfg(not(unix))]
pub async fn listen_signal() -> Result<()> {
let () = std::future::pending().await;
unreachable!();
}

View File

@@ -1,6 +1,5 @@
use async_trait::async_trait;
use hbb_common::{log, ResultType};
use serde_json::value::Value;
use sqlx::{
sqlite::SqliteConnectOptions, ConnectOptions, Connection, Error as SqlxError, SqliteConnection,
};
@@ -8,7 +7,6 @@ use std::{ops::DerefMut, str::FromStr};
//use sqlx::postgres::PgPoolOptions;
//use sqlx::mysql::MySqlPoolOptions;
pub(crate) type MapValue = serde_json::map::Map<String, Value>;
type Pool = deadpool::managed::Pool<DbPool>;
pub struct DbPool {
@@ -54,7 +52,7 @@ impl Database {
std::fs::File::create(url).ok();
}
let n: usize = std::env::var("MAX_DATABASE_CONNECTIONS")
.unwrap_or("1".to_owned())
.unwrap_or_else(|_| "1".to_owned())
.parse()
.unwrap_or(1);
log::debug!("MAX_DATABASE_CONNECTIONS={}", n);
@@ -105,24 +103,6 @@ impl Database {
.await?)
}
#[inline]
pub async fn get_conn(&self) -> ResultType<deadpool::managed::Object<DbPool>> {
Ok(self.pool.get().await?)
}
pub async fn update_peer(&self, payload: MapValue, guid: &[u8]) -> ResultType<()> {
let mut conn = self.get_conn().await?;
let mut tx = conn.begin().await?;
if let Some(v) = payload.get("note") {
let v = get_str(v);
sqlx::query!("update peer set note = ? where guid = ?", v, guid)
.execute(&mut tx)
.await?;
}
tx.commit().await?;
Ok(())
}
pub async fn insert_peer(
&self,
id: &str,
@@ -199,17 +179,3 @@ mod tests {
hbb_common::futures::future::join_all(jobs).await;
}
}
pub(crate) fn get_str(v: &Value) -> Option<&str> {
match v {
Value::String(v) => {
let v = v.trim();
if v.is_empty() {
None
} else {
Some(v)
}
}
_ => None,
}
}

View File

@@ -13,10 +13,9 @@ fn main() -> ResultType<()> {
.write_mode(WriteMode::Async)
.start()?;
let args = format!(
"-p, --port=[NUMBER(default={})] 'Sets the listening port'
"-p, --port=[NUMBER(default={RELAY_PORT})] 'Sets the listening port'
-k, --key=[KEY] 'Only allow the client with the same key'
",
RELAY_PORT,
);
let matches = App::new("hbbr")
.version(version::VERSION)
@@ -29,9 +28,18 @@ fn main() -> ResultType<()> {
section.iter().for_each(|(k, v)| std::env::set_var(k, v));
}
}
let mut port = RELAY_PORT;
if let Ok(v) = std::env::var("PORT") {
let v: i32 = v.parse().unwrap_or_default();
if v > 0 {
port = v + 1;
}
}
start(
matches.value_of("port").unwrap_or(&RELAY_PORT.to_string()),
matches.value_of("key").unwrap_or(""),
matches.value_of("port").unwrap_or(&port.to_string()),
matches
.value_of("key")
.unwrap_or(&std::env::var("KEY").unwrap_or_default()),
)?;
Ok(())
}

View File

@@ -15,16 +15,14 @@ fn main() -> ResultType<()> {
.start()?;
let args = format!(
"-c --config=[FILE] +takes_value 'Sets a custom config file'
-p, --port=[NUMBER(default={})] 'Sets the listening port'
-p, --port=[NUMBER(default={RENDEZVOUS_PORT})] 'Sets the listening port'
-s, --serial=[NUMBER(default=0)] 'Sets configure update serial number'
-R, --rendezvous-servers=[HOSTS] 'Sets rendezvous servers, seperated by colon'
-u, --software-url=[URL] 'Sets download url of RustDesk software of newest version'
-r, --relay-servers=[HOST] 'Sets the default relay servers, seperated by colon'
-M, --rmem=[NUMBER(default={})] 'Sets UDP recv buffer size, set system rmem_max first, e.g., sudo sysctl -w net.core.rmem_max=52428800. vi /etc/sysctl.conf, net.core.rmem_max=52428800, sudo sysctl p'
-M, --rmem=[NUMBER(default={RMEM})] 'Sets UDP recv buffer size, set system rmem_max first, e.g., sudo sysctl -w net.core.rmem_max=52428800. vi /etc/sysctl.conf, net.core.rmem_max=52428800, sudo sysctl p'
, --mask=[MASK] 'Determine if the connection comes from LAN, e.g. 192.168.0.0/16'
-k, --key=[KEY] 'Only allow the client with the same key'",
RENDEZVOUS_PORT,
RMEM,
);
init_args(&args, "hbbs", "RustDesk ID/Rendezvous Server");
let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::<i32>()?;

View File

@@ -1,19 +1,22 @@
use crate::common::*;
use crate::database;
use hbb_common::{
bytes::Bytes,
log,
rendezvous_proto::*,
tokio::sync::{Mutex, RwLock},
bytes::Bytes,
ResultType,
};
use serde_derive::{Deserialize, Serialize};
use std::{collections::HashMap, collections::HashSet, net::SocketAddr, sync::Arc, time::Instant};
type IpBlockMap = HashMap<String, ((u32, Instant), (HashSet<String>, Instant))>;
type UserStatusMap = HashMap<Vec<u8>, Arc<(Option<Vec<u8>>, bool)>>;
type IpChangesMap = HashMap<String, (Instant, HashMap<String, i32>)>;
lazy_static::lazy_static! {
pub(crate) static ref IP_BLOCKER: Mutex<HashMap<String, ((u32, Instant), (HashSet<String>, Instant))>> = Default::default();
pub(crate) static ref USER_STATUS: RwLock<HashMap<Vec<u8>, Arc<(Option<Vec<u8>>, bool)>>> = Default::default();
pub(crate) static ref IP_CHANGES: Mutex<HashMap<String, (Instant, HashMap<String, i32>)>> = Default::default();
pub(crate) static ref IP_BLOCKER: Mutex<IpBlockMap> = Default::default();
pub(crate) static ref USER_STATUS: RwLock<UserStatusMap> = Default::default();
pub(crate) static ref IP_CHANGES: Mutex<IpChangesMap> = Default::default();
}
pub static IP_CHANGE_DUR: u64 = 180;
pub static IP_CHANGE_DUR_X2: u64 = IP_CHANGE_DUR * 2;
@@ -32,9 +35,9 @@ pub(crate) struct Peer {
pub(crate) guid: Vec<u8>,
pub(crate) uuid: Bytes,
pub(crate) pk: Bytes,
pub(crate) user: Option<Vec<u8>>,
// pub(crate) user: Option<Vec<u8>>,
pub(crate) info: PeerInfo,
pub(crate) disabled: bool,
// pub(crate) disabled: bool,
pub(crate) reg_pk: (u32, Instant), // how often register_pk
}
@@ -47,8 +50,8 @@ impl Default for Peer {
uuid: Bytes::new(),
pk: Bytes::new(),
info: Default::default(),
user: None,
disabled: false,
// user: None,
// disabled: false,
reg_pk: (0, get_expired_time()),
}
}
@@ -65,7 +68,6 @@ pub(crate) struct PeerMap {
impl PeerMap {
pub(crate) async fn new() -> ResultType<Self> {
let db = std::env::var("DB_URL").unwrap_or({
#[allow(unused_mut)]
let mut db = "db_v2.sqlite3".to_owned();
#[cfg(all(windows, not(debug_assertions)))]
{
@@ -75,7 +77,7 @@ impl PeerMap {
}
#[cfg(not(windows))]
{
db = format!("./{}", db);
db = format!("./{db}");
}
db
});
@@ -132,24 +134,22 @@ impl PeerMap {
#[inline]
pub(crate) async fn get(&self, id: &str) -> Option<LockPeer> {
let p = self.map.read().await.get(id).map(|x| x.clone());
let p = self.map.read().await.get(id).cloned();
if p.is_some() {
return p;
} else {
if let Ok(Some(v)) = self.db.get_peer(id).await {
let peer = Peer {
guid: v.guid,
uuid: v.uuid.into(),
pk: v.pk.into(),
user: v.user,
info: serde_json::from_str::<PeerInfo>(&v.info).unwrap_or_default(),
disabled: v.status == Some(0),
..Default::default()
};
let peer = Arc::new(RwLock::new(peer));
self.map.write().await.insert(id.to_owned(), peer.clone());
return Some(peer);
}
} else if let Ok(Some(v)) = self.db.get_peer(id).await {
let peer = Peer {
guid: v.guid,
uuid: v.uuid.into(),
pk: v.pk.into(),
// user: v.user,
info: serde_json::from_str::<PeerInfo>(&v.info).unwrap_or_default(),
// disabled: v.status == Some(0),
..Default::default()
};
let peer = Arc::new(RwLock::new(peer));
self.map.write().await.insert(id.to_owned(), peer.clone());
return Some(peer);
}
None
}
@@ -170,7 +170,7 @@ impl PeerMap {
#[inline]
pub(crate) async fn get_in_memory(&self, id: &str) -> Option<LockPeer> {
self.map.read().await.get(id).map(|x| x.clone())
self.map.read().await.get(id).cloned()
}
#[inline]

View File

@@ -8,7 +8,7 @@ use hbb_common::{
protobuf::Message as _,
rendezvous_proto::*,
sleep,
tcp::{new_listener, FramedStream},
tcp::{listen_any, FramedStream},
timeout,
tokio::{
self,
@@ -37,12 +37,12 @@ lazy_static::lazy_static! {
}
static mut DOWNGRADE_THRESHOLD: f64 = 0.66;
static mut DOWNGRADE_START_CHECK: usize = 1800_000; // in ms
static mut DOWNGRADE_START_CHECK: usize = 1_800_000; // in ms
static mut LIMIT_SPEED: usize = 4 * 1024 * 1024; // in bit/s
static mut TOTAL_BANDWIDTH: usize = 1024 * 1024 * 1024; // in bit/s
static mut SINGLE_BANDWIDTH: usize = 16 * 1024 * 1024; // in bit/s
const BLACKLIST_FILE: &'static str = "blacklist.txt";
const BLOCKLIST_FILE: &'static str = "blocklist.txt";
const BLACKLIST_FILE: &str = "blacklist.txt";
const BLOCKLIST_FILE: &str = "blocklist.txt";
#[tokio::main(flavor = "multi_thread")]
pub async fn start(port: &str, key: &str) -> ResultType<()> {
@@ -50,8 +50,8 @@ pub async fn start(port: &str, key: &str) -> ResultType<()> {
if let Ok(mut file) = std::fs::File::open(BLACKLIST_FILE) {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_ok() {
for x in contents.split("\n") {
if let Some(ip) = x.trim().split(' ').nth(0) {
for x in contents.split('\n') {
if let Some(ip) = x.trim().split(' ').next() {
BLACKLIST.write().await.insert(ip.to_owned());
}
}
@@ -65,8 +65,8 @@ pub async fn start(port: &str, key: &str) -> ResultType<()> {
if let Ok(mut file) = std::fs::File::open(BLOCKLIST_FILE) {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_ok() {
for x in contents.split("\n") {
if let Some(ip) = x.trim().split(' ').nth(0) {
for x in contents.split('\n') {
if let Some(ip) = x.trim().split(' ').next() {
BLOCKLIST.write().await.insert(ip.to_owned());
}
}
@@ -77,19 +77,21 @@ pub async fn start(port: &str, key: &str) -> ResultType<()> {
BLOCKLIST_FILE,
BLOCKLIST.read().await.len()
);
let addr = format!("0.0.0.0:{}", port);
log::info!("Listening on tcp {}", addr);
let addr2 = format!("0.0.0.0:{}", port.parse::<u16>().unwrap() + 2);
log::info!("Listening on websocket {}", addr2);
loop {
log::info!("Start");
io_loop(
new_listener(&addr, false).await?,
new_listener(&addr2, false).await?,
&key,
)
.await;
}
let port: u16 = port.parse()?;
log::info!("Listening on tcp :{}", port);
let port2 = port + 2;
log::info!("Listening on websocket :{}", port2);
let main_task = async move {
loop {
log::info!("Start");
io_loop(listen_any(port).await?, listen_any(port2).await?, &key).await;
}
};
let listen_signal = crate::common::listen_signal();
tokio::select!(
res = main_task => res,
res = listen_signal => res,
)
}
fn check_params() {
@@ -151,8 +153,10 @@ fn check_params() {
}
async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
use std::fmt::Write;
let mut res = "".to_owned();
let mut fds = cmd.trim().split(" ");
let mut fds = cmd.trim().split(' ');
match fds.next() {
Some("h") => {
res = format!(
@@ -173,7 +177,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
}
Some("blacklist-add" | "ba") => {
if let Some(ip) = fds.next() {
for ip in ip.split("|") {
for ip in ip.split('|') {
BLACKLIST.write().await.insert(ip.to_owned());
}
}
@@ -183,7 +187,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
if ip == "all" {
BLACKLIST.write().await.clear();
} else {
for ip in ip.split("|") {
for ip in ip.split('|') {
BLACKLIST.write().await.remove(ip);
}
}
@@ -194,13 +198,13 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
res = format!("{}\n", BLACKLIST.read().await.get(ip).is_some());
} else {
for ip in BLACKLIST.read().await.clone().into_iter() {
res += &format!("{}\n", ip);
let _ = writeln!(res, "{ip}");
}
}
}
Some("blocklist-add" | "Ba") => {
if let Some(ip) = fds.next() {
for ip in ip.split("|") {
for ip in ip.split('|') {
BLOCKLIST.write().await.insert(ip.to_owned());
}
}
@@ -210,7 +214,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
if ip == "all" {
BLOCKLIST.write().await.clear();
} else {
for ip in ip.split("|") {
for ip in ip.split('|') {
BLOCKLIST.write().await.remove(ip);
}
}
@@ -221,7 +225,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
res = format!("{}\n", BLOCKLIST.read().await.get(ip).is_some());
} else {
for ip in BLOCKLIST.read().await.clone().into_iter() {
res += &format!("{}\n", ip);
let _ = writeln!(res, "{ip}");
}
}
}
@@ -236,7 +240,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
}
} else {
unsafe {
res = format!("{}\n", DOWNGRADE_THRESHOLD);
res = format!("{DOWNGRADE_THRESHOLD}\n");
}
}
}
@@ -306,15 +310,16 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
.read()
.await
.iter()
.map(|x| (x.0.clone(), x.1.clone()))
.map(|x| (x.0.clone(), *x.1))
.collect();
tmp.sort_by(|a, b| ((b.1).1).partial_cmp(&(a.1).1).unwrap());
for (ip, (elapsed, total, highest, speed)) in tmp {
if elapsed <= 0 {
if elapsed == 0 {
continue;
}
res += &format!(
"{}: {}s {:.2}MB {}kb/s {}kb/s {}kb/s\n",
let _ = writeln!(
res,
"{}: {}s {:.2}MB {}kb/s {}kb/s {}kb/s",
ip,
elapsed / 1000,
total as f64 / 1024. / 1024. / 8.,
@@ -374,7 +379,7 @@ async fn handle_connection(
let limiter = limiter.clone();
tokio::spawn(async move {
let mut stream = stream;
let mut buffer = [0; 64];
let mut buffer = [0; 1024];
if let Ok(Ok(n)) = timeout(1000, stream.read(&mut buffer[..])).await {
if let Ok(data) = std::str::from_utf8(&buffer[..n]) {
let res = check_cmd(data, limiter).await;
@@ -489,7 +494,7 @@ async fn relay(
total_limiter.consume(nb).await;
total += nb;
total_s += nb;
if bytes.len() > 0 {
if !bytes.is_empty() {
stream.send_raw(bytes.into()).await?;
}
} else {
@@ -508,7 +513,7 @@ async fn relay(
total_limiter.consume(nb).await;
total += nb;
total_s += nb;
if bytes.len() > 0 {
if !bytes.is_empty() {
peer.send_raw(bytes.into()).await?;
}
} else {
@@ -530,7 +535,7 @@ async fn relay(
}
blacked = BLACKLIST.read().await.get(&ip).is_some();
tm = std::time::Instant::now();
let speed = total_s / (n as usize);
let speed = total_s / n;
if speed > highest_s {
highest_s = speed;
}
@@ -540,16 +545,17 @@ async fn relay(
(elapsed as _, total as _, highest_s as _, speed as _),
);
total_s = 0;
if elapsed > unsafe { DOWNGRADE_START_CHECK } && !downgrade {
if total > elapsed * downgrade_threshold {
downgrade = true;
log::info!(
"Downgrade {}, exceed downgrade threshold {}bit/ms in {}ms",
id,
downgrade_threshold,
elapsed
);
}
if elapsed > unsafe { DOWNGRADE_START_CHECK }
&& !downgrade
&& total > elapsed * downgrade_threshold
{
downgrade = true;
log::info!(
"Downgrade {}, exceed downgrade threshold {}bit/ms in {}ms",
id,
downgrade_threshold,
elapsed
);
}
}
}

View File

@@ -4,6 +4,7 @@ use hbb_common::{
allow_err,
bytes::{Bytes, BytesMut},
bytes_codec::BytesCodec,
config,
futures::future::join_all,
futures_util::{
sink::SinkExt,
@@ -15,7 +16,7 @@ use hbb_common::{
register_pk_response::Result::{TOO_FREQUENT, UUID_MISMATCH},
*,
},
tcp::{new_listener, FramedStream},
tcp::{listen_any, FramedStream},
timeout,
tokio::{
self,
@@ -25,6 +26,7 @@ use hbb_common::{
time::{interval, Duration},
},
tokio_util::codec::Framed,
try_into_v4,
udp::FramedSocket,
AddrMangle, ResultType,
};
@@ -32,15 +34,14 @@ use ipnetwork::Ipv4Network;
use sodiumoxide::crypto::sign;
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, SocketAddr},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
sync::Arc,
time::Instant,
};
const ADDR_127: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
#[derive(Clone, Debug)]
enum Data {
Msg(RendezvousMessage, SocketAddr),
Msg(Box<RendezvousMessage>, SocketAddr),
RelayServers0(String),
RelayServers(RelayServers),
}
@@ -91,16 +92,15 @@ impl RendezvousServer {
#[tokio::main(flavor = "multi_thread")]
pub async fn start(port: i32, serial: i32, key: &str, rmem: usize) -> ResultType<()> {
let (key, sk) = Self::get_server_sk(key);
let addr = format!("0.0.0.0:{}", port);
let addr2 = format!("0.0.0.0:{}", port - 1);
let addr3 = format!("0.0.0.0:{}", port + 2);
let nat_port = port - 1;
let ws_port = port + 2;
let pm = PeerMap::new().await?;
log::info!("serial={}", serial);
let rendezvous_servers = get_servers(&get_arg("rendezvous-servers"), "rendezvous-servers");
log::info!("Listening on tcp/udp {}", addr);
log::info!("Listening on tcp {}, extra port for NAT test", addr2);
log::info!("Listening on websocket {}", addr3);
let mut socket = FramedSocket::new_with_buf_size(&addr, rmem).await?;
log::info!("Listening on tcp/udp :{}", port);
log::info!("Listening on tcp :{}, extra port for NAT test", nat_port);
log::info!("Listening on websocket :{}", ws_port);
let mut socket = create_udp_listener(port, rmem).await?;
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
let software_url = get_arg("software-url");
let version = hbb_common::get_version_from_url(&software_url);
@@ -138,9 +138,9 @@ impl RendezvousServer {
log::info!("local-ip: {:?}", rs.inner.local_ip);
std::env::set_var("PORT_FOR_API", port.to_string());
rs.parse_relay_servers(&get_arg("relay-servers"));
let mut listener = new_listener(&addr, false).await?;
let mut listener2 = new_listener(&addr2, false).await?;
let mut listener3 = new_listener(&addr3, false).await?;
let mut listener = create_tcp_listener(port).await?;
let mut listener2 = create_tcp_listener(nat_port).await?;
let mut listener3 = create_tcp_listener(ws_port).await?;
let test_addr = std::env::var("TEST_HBBS").unwrap_or_default();
if std::env::var("ALWAYS_USE_RELAY")
.unwrap_or_default()
@@ -160,47 +160,53 @@ impl RendezvousServer {
}
);
if test_addr.to_lowercase() != "no" {
let test_addr = (if test_addr.is_empty() {
addr.replace("0.0.0.0", "127.0.0.1")
let test_addr = if test_addr.is_empty() {
listener.local_addr()?
} else {
test_addr
})
.parse::<SocketAddr>()?;
test_addr.parse()?
};
tokio::spawn(async move {
allow_err!(test_hbbs(test_addr).await);
});
};
loop {
log::info!("Start");
match rs
.io_loop(
&mut rx,
&mut listener,
&mut listener2,
&mut listener3,
&mut socket,
&key,
)
.await
{
LoopFailure::UdpSocket => {
drop(socket);
socket = FramedSocket::new_with_buf_size(&addr, rmem).await?;
}
LoopFailure::Listener => {
drop(listener);
listener = new_listener(&addr, false).await?;
}
LoopFailure::Listener2 => {
drop(listener2);
listener2 = new_listener(&addr2, false).await?;
}
LoopFailure::Listener3 => {
drop(listener3);
listener3 = new_listener(&addr3, false).await?;
let main_task = async move {
loop {
log::info!("Start");
match rs
.io_loop(
&mut rx,
&mut listener,
&mut listener2,
&mut listener3,
&mut socket,
&key,
)
.await
{
LoopFailure::UdpSocket => {
drop(socket);
socket = create_udp_listener(port, rmem).await?;
}
LoopFailure::Listener => {
drop(listener);
listener = create_tcp_listener(port).await?;
}
LoopFailure::Listener2 => {
drop(listener2);
listener2 = create_tcp_listener(nat_port).await?;
}
LoopFailure::Listener3 => {
drop(listener3);
listener3 = create_tcp_listener(ws_port).await?;
}
}
}
}
};
let listen_signal = listen_signal();
tokio::select!(
res = main_task => res,
res = listen_signal => res,
)
}
async fn io_loop(
@@ -226,7 +232,7 @@ impl RendezvousServer {
}
Some(data) = rx.recv() => {
match data {
Data::Msg(msg, addr) => { allow_err!(socket.send(&msg, addr).await); }
Data::Msg(msg, addr) => { allow_err!(socket.send(msg.as_ref(), addr).await); }
Data::RelayServers0(rs) => { self.parse_relay_servers(&rs); }
Data::RelayServers(rs) => { self.relay_servers = Arc::new(rs); }
}
@@ -296,11 +302,11 @@ impl RendezvousServer {
socket: &mut FramedSocket,
key: &str,
) -> ResultType<()> {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) {
match msg_in.union {
Some(rendezvous_message::Union::RegisterPeer(rp)) => {
// B registered
if rp.id.len() > 0 {
if !rp.id.is_empty() {
log::trace!("New peer registered: {:?} {:?}", &rp.id, &addr);
self.update_addr(rp.id, addr, socket).await?;
if self.inner.serial > rp.serial {
@@ -377,12 +383,10 @@ impl RendezvousServer {
*tm = Instant::now();
ips.clear();
ips.insert(ip.clone(), 1);
} else if let Some(v) = ips.get_mut(&ip) {
*v += 1;
} else {
if let Some(v) = ips.get_mut(&ip) {
*v += 1;
} else {
ips.insert(ip.clone(), 1);
}
ips.insert(ip.clone(), 1);
}
} else {
lock.insert(
@@ -420,7 +424,7 @@ impl RendezvousServer {
self.handle_local_addr(la, addr, Some(socket)).await?;
}
Some(rendezvous_message::Union::ConfigureUpdate(mut cu)) => {
if addr.ip() == ADDR_127 && cu.serial > self.inner.serial {
if addr.ip().is_loopback() && cu.serial > self.inner.serial {
let mut inner: Inner = (*self.inner).clone();
inner.serial = cu.serial;
self.inner = Arc::new(inner);
@@ -465,27 +469,27 @@ impl RendezvousServer {
key: &str,
ws: bool,
) -> bool {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) {
match msg_in.union {
Some(rendezvous_message::Union::PunchHoleRequest(ph)) => {
// there maybe several attempt, so sink can be none
if let Some(sink) = sink.take() {
self.tcp_punch.lock().await.insert(addr, sink);
self.tcp_punch.lock().await.insert(try_into_v4(addr), sink);
}
allow_err!(self.handle_tcp_punch_hole_request(addr, ph, &key, ws).await);
allow_err!(self.handle_tcp_punch_hole_request(addr, ph, key, ws).await);
return true;
}
Some(rendezvous_message::Union::RequestRelay(mut rf)) => {
// there maybe several attempt, so sink can be none
if let Some(sink) = sink.take() {
self.tcp_punch.lock().await.insert(addr, sink);
self.tcp_punch.lock().await.insert(try_into_v4(addr), sink);
}
if let Some(peer) = self.pm.get_in_memory(&rf.id).await {
let mut msg_out = RendezvousMessage::new();
rf.socket_addr = AddrMangle::encode(addr).into();
msg_out.set_request_relay(rf);
let peer_addr = peer.read().await.socket_addr;
self.tx.send(Data::Msg(msg_out, peer_addr)).ok();
self.tx.send(Data::Msg(msg_out.into(), peer_addr)).ok();
}
return true;
}
@@ -559,7 +563,7 @@ impl RendezvousServer {
ip != old.socket_addr.ip()
} else {
ip.to_string() != old.info.ip
} && ip != ADDR_127;
} && !ip.is_loopback();
let request_pk = old.pk.is_empty() || ip_change;
if !request_pk {
old.socket_addr = socket_addr;
@@ -740,14 +744,14 @@ impl RendezvousServer {
..Default::default()
});
}
return Ok((msg_out, Some(peer_addr)));
Ok((msg_out, Some(peer_addr)))
} else {
let mut msg_out = RendezvousMessage::new();
msg_out.set_punch_hole_response(PunchHoleResponse {
failure: punch_hole_response::Failure::ID_NOT_EXIST.into(),
..Default::default()
});
return Ok((msg_out, None));
Ok((msg_out, None))
}
}
@@ -758,8 +762,8 @@ impl RendezvousServer {
peers: Vec<String>,
) -> ResultType<()> {
let mut states = BytesMut::zeroed((peers.len() + 7) / 8);
for i in 0..peers.len() {
if let Some(peer) = self.pm.get_in_memory(&peers[i]).await {
for (i, peer_id) in peers.iter().enumerate() {
if let Some(peer) = self.pm.get_in_memory(peer_id).await {
let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i32;
// bytes index from left to right
let states_idx = i / 8;
@@ -782,7 +786,7 @@ impl RendezvousServer {
#[inline]
async fn send_to_tcp(&mut self, msg: RendezvousMessage, addr: SocketAddr) {
let mut tcp = self.tcp_punch.lock().await.remove(&addr);
let mut tcp = self.tcp_punch.lock().await.remove(&try_into_v4(addr));
tokio::spawn(async move {
Self::send_to_sink(&mut tcp, msg).await;
});
@@ -810,7 +814,7 @@ impl RendezvousServer {
msg: RendezvousMessage,
addr: SocketAddr,
) -> ResultType<()> {
let mut sink = self.tcp_punch.lock().await.remove(&addr);
let mut sink = self.tcp_punch.lock().await.remove(&try_into_v4(addr));
Self::send_to_sink(&mut sink, msg).await;
Ok(())
}
@@ -825,7 +829,7 @@ impl RendezvousServer {
) -> ResultType<()> {
let (msg, to_addr) = self.handle_punch_hole_request(addr, ph, key, ws).await?;
if let Some(addr) = to_addr {
self.tx.send(Data::Msg(msg, addr))?;
self.tx.send(Data::Msg(msg.into(), addr))?;
} else {
self.send_to_tcp_sync(msg, addr).await?;
}
@@ -841,7 +845,7 @@ impl RendezvousServer {
) -> ResultType<()> {
let (msg, to_addr) = self.handle_punch_hole_request(addr, ph, key, false).await?;
self.tx.send(Data::Msg(
msg,
msg.into(),
match to_addr {
Some(addr) => addr,
None => addr,
@@ -900,8 +904,10 @@ impl RendezvousServer {
}
async fn check_cmd(&self, cmd: &str) -> String {
use std::fmt::Write as _;
let mut res = "".to_owned();
let mut fds = cmd.trim().split(" ");
let mut fds = cmd.trim().split(' ');
match fds.next() {
Some("h") => {
res = format!(
@@ -919,7 +925,7 @@ impl RendezvousServer {
self.tx.send(Data::RelayServers0(rs.to_owned())).ok();
} else {
for ip in self.relay_servers.iter() {
res += &format!("{}\n", ip);
let _ = writeln!(res, "{ip}");
}
}
}
@@ -935,8 +941,9 @@ impl RendezvousServer {
if start < 0 {
if let Some(ip) = ip {
if let Some((a, b)) = lock.get(ip) {
res += &format!(
"{}/{}s {}/{}s\n",
let _ = writeln!(
res,
"{}/{}s {}/{}s",
a.0,
a.1.elapsed().as_secs(),
b.0.len(),
@@ -961,8 +968,9 @@ impl RendezvousServer {
continue;
}
if let Some((ip, (a, b))) = x {
res += &format!(
"{}: {}/{}s {}/{}s\n",
let _ = writeln!(
res,
"{}: {}/{}s {}/{}s",
ip,
a.0,
a.1.elapsed().as_secs(),
@@ -979,10 +987,10 @@ impl RendezvousServer {
res = format!("{}\n", lock.len());
let id = fds.next();
let mut start = id.map(|x| x.parse::<i32>().unwrap_or(-1)).unwrap_or(-1);
if start < 0 || start > 10_000_000 {
if !(0..=10_000_000).contains(&start) {
if let Some(id) = id {
if let Some((tm, ips)) = lock.get(id) {
res += &format!("{}s {:?}\n", tm.elapsed().as_secs(), ips);
let _ = writeln!(res, "{}s {:?}", tm.elapsed().as_secs(), ips);
}
if fds.next() == Some("-") {
lock.remove(id);
@@ -1002,7 +1010,7 @@ impl RendezvousServer {
continue;
}
if let Some((id, (tm, ips))) = x {
res += &format!("{}: {}s {:?}\n", id, tm.elapsed().as_secs(), ips,);
let _ = writeln!(res, "{}: {}s {:?}", id, tm.elapsed().as_secs(), ips,);
}
}
}
@@ -1016,7 +1024,7 @@ impl RendezvousServer {
}
self.tx.send(Data::RelayServers0(rs.to_owned())).ok();
} else {
res += &format!("ALWAYS_USE_RELAY: {:?}\n", unsafe { ALWAYS_USE_RELAY });
let _ = writeln!(res, "ALWAYS_USE_RELAY: {:?}", unsafe { ALWAYS_USE_RELAY });
}
}
Some("test-geo" | "tg") => {
@@ -1039,10 +1047,10 @@ impl RendezvousServer {
async fn handle_listener2(&self, stream: TcpStream, addr: SocketAddr) {
let mut rs = self.clone();
if addr.ip().to_string() == "127.0.0.1" {
if addr.ip().is_loopback() {
tokio::spawn(async move {
let mut stream = stream;
let mut buffer = [0; 64];
let mut buffer = [0; 1024];
if let Ok(Ok(n)) = timeout(1000, stream.read(&mut buffer[..])).await {
if let Ok(data) = std::str::from_utf8(&buffer[..n]) {
let res = rs.check_cmd(data).await;
@@ -1099,13 +1107,10 @@ impl RendezvousServer {
let (a, mut b) = ws_stream.split();
sink = Some(Sink::Ws(a));
while let Ok(Some(Ok(msg))) = timeout(30_000, b.next()).await {
match msg {
tungstenite::Message::Binary(bytes) => {
if !self.handle_tcp(&bytes, &mut sink, addr, key, ws).await {
break;
}
if let tungstenite::Message::Binary(bytes) = msg {
if !self.handle_tcp(&bytes, &mut sink, addr, key, ws).await {
break;
}
_ => {}
}
}
} else {
@@ -1118,7 +1123,7 @@ impl RendezvousServer {
}
}
if sink.is_none() {
self.tcp_punch.lock().await.remove(&addr);
self.tcp_punch.lock().await.remove(&try_into_v4(addr));
}
log::debug!("Tcp connection from {:?} closed", addr);
Ok(())
@@ -1131,7 +1136,7 @@ impl RendezvousServer {
} else {
match self.pm.get(&id).await {
Some(peer) => {
let pk = peer.read().await.pk.clone().into();
let pk = peer.read().await.pk.clone();
sign::sign(
&hbb_common::message_proto::IdPk {
id,
@@ -1140,7 +1145,7 @@ impl RendezvousServer {
}
.write_to_bytes()
.unwrap_or_default(),
&self.inner.sk.as_ref().unwrap(),
self.inner.sk.as_ref().unwrap(),
)
.into()
}
@@ -1196,13 +1201,13 @@ async fn check_relay_servers(rs0: Arc<RelayServers>, tx: Sender) {
let rs = Arc::new(Mutex::new(Vec::new()));
for x in rs0.iter() {
let mut host = x.to_owned();
if !host.contains(":") {
host = format!("{}:{}", host, hbb_common::config::RELAY_PORT);
if !host.contains(':') {
host = format!("{}:{}", host, config::RELAY_PORT);
}
let rs = rs.clone();
let x = x.clone();
futs.push(tokio::spawn(async move {
if FramedStream::new(&host, "0.0.0.0:0", CHECK_RELAY_TIMEOUT)
if FramedStream::new(&host, None, CHECK_RELAY_TIMEOUT)
.await
.is_ok()
{
@@ -1212,7 +1217,7 @@ async fn check_relay_servers(rs0: Arc<RelayServers>, tx: Sender) {
}
join_all(futs).await;
log::debug!("check_relay_servers");
let rs = std::mem::replace(&mut *rs.lock().await, Default::default());
let rs = std::mem::take(&mut *rs.lock().await);
if !rs.is_empty() {
tx.send(Data::RelayServers(rs)).ok();
}
@@ -1220,7 +1225,7 @@ async fn check_relay_servers(rs0: Arc<RelayServers>, tx: Sender) {
// temp solution to solve udp socket failure
async fn test_hbbs(addr: SocketAddr) -> ResultType<()> {
let mut socket = FramedSocket::new("0.0.0.0:0").await?;
let mut socket = FramedSocket::new(config::Config::get_any_listen_addr(addr.is_ipv4())).await?;
let mut msg_out = RendezvousMessage::new();
msg_out.set_register_peer(RegisterPeer {
id: "(:test_hbbs:)".to_owned(),
@@ -1261,3 +1266,22 @@ async fn send_rk_res(
});
socket.send(&msg_out, addr).await
}
async fn create_udp_listener(port: i32, rmem: usize) -> ResultType<FramedSocket> {
let addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port as _);
if let Ok(s) = FramedSocket::new_reuse(&addr, false, rmem).await {
log::debug!("listen on udp {:?}", s.local_addr());
return Ok(s);
}
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port as _);
let s = FramedSocket::new_reuse(&addr, false, rmem).await?;
log::debug!("listen on udp {:?}", s.local_addr());
Ok(s)
}
#[inline]
async fn create_tcp_listener(port: i32) -> ResultType<TcpListener> {
let s = listen_any(port as _).await?;
log::debug!("listen on tcp {:?}", s.local_addr());
Ok(s)
}

View File

@@ -20,7 +20,7 @@ Available Commands:
}
fn error_then_help(msg: &str) {
println!("ERROR: {}\n", msg);
println!("ERROR: {msg}\n");
print_help();
}
@@ -33,7 +33,7 @@ fn gen_keypair() {
}
fn validate_keypair(pk: &str, sk: &str) -> ResultType<()> {
let sk1 = base64::decode(&sk);
let sk1 = base64::decode(sk);
if sk1.is_err() {
bail!("Invalid secret key");
}
@@ -45,7 +45,7 @@ fn validate_keypair(pk: &str, sk: &str) -> ResultType<()> {
}
let secret_key = secret_key.unwrap();
let pk1 = base64::decode(&pk);
let pk1 = base64::decode(pk);
if pk1.is_err() {
bail!("Invalid public key");
}
@@ -74,7 +74,7 @@ fn validate_keypair(pk: &str, sk: &str) -> ResultType<()> {
fn doctor_tcp(address: std::net::IpAddr, port: &str, desc: &str) {
let start = std::time::Instant::now();
let conn = format!("{}:{}", address, port);
let conn = format!("{address}:{port}");
if let Ok(_stream) = TcpStream::connect(conn.as_str()) {
let elapsed = std::time::Instant::now().duration_since(start);
println!(
@@ -84,26 +84,24 @@ fn doctor_tcp(address: std::net::IpAddr, port: &str, desc: &str) {
elapsed.as_millis()
);
} else {
println!("TCP Port {} ({}): ERROR", port, desc);
println!("TCP Port {port} ({desc}): ERROR");
}
}
fn doctor_ip(server_ip_address: std::net::IpAddr, server_address: Option<&str>) {
println!("\nChecking IP address: {}", server_ip_address);
println!("\nChecking IP address: {server_ip_address}");
println!("Is IPV4: {}", server_ip_address.is_ipv4());
println!("Is IPV6: {}", server_ip_address.is_ipv6());
// reverse dns lookup
// TODO: (check) doesn't seem to do reverse lookup on OSX...
let reverse = lookup_addr(&server_ip_address).unwrap();
if server_address.is_some() {
if reverse == server_address.unwrap() {
println!("Reverse DNS lookup: '{}' MATCHES server address", reverse);
if let Some(server_address) = server_address {
if reverse == server_address {
println!("Reverse DNS lookup: '{reverse}' MATCHES server address");
} else {
println!(
"Reverse DNS lookup: '{}' DOESN'T MATCH server address '{}'",
reverse,
server_address.unwrap()
"Reverse DNS lookup: '{reverse}' DOESN'T MATCH server address '{server_address}'"
);
}
}
@@ -125,20 +123,19 @@ fn doctor(server_address_unclean: &str) {
let server_address3 = server_address_unclean.trim();
let server_address2 = server_address3.to_lowercase();
let server_address = server_address2.as_str();
println!("Checking server: {}\n", server_address);
let server_ipaddr = server_address.parse::<IpAddr>();
if server_ipaddr.is_err() {
println!("Checking server: {server_address}\n");
if let Ok(server_ipaddr) = server_address.parse::<IpAddr>() {
// user requested an ip address
doctor_ip(server_ipaddr, None);
} else {
// the passed string is not an ip address
let ips: Vec<std::net::IpAddr> = lookup_host(server_address).unwrap();
println!("Found {} IP addresses: ", ips.iter().count());
println!("Found {} IP addresses: ", ips.len());
ips.iter().for_each(|ip| println!(" - {ip}"));
ips.iter().for_each(|ip| doctor_ip(*ip, Some(server_address)));
} else {
// user requested an ip address
doctor_ip(server_ipaddr.unwrap(), None);
ips.iter()
.for_each(|ip| doctor_ip(*ip, Some(server_address)));
}
}
@@ -157,7 +154,7 @@ fn main() {
}
let res = validate_keypair(args[2].as_str(), args[3].as_str());
if let Err(e) = res {
println!("{}", e);
println!("{e}");
process::exit(0x0001);
}
println!("Key pair is VALID");

View File

@@ -1 +0,0 @@
pub const VERSION: &str = "1.1.6";

View File

@@ -10,6 +10,8 @@ WorkingDirectory=/var/lib/rustdesk-server/
User=
Group=
Restart=always
StandardOutput=append:/var/log/rustdesk/rustdesk-hbbr.log
StandardError=append:/var/log/rustdesk/rustdesk-hbbr.error
# Restart service after 10 seconds if node service crashes
RestartSec=10

View File

@@ -10,6 +10,8 @@ WorkingDirectory=/var/lib/rustdesk-server/
User=
Group=
Restart=always
StandardOutput=append:/var/log/rustdesk/rustdesk-hbbs.log
StandardError=append:/var/log/rustdesk/rustdesk-hbbs.error
# Restart service after 10 seconds if node service crashes
RestartSec=10

8
ui/.cargo/config.toml Normal file
View File

@@ -0,0 +1,8 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.'cfg(target_os="macos")']
rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
]

4
ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/

3787
ui/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

31
ui/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "rustdesk_server"
version = "0.1.2"
description = "rustdesk server gui"
authors = ["elilchen"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.2", features = [] }
winres = "0.1"
[dependencies]
async-std = { version = "1.12", features = ["attributes", "unstable"] }
crossbeam-channel = "0.5"
derive-new = "0.5"
notify = "5.1"
once_cell = "1.17"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["fs-exists", "fs-read-dir", "fs-read-file", "fs-write-file", "path-all", "shell-open", "system-tray"] }
windows-service = "0.5.0"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]

21
ui/build.rs Normal file
View File

@@ -0,0 +1,21 @@
fn main() {
tauri_build::build();
if cfg!(target_os = "windows") {
let mut res = winres::WindowsResource::new();
res.set_icon("icons\\icon.ico");
res.set_manifest(
r#"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
"#,
);
res.compile().unwrap();
}
}

24
ui/html/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

18
ui/html/index.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RustDesk Server</title>
<link rel="icon" href="data:;base64,=">
<script>addEventListener('contextmenu', e => e.preventDefault());</script>
<script type="module" src="/main.js" defer></script>
</head>
<body style="visibility: hidden">
<textarea></textarea>
<form>
<label><input type="checkbox"> <p>Turn on auto scroll</p></label>
<label><p>Press ctrl + s to save</p></label>
</form>
</body>
</html>

159
ui/html/main.js Normal file
View File

@@ -0,0 +1,159 @@
import 'codemirror/lib/codemirror.css';
import './style.css';
import 'codemirror/mode/toml/toml.js';
import CodeMirror from 'codemirror';
const { event, fs, path, tauri } = window.__TAURI__;
class View {
constructor() {
Object.assign(this, {
content: '',
action_time: 0,
is_auto_scroll: true,
is_edit_mode: false,
is_file_changed: false,
is_form_changed: false,
is_content_changed: false
}, ...arguments);
addEventListener('DOMContentLoaded', this.init.bind(this));
}
async init() {
this.editor = this.renderEditor();
this.editor.on('scroll', this.editorScroll.bind(this));
this.editor.on('keypress', this.editorSave.bind(this));
this.form = this.renderForm();
this.form.addEventListener('change', this.formChange.bind(this));
event.listen('__update__', this.appAction.bind(this));
event.emit('__action__', '__init__');
while (true) {
let now = Date.now();
try {
await this.update();
this.render();
} catch (e) {
console.error(e);
}
await new Promise(r => setTimeout(r, Math.max(0, 33 - (Date.now() - now))));
}
}
async update() {
if (this.is_file_changed) {
this.is_file_changed = false;
let now = Date.now(),
file = await path.resolveResource(this.file);
if (await fs.exists(file)) {
let content = await fs.readTextFile(file);
if (this.action_time < now) {
this.content = content;
this.is_content_changed = true;
}
} else {
if (now >= this.action_time) {
if (this.is_edit_mode) {
this.content = `# https://github.com/rustdesk/rustdesk-server#env-variables
RUST_LOG=info
`;
}
this.is_content_changed = true;
}
console.warn(`${this.file} file is missing`);
}
}
}
async editorSave(editor, e) {
if (e.ctrlKey && e.keyCode === 19 && this.is_edit_mode && !this.locked) {
this.locked = true;
try {
let now = Date.now(),
content = this.editor.doc.getValue(),
file = await path.resolveResource(this.file);
await fs.writeTextFile(file, content);
event.emit('__action__', 'restart');
} catch (e) {
console.error(e);
} finally {
this.locked = false;
}
}
}
editorScroll(e) {
let info = this.editor.getScrollInfo(),
distance = info.height - info.top - info.clientHeight,
is_end = distance < 1;
if (this.is_auto_scroll !== is_end) {
this.is_auto_scroll = is_end;
this.is_form_changed = true;
}
}
formChange(e) {
switch (e.target.tagName.toLowerCase()) {
case 'input':
this.is_auto_scroll = e.target.checked;
break;
}
}
appAction(e) {
let [action, data] = e.payload;
switch (action) {
case 'file':
if (data === '.env') {
this.is_edit_mode = true;
this.file = `bin/${data}`;
} else {
this.is_edit_mode = false;
this.file = `logs/${data}`;
}
this.action_time = Date.now();
this.is_file_changed = true;
this.is_form_changed = true;
break;
}
}
render() {
if (this.is_form_changed) {
this.is_form_changed = false;
this.renderForm();
}
if (this.is_content_changed) {
this.is_content_changed = false;
this.renderEditor();
}
if (this.is_auto_scroll && !this.is_edit_mode) {
this.renderScrollbar();
}
}
renderForm() {
let form = this.form || document.querySelector('form'),
label = form.querySelectorAll('label'),
input = form.querySelector('input');
input.checked = this.is_auto_scroll;
if (this.is_edit_mode) {
label[0].style.display = 'none';
label[1].style.display = 'block';
} else {
label[0].style.display = 'block';
label[1].style.display = 'none';
}
return form;
}
renderEditor() {
let editor = this.editor || CodeMirror.fromTextArea(document.querySelector('textarea'), {
mode: { name: 'toml' },
lineNumbers: true,
autofocus: true
});
editor.setOption('readOnly', !this.is_edit_mode);
editor.doc.setValue(this.content);
editor.doc.clearHistory();
this.content = '';
editor.focus();
return editor;
}
renderScrollbar() {
let info = this.editor.getScrollInfo();
this.editor.scrollTo(info.left, info.height);
}
}
new View();

17
ui/html/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "rustdesk_server",
"private": true,
"version": "0.1.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.1.0"
},
"dependencies": {
"codemirror": "v5"
}
}

35
ui/html/style.css Normal file
View File

@@ -0,0 +1,35 @@
body {
visibility: visible !important;
margin: 0;
background: #fff;
}
.CodeMirror {
height: calc(100vh - 20px);
}
form {
height: 20px;
position: fixed;
right: 0;
bottom: 0;
left: 5px;
font-size: 13px;
background: #fff;
}
form>label {
display: none;
vertical-align: middle;
}
form>label>input,
form>label>p {
height: 19px;
padding: 0;
display: inline-block;
margin: 0;
vertical-align: middle;
cursor: pointer;
user-select: none;
}

8
ui/html/vite.config.js Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: '5177',
strictPort: true
}
});

BIN
ui/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
ui/icons/128x128@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
ui/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
ui/icons/StoreLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
ui/icons/icon.icns Normal file

Binary file not shown.

BIN
ui/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
ui/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

178
ui/setup.nsi Normal file
View File

@@ -0,0 +1,178 @@
Unicode true
####################################################################
# Includes
!include nsDialogs.nsh
!include MUI2.nsh
!include x64.nsh
!include LogicLib.nsh
####################################################################
# File Info
!define APP_NAME "RustDeskServer"
!define PRODUCT_NAME "rustdesk_server"
!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}"
!define COPYRIGHT "Copyright © 2021"
!define VERSION "1.1.7"
VIProductVersion "${VERSION}.0"
VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
VIAddVersionKey "ProductVersion" "${VERSION}"
VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}"
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileVersion" "${VERSION}"
####################################################################
# Installer Attributes
Name "${APP_NAME}"
Outfile "${APP_NAME}.Setup.exe"
Caption "Setup - ${APP_NAME}"
BrandingText "${APP_NAME}"
ShowInstDetails show
RequestExecutionLevel admin
SetOverwrite on
InstallDir "$PROGRAMFILES64\${APP_NAME}"
####################################################################
# Pages
!define MUI_ICON "icons\icon.ico"
!define MUI_ABORTWARNING
!define MUI_LANGDLL_ALLLANGUAGES
!define MUI_FINISHPAGE_SHOWREADME ""
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Startup Shortcut"
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateStartupShortcut
!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
####################################################################
# Language
!insertmacro MUI_LANGUAGE "English" ; The first language is the default language
!insertmacro MUI_LANGUAGE "French"
!insertmacro MUI_LANGUAGE "German"
!insertmacro MUI_LANGUAGE "Spanish"
!insertmacro MUI_LANGUAGE "SpanishInternational"
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "TradChinese"
!insertmacro MUI_LANGUAGE "Japanese"
!insertmacro MUI_LANGUAGE "Korean"
!insertmacro MUI_LANGUAGE "Italian"
!insertmacro MUI_LANGUAGE "Dutch"
!insertmacro MUI_LANGUAGE "Danish"
!insertmacro MUI_LANGUAGE "Swedish"
!insertmacro MUI_LANGUAGE "Norwegian"
!insertmacro MUI_LANGUAGE "NorwegianNynorsk"
!insertmacro MUI_LANGUAGE "Finnish"
!insertmacro MUI_LANGUAGE "Greek"
!insertmacro MUI_LANGUAGE "Russian"
!insertmacro MUI_LANGUAGE "Portuguese"
!insertmacro MUI_LANGUAGE "PortugueseBR"
!insertmacro MUI_LANGUAGE "Polish"
!insertmacro MUI_LANGUAGE "Ukrainian"
!insertmacro MUI_LANGUAGE "Czech"
!insertmacro MUI_LANGUAGE "Slovak"
!insertmacro MUI_LANGUAGE "Croatian"
!insertmacro MUI_LANGUAGE "Bulgarian"
!insertmacro MUI_LANGUAGE "Hungarian"
!insertmacro MUI_LANGUAGE "Thai"
!insertmacro MUI_LANGUAGE "Romanian"
!insertmacro MUI_LANGUAGE "Latvian"
!insertmacro MUI_LANGUAGE "Macedonian"
!insertmacro MUI_LANGUAGE "Estonian"
!insertmacro MUI_LANGUAGE "Turkish"
!insertmacro MUI_LANGUAGE "Lithuanian"
!insertmacro MUI_LANGUAGE "Slovenian"
!insertmacro MUI_LANGUAGE "Serbian"
!insertmacro MUI_LANGUAGE "SerbianLatin"
!insertmacro MUI_LANGUAGE "Arabic"
!insertmacro MUI_LANGUAGE "Farsi"
!insertmacro MUI_LANGUAGE "Hebrew"
!insertmacro MUI_LANGUAGE "Indonesian"
!insertmacro MUI_LANGUAGE "Mongolian"
!insertmacro MUI_LANGUAGE "Luxembourgish"
!insertmacro MUI_LANGUAGE "Albanian"
!insertmacro MUI_LANGUAGE "Breton"
!insertmacro MUI_LANGUAGE "Belarusian"
!insertmacro MUI_LANGUAGE "Icelandic"
!insertmacro MUI_LANGUAGE "Malay"
!insertmacro MUI_LANGUAGE "Bosnian"
!insertmacro MUI_LANGUAGE "Kurdish"
!insertmacro MUI_LANGUAGE "Irish"
!insertmacro MUI_LANGUAGE "Uzbek"
!insertmacro MUI_LANGUAGE "Galician"
!insertmacro MUI_LANGUAGE "Afrikaans"
!insertmacro MUI_LANGUAGE "Catalan"
!insertmacro MUI_LANGUAGE "Esperanto"
!insertmacro MUI_LANGUAGE "Asturian"
!insertmacro MUI_LANGUAGE "Basque"
!insertmacro MUI_LANGUAGE "Pashto"
!insertmacro MUI_LANGUAGE "ScotsGaelic"
!insertmacro MUI_LANGUAGE "Georgian"
!insertmacro MUI_LANGUAGE "Vietnamese"
!insertmacro MUI_LANGUAGE "Welsh"
!insertmacro MUI_LANGUAGE "Armenian"
!insertmacro MUI_LANGUAGE "Corsican"
!insertmacro MUI_LANGUAGE "Tatar"
!insertmacro MUI_LANGUAGE "Hindi"
####################################################################
# Sections
Section "Install"
SetShellVarContext all
nsExec::Exec 'sc stop hbbr'
nsExec::Exec 'sc stop hbbs'
nsExec::Exec 'taskkill /F /IM ${PRODUCT_NAME}.exe'
Sleep 500
SetOutPath $INSTDIR
File /r "setup\*.*"
WriteUninstaller $INSTDIR\uninstall.exe
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
CreateShortCut "$SMPROGRAMS\${APP_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\hbbs.exe" enable=yes'
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\hbbs.exe" enable=yes'
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\hbbr.exe" enable=yes'
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\hbbr.exe" enable=yes'
ExecWait 'powershell.exe -NoProfile -windowstyle hidden try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 } catch {}; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ($\'/silent$\', $\'/install$\') -Wait'
SectionEnd
Section "Uninstall"
SetShellVarContext all
nsExec::Exec 'sc stop hbbr'
nsExec::Exec 'sc stop hbbs'
nsExec::Exec 'taskkill /F /IM ${PRODUCT_NAME}.exe'
Sleep 500
RMDir /r "$SMPROGRAMS\${APP_NAME}"
Delete "$SMSTARTUP\${APP_NAME}.lnk"
Delete "$DESKTOP\${APP_NAME}.lnk"
nsExec::Exec 'sc delete hbbr'
nsExec::Exec 'sc delete hbbs'
nsExec::Exec 'netsh advfirewall firewall delete rule name="${APP_NAME}"'
RMDir /r "$INSTDIR\bin"
RMDir /r "$INSTDIR\logs"
RMDir /r "$INSTDIR\service"
Delete "$INSTDIR\${PRODUCT_NAME}.exe"
Delete "$INSTDIR\uninstall.exe"
SectionEnd
####################################################################
# Functions
Function CreateStartupShortcut
CreateShortCut "$SMSTARTUP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
FunctionEnd

BIN
ui/setup/service/nssm.exe Normal file

Binary file not shown.

23
ui/setup/service/run.cmd Normal file
View File

@@ -0,0 +1,23 @@
@echo off
%~d0
cd "%~dp0"
set nssm="%cd%\nssm"
cd ..
%nssm% install %1 "%cd%\bin\%1.exe"
%nssm% set %1 DisplayName %1
%nssm% set %1 Description rustdesk %1 server
%nssm% set %1 Start SERVICE_AUTO_START
%nssm% set %1 ObjectName LocalSystem
%nssm% set %1 Type SERVICE_WIN32_OWN_PROCESS
%nssm% set %1 AppThrottle 1000
%nssm% set %1 AppExit Default Restart
%nssm% set %1 AppRestartDelay 0
%nssm% set %1 AppStdout "%cd%\logs\%1.out"
%nssm% set %1 AppStderr "%cd%\logs\%1.err"
%nssm% start %1

5
ui/src/adapter/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod view;
pub mod service;
pub use view::*;
pub use service::*;

View File

@@ -0,0 +1,3 @@
pub mod windows;
pub use windows::*;

View File

@@ -0,0 +1,130 @@
use std::{ffi::OsStr, process::Command};
use crate::{path, usecase::service::*};
use derive_new::new;
use windows_service::{
service::ServiceAccess,
service_manager::{ServiceManager, ServiceManagerAccess},
};
#[derive(Debug, new)]
pub struct WindowsDesktopService {
#[new(value = "DesktopServiceState::Stopped")]
pub state: DesktopServiceState,
}
impl IDesktopService for WindowsDesktopService {
fn start(&mut self) {
call(
[
"echo.",
"%nssm% stop hbbr",
"%nssm% remove hbbr confirm",
"%nssm% stop hbbs",
"%nssm% remove hbbs confirm",
"mkdir logs",
"echo.",
"service\\run.cmd hbbs",
"echo.",
"service\\run.cmd hbbr",
"echo.",
"@ping 127.1 -n 3 >nul",
]
.join(" & "),
);
self.check();
}
fn stop(&mut self) {
call(
[
"echo.",
"%nssm% stop hbbr",
"%nssm% remove hbbr confirm",
"echo.",
"%nssm% stop hbbs",
"%nssm% remove hbbs confirm",
"echo.",
"@ping 127.1 -n 3 >nul",
]
.join(" & "),
);
self.check();
}
fn restart(&mut self) {
nssm(["restart", "hbbs"].map(|x| x.to_owned()));
nssm(["restart", "hbbr"].map(|x| x.to_owned()));
self.check();
}
fn pause(&mut self) {
call(
[
"echo.",
"%nssm% stop hbbr",
"echo.",
"%nssm% stop hbbs",
"echo.",
"@ping 127.1 -n 3 >nul",
]
.join(" & "),
);
self.check();
}
fn check(&mut self) -> DesktopServiceState {
self.state = match service_status("hbbs").as_str() {
"Running" => DesktopServiceState::Started,
// "Stopped" => DeskServerServiceState::Paused,
_ => DesktopServiceState::Stopped,
};
self.state.to_owned()
}
}
fn call(cmd: String) {
Command::new("cmd")
.current_dir(&path())
.env("nssm", "service\\nssm.exe")
.arg("/c")
.arg("start")
.arg("cmd")
.arg("/c")
.arg(cmd)
.output()
.expect("cmd exec error!");
}
fn exec<I, S>(program: S, args: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
match Command::new(program).args(args).output() {
Ok(out) => String::from_utf8(out.stdout).unwrap_or("".to_owned()),
Err(e) => e.to_string(),
}
}
fn nssm<I>(args: I) -> String
where
I: IntoIterator<Item = String>,
{
exec(
format!("{}\\service\\nssm.exe", path().to_str().unwrap_or_default()),
args,
)
.replace("\0", "")
.trim()
.to_owned()
}
fn service_status(name: &str) -> String {
match ServiceManager::local_computer(None::<&OsStr>, ServiceManagerAccess::CONNECT) {
Ok(manager) => match manager.open_service(name, ServiceAccess::QUERY_STATUS) {
Ok(service) => match service.query_status() {
Ok(status) => format!("{:?}", status.current_state),
Err(e) => e.to_string(),
},
Err(e) => e.to_string(),
},
Err(e) => e.to_string(),
}
}

View File

@@ -0,0 +1,221 @@
use std::{
process::exit,
time::{Duration, Instant},
};
use crate::{
path,
usecase::{view::Event, DesktopServiceState},
BUFFER,
};
use async_std::task::sleep;
use crossbeam_channel::{Receiver, Sender};
use tauri::{
CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu,
SystemTrayMenuItem, WindowEvent,
};
pub async fn run(sender: Sender<Event>, receiver: Receiver<Event>) {
let setup_sender = sender.clone();
let menu_sender = sender.clone();
let tray_sender = sender.clone();
let menu = Menu::new()
.add_submenu(Submenu::new(
"Service",
Menu::new()
.add_item(CustomMenuItem::new("restart", "Restart"))
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("start", "Start"))
.add_item(CustomMenuItem::new("stop", "Stop")),
))
.add_submenu(Submenu::new(
"Logs",
Menu::new()
.add_item(CustomMenuItem::new("hbbs.out", "hbbs.out"))
.add_item(CustomMenuItem::new("hbbs.err", "hbbs.err"))
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("hbbr.out", "hbbr.out"))
.add_item(CustomMenuItem::new("hbbr.err", "hbbr.err")),
))
.add_submenu(Submenu::new(
"Configuration",
Menu::new().add_item(CustomMenuItem::new(".env", ".env")),
));
let tray = SystemTray::new().with_menu(
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("restart", "Restart"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("start", "Start"))
.add_item(CustomMenuItem::new("stop", "Stop"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("exit", "Exit GUI")),
);
let mut app = tauri::Builder::default()
.on_window_event(|event| match event.event() {
// WindowEvent::Resized(size) => {
// if size.width == 0 && size.height == 0 {
// event.window().hide().unwrap();
// }
// }
WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
event.window().minimize().unwrap();
event.window().hide().unwrap();
}
_ => {}
})
.menu(menu)
.on_menu_event(move |event| {
// println!(
// "send {}: {}",
// std::time::SystemTime::now()
// .duration_since(std::time::UNIX_EPOCH)
// .unwrap_or_default()
// .as_millis(),
// event.menu_item_id()
// );
menu_sender
.send(Event::ViewAction(event.menu_item_id().to_owned()))
.unwrap_or_default()
})
.system_tray(tray)
.on_system_tray_event(move |app, event| match event {
SystemTrayEvent::LeftClick { .. } => {
let main = app.get_window("main").unwrap();
if main.is_visible().unwrap() {
main.hide().unwrap();
} else {
main.show().unwrap();
main.unminimize().unwrap();
main.set_focus().unwrap();
}
}
SystemTrayEvent::MenuItemClick { id, .. } => {
tray_sender.send(Event::ViewAction(id)).unwrap_or_default();
}
_ => {}
})
.setup(move |app| {
setup_sender.send(Event::ViewInit).unwrap_or_default();
app.listen_global("__action__", move |msg| {
match msg.payload().unwrap_or_default() {
r#""__init__""# => setup_sender.send(Event::BrowserInit).unwrap_or_default(),
r#""restart""# => setup_sender
.send(Event::BrowserAction("restart".to_owned()))
.unwrap_or_default(),
_ => (),
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![root])
.build(tauri::generate_context!())
.expect("error while running tauri application");
let mut now = Instant::now();
let mut blink = false;
let mut span = 0;
let mut title = "".to_owned();
let product = "RustDesk Server";
let buffer = BUFFER.get().unwrap().to_owned();
loop {
for _ in 1..buffer {
match receiver.recv_timeout(Duration::from_nanos(1)) {
Ok(event) => {
let main = app.get_window("main").unwrap();
let menu = main.menu_handle();
let tray = app.tray_handle();
match event {
Event::BrowserUpdate((action, data)) => match action.as_str() {
"file" => {
let list = ["hbbs.out", "hbbs.err", "hbbr.out", "hbbr.err", ".env"];
let id = data.as_str();
if list.contains(&id) {
for file in list {
menu.get_item(file)
.set_selected(file == id)
.unwrap_or_default();
}
// println!(
// "emit {}: {}",
// std::time::SystemTime::now()
// .duration_since(std::time::UNIX_EPOCH)
// .unwrap_or_default()
// .as_millis(),
// data
// );
app.emit_all("__update__", (action, data))
.unwrap_or_default();
}
}
_ => (),
},
Event::ViewRenderAppExit => exit(0),
Event::ViewRenderServiceState(state) => {
let enabled = |id, enabled| {
menu.get_item(id).set_enabled(enabled).unwrap_or_default();
tray.get_item(id).set_enabled(enabled).unwrap_or_default();
};
title = format!("{} {:?}", product, state);
main.set_title(title.as_str()).unwrap_or_default();
match state {
DesktopServiceState::Started => {
enabled("start", false);
enabled("stop", true);
enabled("restart", true);
blink = false;
}
DesktopServiceState::Stopped => {
enabled("start", true);
enabled("stop", false);
enabled("restart", false);
blink = true;
}
_ => {
enabled("start", false);
enabled("stop", false);
enabled("restart", false);
blink = true;
}
}
}
_ => (),
}
}
Err(_) => break,
}
}
let elapsed = now.elapsed().as_micros();
if elapsed > 16666 {
now = Instant::now();
// println!("{}ms", elapsed as f64 * 0.001);
let iteration = app.run_iteration();
if iteration.window_count == 0 {
break;
}
if blink {
if span > 1000000 {
span = 0;
app.get_window("main")
.unwrap()
.set_title(title.as_str())
.unwrap_or_default();
} else {
span += elapsed;
if span > 500000 {
app.get_window("main")
.unwrap()
.set_title(product)
.unwrap_or_default();
}
}
}
} else {
sleep(Duration::from_micros(999)).await;
}
}
}
#[tauri::command]
fn root() -> String {
path().to_str().unwrap_or_default().to_owned()
}

View File

@@ -0,0 +1,3 @@
pub mod desktop;
pub use desktop::*;

17
ui/src/lib.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::{env::current_exe, path::PathBuf};
use once_cell::sync::OnceCell;
pub mod adapter;
pub mod usecase;
pub static BUFFER: OnceCell<usize> = OnceCell::new();
pub fn path() -> PathBuf {
current_exe()
.unwrap_or_default()
.as_path()
.parent()
.unwrap()
.to_owned()
}

25
ui/src/main.rs Normal file
View File

@@ -0,0 +1,25 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use async_std::{
prelude::FutureExt,
task::{spawn, spawn_local},
};
use crossbeam_channel::bounded;
use rustdesk_server::{
usecase::{presenter, view, watcher},
BUFFER,
};
#[async_std::main]
async fn main() {
let buffer = BUFFER.get_or_init(|| 10).to_owned();
let (view_sender, presenter_receiver) = bounded(buffer);
let (presenter_sender, view_receiver) = bounded(buffer);
spawn_local(view::create(presenter_sender.clone(), presenter_receiver))
.join(spawn(presenter::create(view_sender, view_receiver)))
.join(spawn(watcher::create(presenter_sender)))
.await;
}

9
ui/src/usecase/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod presenter;
pub mod service;
pub mod view;
pub mod watcher;
pub use presenter::*;
pub use service::*;
pub use view::*;
pub use watcher::*;

View File

@@ -0,0 +1,59 @@
use std::time::{Duration, Instant};
use super::{service, DesktopServiceState, Event};
use crate::BUFFER;
use async_std::task::sleep;
use crossbeam_channel::{Receiver, Sender};
pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
let mut now = Instant::now();
let buffer = BUFFER.get().unwrap().to_owned();
let send = |event| sender.send(event).unwrap_or_default();
if let Some(mut service) = service::create() {
let mut service_state = DesktopServiceState::Unknown;
let mut file = "hbbs.out".to_owned();
send(Event::ViewRenderServiceState(service_state.to_owned()));
loop {
for _ in 1..buffer {
match receiver.recv_timeout(Duration::from_nanos(1)) {
Ok(event) => match event {
Event::BrowserInit => {
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
}
Event::BrowserAction(action) => match action.as_str() {
"restart" => service.restart(),
_ => (),
},
Event::FileChange(path) => {
if path == file {
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
}
}
Event::ViewAction(action) => match action.as_str() {
"start" => service.start(),
"stop" => service.stop(),
"restart" => service.restart(),
"pause" => service.pause(),
"exit" => send(Event::ViewRenderAppExit),
_ => {
file = action;
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
}
},
_ => (),
},
Err(_) => break,
}
}
sleep(Duration::from_micros(999)).await;
if now.elapsed().as_millis() > 999 {
let state = service.check();
if state != service_state {
service_state = state.to_owned();
send(Event::ViewRenderServiceState(state));
}
now = Instant::now();
}
}
}
}

24
ui/src/usecase/service.rs Normal file
View File

@@ -0,0 +1,24 @@
use crate::adapter;
pub fn create() -> Option<Box<dyn IDesktopService + Send>> {
if cfg!(target_os = "windows") {
return Some(Box::new(adapter::WindowsDesktopService::new()));
}
None
}
#[derive(Debug, Clone, PartialEq)]
pub enum DesktopServiceState {
Paused,
Started,
Stopped,
Unknown,
}
pub trait IDesktopService {
fn start(&mut self);
fn stop(&mut self);
fn restart(&mut self);
fn pause(&mut self);
fn check(&mut self) -> DesktopServiceState;
}

22
ui/src/usecase/view.rs Normal file
View File

@@ -0,0 +1,22 @@
use super::DesktopServiceState;
use crate::adapter::desktop;
use crossbeam_channel::{Receiver, Sender};
pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
desktop::run(sender, receiver).await;
}
#[derive(Debug, Clone, PartialEq)]
pub enum Event {
BrowserAction(String),
BrowserInit,
BrowserUpdate((String, String)),
BrowserRender(String),
FileChange(String),
ViewAction(String),
ViewInit,
ViewUpdate(String),
ViewRender(String),
ViewRenderAppExit,
ViewRenderServiceState(DesktopServiceState),
}

46
ui/src/usecase/watcher.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::{path::Path, time::Duration};
use super::Event;
use crate::path;
use async_std::task::{sleep, spawn_blocking};
use crossbeam_channel::{bounded, Sender};
use notify::{Config, RecommendedWatcher, RecursiveMode, Result, Watcher};
pub async fn create(sender: Sender<Event>) {
loop {
let watch_sender = sender.clone();
match spawn_blocking(|| {
watch(
format!("{}/logs/", path().to_str().unwrap_or_default()),
watch_sender,
)
})
.await
{
Ok(_) => (),
Err(e) => println!("error: {e}"),
}
sleep(Duration::from_secs(1)).await;
}
}
fn watch<P: AsRef<Path>>(path: P, sender: Sender<Event>) -> Result<()> {
let (tx, rx) = bounded(10);
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
for res in rx {
let event = res?;
for p in event.paths {
let path = p
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_owned();
if path.len() > 0 {
sender.send(Event::FileChange(path)).unwrap_or_default();
}
}
}
Ok(())
}

89
ui/tauri.conf.json Normal file
View File

@@ -0,0 +1,89 @@
{
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://127.0.0.1:5177/",
"distDir": "html/dist",
"withGlobalTauri": true
},
"package": {
"productName": "rustdesk_server",
"version": "0.1.2"
},
"tauri": {
"allowlist": {
"all": false,
"fs": {
"scope": [
"$RESOURCE/bin/.env",
"$RESOURCE/logs/*"
],
"all": false,
"exists": true,
"readDir": true,
"readFile": true,
"writeFile": true
},
"path": {
"all": true
},
"shell": {
"all": false,
"open": true
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "rustdesk.server",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"systemTray": {
"iconPath": "icons/icon.ico",
"iconAsTemplate": true
},
"updater": {
"active": false
},
"windows": [
{
"center": true,
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "RustDesk Server",
"width": 980
}
]
}
}