Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd7bc52c43 | ||
|
|
34cbd57e71 | ||
|
|
46e9ecd47d | ||
|
|
d0770e6c06 | ||
|
|
f87372ead0 | ||
|
|
04efb7033c | ||
|
|
9bae9f2f39 | ||
|
|
7c2057c360 | ||
|
|
f61b31f04e | ||
|
|
bb455a3eb7 | ||
|
|
631618a6be | ||
|
|
34946219c3 | ||
|
|
5d1b4ba78a | ||
|
|
7e921be539 | ||
|
|
716041bac1 | ||
|
|
6b72cefbc5 | ||
|
|
386c83874c | ||
|
|
5cc309dc9f | ||
|
|
4e8d6d08e2 | ||
|
|
c8a5d41906 | ||
|
|
2c57e51ec5 | ||
|
|
796a75af5c | ||
|
|
f09b79c6da | ||
|
|
1e84478837 | ||
|
|
2417c7ddd0 | ||
|
|
235a3c326c | ||
|
|
e4c85764f9 | ||
|
|
c650217919 | ||
|
|
262a1d49f8 | ||
|
|
f0a6c3de0b | ||
|
|
621da3c8fe | ||
|
|
801826f36d | ||
|
|
8557c4ab82 | ||
|
|
1fd1a2a3b6 | ||
|
|
bcdfda43af | ||
|
|
11a1f99169 | ||
|
|
464a687043 | ||
|
|
66c78c23fe | ||
|
|
e251161c5d | ||
|
|
b9e5968299 | ||
|
|
7a509f6975 | ||
|
|
772db7422f | ||
|
|
2f246537df | ||
|
|
4c74586ce0 | ||
|
|
2ac3169d77 | ||
|
|
6f18a97644 | ||
|
|
3b386b6b54 | ||
|
|
b37033d92c | ||
|
|
b7bab80bfe | ||
|
|
041a603173 | ||
|
|
e40994d62e | ||
|
|
5078a1f797 | ||
|
|
a22dacce0c | ||
|
|
064c9e4bb4 | ||
|
|
3cf0f6560f | ||
|
|
c4c26dd6d7 | ||
|
|
4240c47244 | ||
|
|
6e91f41a10 | ||
|
|
1a7cee157c | ||
|
|
72641270f1 | ||
|
|
19f8d3a0f4 | ||
|
|
bac9548f86 | ||
|
|
79f0eb497b | ||
|
|
94ae51458c | ||
|
|
778c89efb1 | ||
|
|
a7a0fa7cb5 | ||
|
|
2d8f6ae4f4 | ||
|
|
324dfd6a1f | ||
|
|
70242e6eb2 | ||
|
|
42cdfb0885 | ||
|
|
cea8403dbc | ||
|
|
0ebfc09f8b | ||
|
|
2e06125974 | ||
|
|
fc775102ff | ||
|
|
891f388040 | ||
|
|
8b7f3491b1 | ||
|
|
27ac9dec56 | ||
|
|
acf2c6d787 | ||
|
|
5f137710be | ||
|
|
1a6016f08f | ||
|
|
f67e8991ef | ||
|
|
d81010224d | ||
|
|
4c27143125 | ||
|
|
9461bbe8f3 | ||
|
|
1142cf105b | ||
|
|
5133af1863 | ||
|
|
7c7d554609 | ||
|
|
272a094fde | ||
|
|
b33d7954be | ||
|
|
f519be8e92 | ||
|
|
33331be361 | ||
|
|
04a9d307c5 | ||
|
|
35c2386c98 | ||
|
|
4f41300450 | ||
|
|
d6f99ed9a2 | ||
|
|
95fe0e5a78 | ||
|
|
b713303c15 | ||
|
|
d8f88e72c1 | ||
|
|
d3a459542d | ||
|
|
afeebe852d | ||
|
|
f1e941bf9f | ||
|
|
c871978475 | ||
|
|
411502cd0b | ||
|
|
243fb1fb06 | ||
|
|
9657dcf596 | ||
|
|
946845cd01 | ||
|
|
fd1c21b114 | ||
|
|
d8e3cb9e65 | ||
|
|
3a7904fa8e | ||
|
|
85a20769fb | ||
|
|
aeeca0d7d1 | ||
|
|
482d7fb8cc | ||
|
|
c291900e37 | ||
|
|
089352420f | ||
|
|
1addf8c9eb | ||
|
|
1f7d3fa05c | ||
|
|
c3b6e1351f | ||
|
|
336b281657 | ||
|
|
57898642e8 | ||
|
|
fa0c006ac5 | ||
|
|
178ff59623 | ||
|
|
7bbed69ad2 | ||
|
|
d4b6e6ee28 | ||
|
|
b3fbb9e179 | ||
|
|
b44eb1bbc3 | ||
|
|
d88286642e | ||
|
|
74ff886900 | ||
|
|
9e716b3b7b | ||
|
|
bea99ae315 | ||
|
|
6068b5941c | ||
|
|
675bf3c1f5 | ||
|
|
dc81956d42 | ||
|
|
ffe736be17 | ||
|
|
b83dae4cc4 | ||
|
|
8d9203ecdb | ||
|
|
13321b5a90 | ||
|
|
0a8c39c11f | ||
|
|
7dd812c79a | ||
|
|
9d524443ec | ||
|
|
35a192a478 | ||
|
|
2f4235a968 | ||
|
|
12b57238d2 | ||
|
|
fe805e8554 | ||
|
|
4d6d439b1a | ||
|
|
ec202209f3 | ||
|
|
49f10a288d | ||
|
|
986d16eb2d | ||
|
|
4fd83deaf1 | ||
|
|
85150127bb | ||
|
|
26d8c13fe4 | ||
|
|
388ae586ec | ||
|
|
6ad923d519 | ||
|
|
fe661fe067 | ||
|
|
ad40d65070 | ||
|
|
10bb0530ae | ||
|
|
7c3be2d9fb | ||
|
|
14301a7d5f | ||
|
|
d0841f7558 | ||
|
|
467298efa7 | ||
|
|
75203d2e4e | ||
|
|
27d8f9cbb4 | ||
|
|
7a0e300ff9 | ||
|
|
b2f381913d | ||
|
|
6ec46cb95f | ||
|
|
e2f4962ba8 | ||
|
|
7e307a5a1c | ||
|
|
33f54ba5aa | ||
|
|
6a83ffea62 | ||
|
|
af848f96df | ||
|
|
d88e4b5151 | ||
|
|
fe3b42809a | ||
|
|
2830be95a7 | ||
|
|
a974906fdc | ||
|
|
17ddc89bd0 | ||
|
|
088a009078 | ||
|
|
be2ce5c93b | ||
|
|
f8936eff93 | ||
|
|
accd96f1d8 | ||
|
|
32ee474813 | ||
|
|
46a7c025a0 | ||
|
|
3ca4035d0c | ||
|
|
86a75451d8 | ||
|
|
8cdfe0fec6 | ||
|
|
5aaad36729 | ||
|
|
fc83fa0a04 | ||
|
|
338af1af9d | ||
|
|
6538023a11 | ||
|
|
f139ad69a1 | ||
|
|
55fcf241c6 | ||
|
|
ebd73e1a09 | ||
|
|
00e6a016f8 | ||
|
|
78d7e9437e | ||
|
|
6bd5621fb0 | ||
|
|
ee794d2e40 | ||
|
|
605d0dd6c1 | ||
|
|
ebbe5d5297 | ||
|
|
cd1a9885db | ||
|
|
81d4fb6d6a | ||
|
|
a766aaf165 | ||
|
|
55b841afb5 | ||
|
|
d48913d7b5 | ||
|
|
1557203912 | ||
|
|
0e01cfcd3a | ||
|
|
e70d82b30f | ||
|
|
60a6d672c5 | ||
|
|
d7b2060a5b | ||
|
|
75a40412b4 | ||
|
|
93a89b8ea3 | ||
|
|
8f5ce48939 | ||
|
|
2314783d42 | ||
|
|
e732599941 | ||
|
|
29b45dddb4 | ||
|
|
650f2410ed | ||
|
|
c16101a44c | ||
|
|
4baab96183 | ||
|
|
ca2bc99a38 |
8
.cargo/config.toml
Normal 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",
|
||||
]
|
||||
14
.github/ISSUE_TEMPLATE/ask-a-question.md
vendored
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Ask a question
|
||||
about: Ask the community for help
|
||||
title: ''
|
||||
labels: 'question'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
This is the place for generic questions. Please stay on topic and be polite.
|
||||
|
||||
**Notes**
|
||||
- Please write in english only. If you provide some images in different languages, you're required to write a translation in english.
|
||||
- In any case, **NEVER** put here the content if your `id_ed25519` file
|
||||
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
target-branch: "master"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "Git submodule"
|
||||
labels:
|
||||
- "dependencies"
|
||||
305
.github/workflows/build.yaml
vendored
@@ -4,8 +4,12 @@ name: build
|
||||
# please setup some secrets before running this workflow:
|
||||
# DOCKER_IMAGE should be the target image name on docker hub (e.g. "rustdesk/rustdesk-server-s6" )
|
||||
# DOCKER_IMAGE_CLASSIC should be the target image name on docker hub for the old build (e.g. "rustdesk/rustdesk-server" )
|
||||
# DOCKER_USERNAME is the username you normally use to login at https://hub.docker.com/
|
||||
# DOCKER_PASSWORD is a token you should create under "account settings / security" with read/write access
|
||||
# DOCKER_HUB_USERNAME is the username you normally use to login at https://hub.docker.com/
|
||||
# DOCKER_HUB_PASSWORD is a token you should create under "account settings / security" with read/write access
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -19,6 +23,8 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
LATEST_TAG: latest
|
||||
GHCR_IMAGE: ghcr.io/rustdesk/rustdesk-server-s6
|
||||
GHCR_IMAGE_CLASSIC: ghcr.io/rustdesk/rustdesk-server
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -35,16 +41,19 @@ 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:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
toolchain: "1.90"
|
||||
override: true
|
||||
default: true
|
||||
components: rustfmt
|
||||
@@ -62,7 +71,7 @@ jobs:
|
||||
run: chmod -v a+x target/${{ matrix.job.target }}/release/*
|
||||
|
||||
- name: Publish Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries-linux-${{ matrix.job.name }}
|
||||
path: |
|
||||
@@ -73,17 +82,19 @@ jobs:
|
||||
|
||||
build-win:
|
||||
name: Build - windows
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "1.62"
|
||||
toolchain: "1.90"
|
||||
override: true
|
||||
default: true
|
||||
components: rustfmt
|
||||
@@ -95,16 +106,69 @@ 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
|
||||
if: false
|
||||
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
|
||||
if: false
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
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
|
||||
@@ -119,16 +183,17 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { os: "linux", name: "amd64" }
|
||||
- { os: "linux", name: "arm64v8" }
|
||||
- { os: "linux", name: "armv7" }
|
||||
- { os: "linux", name: "i386" }
|
||||
- { os: "windows", name: "x86_64" }
|
||||
|
||||
- { os: "linux", name: "amd64", suffix: "" }
|
||||
- { os: "linux", name: "arm64v8", suffix: "" }
|
||||
- { os: "linux", name: "armv7", suffix: "" }
|
||||
- { os: "linux", name: "i386", suffix: "" }
|
||||
#- { os: "linux", name: "amd64fb", suffix: "" }
|
||||
- { os: "windows", name: "x86_64", suffix: "-unsigned" }
|
||||
|
||||
steps:
|
||||
|
||||
- name: Download binaries (${{ matrix.job.os }} - ${{ matrix.job.name }})
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries-${{ matrix.job.os }}-${{ matrix.job.name }}
|
||||
path: ${{ matrix.job.name }}
|
||||
@@ -140,13 +205,13 @@ jobs:
|
||||
run: |
|
||||
sudo apt update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y zip
|
||||
zip ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}.zip ${{ matrix.job.name }}/*
|
||||
zip ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}${{ matrix.job.suffix }}.zip ${{ matrix.job.name }}/*
|
||||
|
||||
- name: Create Release (${{ matrix.job.os }} - (${{ matrix.job.name }})
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
files: ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}.zip
|
||||
files: ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}${{ matrix.job.suffix }}.zip
|
||||
|
||||
# docker build and push of single-arch images
|
||||
docker:
|
||||
@@ -167,9 +232,11 @@ jobs:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries-linux-${{ matrix.job.name }}
|
||||
path: docker/rootfs/usr/bin
|
||||
@@ -187,8 +254,16 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: rustdesk
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
@@ -205,17 +280,21 @@ jobs:
|
||||
echo "MAJOR_TAG=$M" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: "./docker"
|
||||
platforms: ${{ matrix.job.docker_platform }}
|
||||
push: true
|
||||
provenance: false
|
||||
build-args: |
|
||||
S6_ARCH=${{ matrix.job.s6_platform }}
|
||||
tags: |
|
||||
${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }}
|
||||
${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}-${{ matrix.job.name }}
|
||||
${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }}
|
||||
${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }}
|
||||
${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-${{ matrix.job.name }}
|
||||
${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# docker build and push of multiarch images
|
||||
@@ -231,8 +310,8 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Get git tag
|
||||
id: vars
|
||||
@@ -242,10 +321,18 @@ jobs:
|
||||
echo "GIT_TAG=$T" >> $GITHUB_ENV
|
||||
echo "MAJOR_TAG=$M" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: rustdesk
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# manifest for :1.2.3 tag
|
||||
# this has to run only if invoked by a new tag
|
||||
- name: Create and push manifest (:ve.rs.ion)
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
with:
|
||||
base-image: ${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}
|
||||
@@ -254,7 +341,7 @@ jobs:
|
||||
|
||||
# manifest for :1 tag (major release)
|
||||
- name: Create and push manifest (:major)
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}
|
||||
extra-images: ${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-amd64,${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-armv7,${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-i386
|
||||
@@ -262,40 +349,68 @@ jobs:
|
||||
|
||||
# manifest for :latest tag
|
||||
- name: Create and push manifest (:latest)
|
||||
uses: Noelware/docker-manifest-action@master
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
extra-images: ${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-amd64,${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-armv7,${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-i386
|
||||
push: true
|
||||
|
||||
# GHCR manifests
|
||||
# manifest for :1.2.3 tag
|
||||
- name: Create and push GHCR manifest (:ve.rs.ion)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
with:
|
||||
base-image: ${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}
|
||||
extra-images: ${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-amd64,${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-arm64v8,${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-armv7,${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-i386
|
||||
push: true
|
||||
|
||||
# manifest for :1 tag (major release)
|
||||
- name: Create and push GHCR manifest (:major)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}
|
||||
extra-images: ${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-amd64,${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-arm64v8,${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-armv7,${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-i386
|
||||
push: true
|
||||
|
||||
# manifest for :latest tag
|
||||
- name: Create and push GHCR manifest (:latest)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
extra-images: ${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-amd64,${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-arm64v8,${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-armv7,${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-i386
|
||||
push: true
|
||||
|
||||
|
||||
# docker build and push of classic images
|
||||
# docker build and push of single-arch images
|
||||
docker-classic:
|
||||
|
||||
name: Docker push classic - ${{ matrix.job.name }}
|
||||
name: Docker push - ${{ matrix.job.name }}
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { name: "amd64", docker_platform: "linux/amd64", tag: "latest" }
|
||||
- { name: "arm64v8", docker_platform: "linux/arm64", tag: "latest-arm64v8" }
|
||||
- { name: "amd64", docker_platform: "linux/amd64" }
|
||||
- { name: "arm64v8", docker_platform: "linux/arm64" }
|
||||
- { name: "armv7", docker_platform: "linux/arm/v7" }
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries-linux-${{ matrix.job.name }}
|
||||
path: docker-classic/
|
||||
|
||||
- name: Make binaries executable
|
||||
run: chmod -v a+x docker-classic/hbb*
|
||||
run: chmod -v a+x docker-classic/*
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@@ -307,8 +422,16 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: rustdesk
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
@@ -316,16 +439,114 @@ jobs:
|
||||
with:
|
||||
images: registry.hub.docker.com/${{ secrets.DOCKER_IMAGE_CLASSIC }}
|
||||
|
||||
- name: Get git tag
|
||||
id: vars
|
||||
run: |
|
||||
T=${GITHUB_REF#refs/*/}
|
||||
M=${T%%.*}
|
||||
echo "GIT_TAG=$T" >> $GITHUB_ENV
|
||||
echo "MAJOR_TAG=$M" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: "./docker-classic"
|
||||
platforms: ${{ matrix.job.docker_platform }}
|
||||
push: true
|
||||
provenance: false
|
||||
tags: |
|
||||
${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ matrix.job.tag }}
|
||||
${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }}
|
||||
${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-${{ matrix.job.name }}
|
||||
${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }}
|
||||
${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }}
|
||||
${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-${{ matrix.job.name }}
|
||||
${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# docker build and push of multiarch images
|
||||
docker-manifest-classic:
|
||||
|
||||
name: Docker manifest
|
||||
needs: docker
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Get git tag
|
||||
id: vars
|
||||
run: |
|
||||
T=${GITHUB_REF#refs/*/}
|
||||
M=${T%%.*}
|
||||
echo "GIT_TAG=$T" >> $GITHUB_ENV
|
||||
echo "MAJOR_TAG=$M" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: rustdesk
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# manifest for :1.2.3 tag
|
||||
# this has to run only if invoked by a new tag
|
||||
- name: Create and push manifest (:ve.rs.ion)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
with:
|
||||
base-image: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}
|
||||
extra-images: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-amd64,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-armv7
|
||||
push: true
|
||||
|
||||
# manifest for :1 tag (major release)
|
||||
- name: Create and push manifest (:major)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}
|
||||
extra-images: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-amd64,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-armv7
|
||||
push: true
|
||||
|
||||
# manifest for :latest tag
|
||||
- name: Create and push manifest (:latest)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}
|
||||
extra-images: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-amd64,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-armv7
|
||||
push: true
|
||||
|
||||
# GHCR manifests
|
||||
# manifest for :1.2.3 tag
|
||||
- name: Create and push GHCR manifest (:ve.rs.ion)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
with:
|
||||
base-image: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}
|
||||
extra-images: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-amd64,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-arm64v8,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-armv7
|
||||
push: true
|
||||
|
||||
# manifest for :1 tag (major release)
|
||||
- name: Create and push GHCR manifest (:major)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}
|
||||
extra-images: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-amd64,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-arm64v8,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-armv7
|
||||
push: true
|
||||
|
||||
# manifest for :latest tag
|
||||
- name: Create and push GHCR manifest (:latest)
|
||||
uses: Noelware/docker-manifest-action@0.4.3
|
||||
with:
|
||||
base-image: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}
|
||||
extra-images: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-amd64,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-arm64v8,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-armv7
|
||||
push: true
|
||||
|
||||
|
||||
deb-package:
|
||||
|
||||
@@ -345,7 +566,9 @@ jobs:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
@@ -356,7 +579,7 @@ jobs:
|
||||
mkdir -p debian-build/${{ matrix.job.name }}/bin
|
||||
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries-linux-${{ matrix.job.name }}
|
||||
path: debian-build/${{ matrix.job.name }}/bin
|
||||
|
||||
4
.gitignore
vendored
@@ -6,3 +6,7 @@ debian/.debhelper
|
||||
debian/debhelper-build-stamp
|
||||
.DS_Store
|
||||
.vscode
|
||||
src/version.rs
|
||||
db_v2.sqlite3
|
||||
test.*
|
||||
.idea
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "libs/hbb_common"]
|
||||
path = libs/hbb_common
|
||||
url = https://github.com/rustdesk/hbb_common
|
||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rust.checkWith": "clippy",
|
||||
"rust.formatOnSave": true,
|
||||
"rust.checkOnSave": true,
|
||||
"rust.useNewErrorFormat": true
|
||||
}
|
||||
1790
Cargo.lock
generated
29
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "hbbs"
|
||||
version = "1.1.6"
|
||||
authors = ["open-trade <info@rustdesk.com>"]
|
||||
version = "1.1.15"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
default-run = "hbbs"
|
||||
@@ -26,7 +26,7 @@ clap = "2"
|
||||
rust-ini = "0.18"
|
||||
minreq = { version = "2.4", features = ["punycode"] }
|
||||
machine-uid = "0.2"
|
||||
mac_address = "1.1"
|
||||
mac_address = "1.1.5"
|
||||
whoami = "1.2"
|
||||
base64 = "0.13"
|
||||
axum = { version = "0.5", features = ["headers"] }
|
||||
@@ -46,14 +46,33 @@ tungstenite = "0.17"
|
||||
regex = "1.4"
|
||||
tower-http = { version = "0.3", features = ["fs", "trace", "cors"] }
|
||||
http = "0.2"
|
||||
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
|
||||
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset", "dont_minimize_extra_stacks"] }
|
||||
ipnetwork = "0.20"
|
||||
local-ip-address = "0.4"
|
||||
local-ip-address = "0.5.1"
|
||||
dns-lookup = "1.0.8"
|
||||
ping = "0.4.0"
|
||||
flate2 = "1.0"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||
|
||||
[build-dependencies]
|
||||
hbb_common = { path = "libs/hbb_common" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/hbb_common"]
|
||||
exclude = ["ui"]
|
||||
|
||||
#https://github.com/johnthagen/min-sized-rust
|
||||
#https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip # Default is 3, better performance
|
||||
#rpath = true # Not needed
|
||||
|
||||
297
README.md
@@ -8,6 +8,8 @@
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**How to migrate OSS to Pro**](https://rustdesk.com/docs/en/self-host/rustdesk-server-pro/installscript/#convert-from-open-source)
|
||||
|
||||
Self-host your own RustDesk server, it is free and open source.
|
||||
|
||||
## How to build manually
|
||||
@@ -22,297 +24,12 @@ Three executables will be generated in target/release.
|
||||
- hbbr - RustDesk relay server
|
||||
- rustdesk-utils - RustDesk CLI utilities
|
||||
|
||||
You can find updated binaries on the [releases](https://github.com/rustdesk/rustdesk-server/releases) page.
|
||||
You can find updated binaries on the [Releases](https://github.com/rustdesk/rustdesk-server/releases) page.
|
||||
|
||||
If you wanna develop your own server, [rustdesk-server-demo](https://github.com/rustdesk/rustdesk-server-demo) might be a better and simpler start for you than this repo.
|
||||
If you want extra features, [RustDesk Server Pro](https://rustdesk.com/pricing.html) might suit you better.
|
||||
|
||||
## Docker images
|
||||
If you want to develop your own server, [rustdesk-server-demo](https://github.com/rustdesk/rustdesk-server-demo) might be a better and simpler start for you than this repo.
|
||||
|
||||
Docker images are automatically generated and published on every github release. We have 2 kind of images.
|
||||
## Installation
|
||||
|
||||
### Classic image
|
||||
|
||||
These images are build against `ubuntu-20.04` with the only addition of the main binaries (`hbbr` and `hbbs`). They're available on [Docker hub](https://hub.docker.com/r/rustdesk/rustdesk-server/) with these tags:
|
||||
|
||||
| architecture | image:tag |
|
||||
| --- | --- |
|
||||
| amd64 | `rustdesk/rustdesk-server:latest` |
|
||||
| arm64v8 | `rustdesk/rustdesk-server:latest-arm64v8` |
|
||||
|
||||
You can start these images directly with `docker run` with these commands:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
or without --net=host, but P2P direct connection can not work.
|
||||
|
||||
For systems using SELinux, replacing `/root` by `/root:z` is required for the containers to run correctly. Alternatively, SELinux container separation can be disabled completely adding the option `--security-opt label=disable`.
|
||||
|
||||
```bash
|
||||
docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbs -r <relay-server-ip[:port]>
|
||||
docker run --name hbbr -p 21117:21117 -p 21119:21119 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbr
|
||||
```
|
||||
|
||||
The `relay-server-ip` parameter is the IP address (or dns name) of the server running these containers. The **optional** `port` parameter has to be used if you use a port different than **21117** for `hbbr`.
|
||||
|
||||
You can also use docker-compose, using this configuration as a template:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Edit line 16 to point to your relay server (the one listening on port 21117). You can also edit the volume lines (L18 and L33) if you need.
|
||||
|
||||
(docker-compose credit goes to @lukebarone and @QuiGonLeong)
|
||||
|
||||
## S6-overlay based images
|
||||
|
||||
These images are build against `busybox:stable` with the addition of the binaries (both hbbr and hbbs) and [S6-overlay](https://github.com/just-containers/s6-overlay). They're available on [Docker hub](https://hub.docker.com/r/rustdesk/rustdesk-server-s6/) with these tags:
|
||||
|
||||
| architecture | version | 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` |
|
||||
|
||||
You're strongly encuraged to use the `multiarch` image either with the `major version` or `latest` tag.
|
||||
|
||||
The S6-overlay acts as a supervisor and keeps both process running, so with this image there's no need to have two separate running containers.
|
||||
|
||||
You can start these images directly with `docker run` with this command:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
or without --net=host, but P2P direct connection can not work.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Or you can use a docker-compose file:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
We use these environment variables:
|
||||
|
||||
| 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
|
||||
|
||||
You can obviously keep the key pair in a docker volume, but the best practices tells you to not write the keys on the filesystem; so we provide a couple of options.
|
||||
|
||||
On container startup, the presence of the keypair is checked (`/data/id_ed25519.pub` and `/data/id_ed25519`) and if one of these keys doesn't exist, it's recreated from ENV variables or docker secrets.
|
||||
Then the validity of the keypair is checked: if public and private keys doesn't match, the container will stop.
|
||||
If you provide no keys, `hbbs` will generate one for you, and it'll place it in the default location.
|
||||
|
||||
#### Use ENV to store the key pair
|
||||
|
||||
You can use docker environment variables to store the keys. Just follow this examples:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
#### Use Docker secrets to store the key pair
|
||||
|
||||
You can alternatively use docker secrets to store the keys.
|
||||
This is useful if you're using **docker-compose** or **docker swarm**.
|
||||
Just follow this examples:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## How to create a keypair
|
||||
|
||||
A keypair is needed for encryption; you can provide it, as explained before, but you need a way to create one.
|
||||
|
||||
You can use this command to generate a keypair:
|
||||
|
||||
```bash
|
||||
/usr/bin/rustdesk-utils genkeypair
|
||||
```
|
||||
|
||||
If you don't have (or don't want) the `rustdesk-utils` package installed on your system, you can invoke the same command with docker:
|
||||
|
||||
```bash
|
||||
docker run --rm --entrypoint /usr/bin/rustdesk-utils rustdesk/rustdesk-server-s6:latest genkeypair
|
||||
```
|
||||
|
||||
The output will be something like this:
|
||||
|
||||
```text
|
||||
Public Key: 8BLLhtzUBU/XKAH4mep3p+IX4DSApe7qbAwNH9nv4yA=
|
||||
Secret Key: egAVd44u33ZEUIDTtksGcHeVeAwywarEdHmf99KM5ajwEsuG3NQFT9coAfiZ6nen4hfgNICl7upsDA0f2e/jIA==
|
||||
```
|
||||
|
||||
## .deb packages
|
||||
|
||||
Separate .deb packages are available for each binary, you can find them in the [releases](https://github.com/rustdesk/rustdesk-server/releases).
|
||||
These packages are meant for the following distributions:
|
||||
|
||||
- Ubuntu 22.04 LTS
|
||||
- Ubuntu 20.04 LTS
|
||||
- Ubuntu 18.04 LTS
|
||||
- Debian 11 bullseye
|
||||
- Debian 10 buster
|
||||
Please follow this [doc](https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/)
|
||||
|
||||
BIN
db_v2.sqlite3
62
debian/README.source
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
#
|
||||
# Juli Augustus 2025 was building the downloadable Debian packages done
|
||||
# by github.com CI, meaning you being depended on github.com
|
||||
# for getting a .deb of the libre source code (four GNU freedoms) you have.
|
||||
#
|
||||
# And in those days there was no official Debian package.
|
||||
#
|
||||
# What follows are instructions that make it possible to do
|
||||
# a succesfull
|
||||
#
|
||||
# debuild -uc -us
|
||||
#
|
||||
# in the directory target/rustdesk-server
|
||||
#
|
||||
# The instructions are written in bash syntax,
|
||||
#
|
||||
# bash debian/README.source
|
||||
#
|
||||
# should help you immens
|
||||
#
|
||||
|
||||
# Make sure the submodules are present
|
||||
git submodule update --init --recursive
|
||||
|
||||
rm -rf target/rustdesk-server # avoid clutter from previous iteration
|
||||
mkdir -p target/rustdesk-server # FYI: target/ is ignored by git
|
||||
|
||||
tar cf - \
|
||||
--exclude .git \
|
||||
Cargo.toml Cargo.lock \
|
||||
LICENSE README.md \
|
||||
db_v2.sqlite3 \
|
||||
build.rs \
|
||||
debian \
|
||||
libs rcd src \
|
||||
systemd ui \
|
||||
| ( cd target/rustdesk-server && tar x )
|
||||
|
||||
mv target/rustdesk-server/debian/Makefile target/rustdesk-server/
|
||||
|
||||
tar cjf target/rustdesk-server-orig.tar.xz \
|
||||
--strip-components=1 \
|
||||
target/rustdesk-server
|
||||
|
||||
# an .orig.tar.xz tarball exists,
|
||||
# work on the debian directory
|
||||
|
||||
eval $( dpkg-architecture )
|
||||
sed -e "s/{{ ARCH }}/${DEB_TARGET_ARCH}/" \
|
||||
debian/control.tpl > target/rustdesk-server/debian/control
|
||||
|
||||
|
||||
cd target/rustdesk-server
|
||||
dpkg-checkbuilddeps || echo sudo apt install dpkg-dev
|
||||
debuild -uc -us
|
||||
|
||||
#
|
||||
# For what it is worth:
|
||||
# Early September 2025 there were
|
||||
# several WARNINGS and ERRORS from Lintian
|
||||
#
|
||||
# l l
|
||||
73
debian/changelog
vendored
@@ -1,3 +1,76 @@
|
||||
rustdesk-server (1.1.15) UNRELEASED; urgency=medium
|
||||
|
||||
* Fix: 127.0.0.1 is not loopback (#515)
|
||||
* Higher default bandwidth
|
||||
* Define the HOME env to allow running rootless (#612)
|
||||
* Add option to log (failed) authentication attempts to enable the usage of tools like fail2ban and crowdsec (#435)
|
||||
* Connection log query
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Sat, 10 Jan 2026 16:04:19 +0800
|
||||
|
||||
rustdesk-server (1.1.14) UNRELEASED; urgency=medium
|
||||
|
||||
* Fix windows crash
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Sat, 25 Jan 2025 20:47:19 +0800
|
||||
|
||||
rustdesk-server (1.1.13) UNRELEASED; urgency=medium
|
||||
|
||||
* Version check and refactor hbb_common to share with rustdesk client
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Tue, 21 Jan 2025 01:33:42 +0800
|
||||
|
||||
rustdesk-server (1.1.12) UNRELEASED; urgency=medium
|
||||
|
||||
* WS real ip
|
||||
* Bump s6-overlay to v3.2.0.0 and fix env warnings
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Mon, 7 Oct 2024 16:21:36 +0800
|
||||
|
||||
rustdesk-server (1.1.11-1) UNRELEASED; urgency=medium
|
||||
|
||||
* set reuse port to make restart friendly
|
||||
* revert hbbr `-k` to not ruin back-compatibility
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Fri, 24 May 2024 18:37:11 +0800
|
||||
|
||||
rustdesk-server (1.1.11) UNRELEASED; urgency=medium
|
||||
|
||||
* change -k to default '-', so you need not to set -k any more
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Fri, 24 May 2024 18:09:12 +0800
|
||||
|
||||
rustdesk-server (1.1.10-3) UNRELEASED; urgency=medium
|
||||
|
||||
* fix on -2
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Wed, 31 Jan 2024 11:30:42 +0800
|
||||
|
||||
rustdesk-server (1.1.10-2) UNRELEASED; urgency=medium
|
||||
|
||||
* fix hangup signal exit when run with nohup
|
||||
* some minors
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Tue, 30 Jan 2024 19:23:36 +0800
|
||||
|
||||
rustdesk-server (1.1.9) UNRELEASED; urgency=medium
|
||||
|
||||
* remove unsafe
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Tue, 5 Dec 2023 17:22:40 +0800
|
||||
|
||||
rustdesk-server (1.1.8) UNRELEASED; urgency=medium
|
||||
|
||||
* fix test_hbbs and mask in lan
|
||||
|
||||
-- rustdesk <info@rustdesk.com> Thu, 6 Jul 2023 00:50:11 +0800
|
||||
|
||||
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
|
||||
|
||||
4
debian/rustdesk-server-hbbr.postinst
vendored
@@ -3,6 +3,10 @@ set -e
|
||||
|
||||
SERVICE=rustdesk-hbbr.service
|
||||
|
||||
if [ "$1" = "configure" ]; then
|
||||
mkdir -p /var/log/rustdesk-server
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
configure|abort-upgrade|abort-deconfigure|abort-remove)
|
||||
mkdir -p /var/lib/rustdesk-server/
|
||||
|
||||
2
debian/rustdesk-server-hbbr.postrm
vendored
@@ -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/log/rustdesk-server/rustdesk-hbbr.*
|
||||
deb-systemd-helper purge "${SERVICE}" >/dev/null || true
|
||||
deb-systemd-helper unmask "${SERVICE}" >/dev/null || true
|
||||
fi
|
||||
|
||||
4
debian/rustdesk-server-hbbs.postinst
vendored
@@ -3,6 +3,10 @@ set -e
|
||||
|
||||
SERVICE=rustdesk-hbbs.service
|
||||
|
||||
if [ "$1" = "configure" ]; then
|
||||
mkdir -p /var/log/rustdesk-server
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
configure|abort-upgrade|abort-deconfigure|abort-remove)
|
||||
mkdir -p /var/lib/rustdesk-server/
|
||||
|
||||
2
debian/rustdesk-server-hbbs.postrm
vendored
@@ -6,7 +6,7 @@ SERVICE=rustdesk-hbbs.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-server/rustdesk-hbbs.*
|
||||
deb-systemd-helper purge "${SERVICE}" >/dev/null || true
|
||||
deb-systemd-helper unmask "${SERVICE}" >/dev/null || true
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM ubuntu:20.04
|
||||
COPY hbbs /usr/bin/hbbs
|
||||
COPY hbbr /usr/bin/hbbr
|
||||
WORKDIR /root
|
||||
FROM scratch
|
||||
COPY hbbs /usr/bin/hbbs
|
||||
COPY hbbr /usr/bin/hbbr
|
||||
WORKDIR /root
|
||||
ENV HOME=/root
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM busybox:stable
|
||||
|
||||
ARG S6_OVERLAY_VERSION=3.1.1.2
|
||||
ARG S6_OVERLAY_VERSION=3.2.0.0
|
||||
ARG S6_ARCH=x86_64
|
||||
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
|
||||
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz /tmp
|
||||
@@ -12,8 +12,8 @@ RUN \
|
||||
|
||||
COPY rootfs /
|
||||
|
||||
ENV RELAY relay.example.com
|
||||
ENV ENCRYPTED_ONLY 0
|
||||
ENV RELAY=relay.example.com
|
||||
ENV ENCRYPTED_ONLY=0
|
||||
|
||||
EXPOSE 21115 21116 21116/udp 21117 21118 21119
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
87
kubernetes/example.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rustdesk-server
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: rustdesk
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: rustdesk
|
||||
spec:
|
||||
containers:
|
||||
# id Server (HBBS)
|
||||
- name: hbbs
|
||||
image: rustdesk/rustdesk-server:latest
|
||||
env:
|
||||
- name: RELAY_HOST
|
||||
value: "rustdesk.yourdomain.tld"
|
||||
command: ["hbbs", "-r", "$(RELAY_HOST):21117", "-k", "_"]
|
||||
volumeMounts:
|
||||
- mountPath: /root
|
||||
name: rustdesk-data
|
||||
ports:
|
||||
- containerPort: 21115
|
||||
- containerPort: 21116
|
||||
protocol: TCP
|
||||
- containerPort: 21116
|
||||
protocol: UDP
|
||||
- containerPort: 21118
|
||||
|
||||
# Relay Server (HBBR)
|
||||
- name: hbbr
|
||||
image: rustdesk/rustdesk-server:latest
|
||||
command: ["hbbr", "-k", "_"]
|
||||
volumeMounts:
|
||||
- mountPath: /root
|
||||
name: rustdesk-data
|
||||
ports:
|
||||
- containerPort: 21117
|
||||
- containerPort: 21119
|
||||
|
||||
volumes:
|
||||
- name: rustdesk-data
|
||||
persistentVolumeClaim:
|
||||
claimName: rustdesk-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: rustdesk-service
|
||||
spec:
|
||||
type: LoadBalancer # Or NodePort
|
||||
selector:
|
||||
app: rustdesk
|
||||
ports:
|
||||
- name: hbbs-tcp
|
||||
port: 21115
|
||||
targetPort: 21115
|
||||
protocol: TCP
|
||||
- name: hbbs-udp
|
||||
port: 21116
|
||||
targetPort: 21116
|
||||
protocol: UDP
|
||||
- name: hbbs-tcp-main
|
||||
port: 21116
|
||||
targetPort: 21116
|
||||
protocol: TCP
|
||||
- name: hbbr-tcp
|
||||
port: 21117
|
||||
targetPort: 21117
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: rustdesk-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce # RWM would be available only with pro version
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
1
libs/hbb_common
Submodule
4
libs/hbb_common/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
src/protos/
|
||||
@@ -1,48 +0,0 @@
|
||||
[package]
|
||||
name = "hbb_common"
|
||||
version = "0.1.0"
|
||||
authors = ["open-trade <info@opentradesolutions.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||
zstd = "0.9"
|
||||
quinn = {version = "0.8", optional = true }
|
||||
anyhow = "1.0"
|
||||
futures-util = "0.3"
|
||||
directories-next = "2.0"
|
||||
rand = "0.8"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
lazy_static = "1.4"
|
||||
confy = { git = "https://github.com/open-trade/confy" }
|
||||
dirs-next = "2.0"
|
||||
filetime = "0.2"
|
||||
sodiumoxide = "0.2"
|
||||
regex = "1.4"
|
||||
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
|
||||
[features]
|
||||
quic = []
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen = "3.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
|
||||
[dev-dependencies]
|
||||
toml = "0.5"
|
||||
serde_json = "1.0"
|
||||
@@ -1,14 +0,0 @@
|
||||
fn main() {
|
||||
std::fs::create_dir_all("src/protos").unwrap();
|
||||
protobuf_codegen::Codegen::new()
|
||||
.pure()
|
||||
.out_dir("src/protos")
|
||||
.inputs(&["protos/rendezvous.proto", "protos/message.proto"])
|
||||
.include("protos")
|
||||
.customize(
|
||||
protobuf_codegen::Customize::default()
|
||||
.tokio_bytes(true)
|
||||
)
|
||||
.run()
|
||||
.expect("Codegen failed.");
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
syntax = "proto3";
|
||||
package hbb;
|
||||
|
||||
message VP9 {
|
||||
bytes data = 1;
|
||||
bool key = 2;
|
||||
int64 pts = 3;
|
||||
}
|
||||
|
||||
message VP9s { repeated VP9 frames = 1; }
|
||||
|
||||
message RGB { bool compress = 1; }
|
||||
|
||||
// planes data send directly in binary for better use arraybuffer on web
|
||||
message YUV {
|
||||
bool compress = 1;
|
||||
int32 stride = 2;
|
||||
}
|
||||
|
||||
message VideoFrame {
|
||||
oneof union {
|
||||
VP9s vp9s = 6;
|
||||
RGB rgb = 7;
|
||||
YUV yuv = 8;
|
||||
}
|
||||
int64 timestamp = 9;
|
||||
}
|
||||
|
||||
message IdPk {
|
||||
string id = 1;
|
||||
bytes pk = 2;
|
||||
}
|
||||
|
||||
message DisplayInfo {
|
||||
sint32 x = 1;
|
||||
sint32 y = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
string name = 5;
|
||||
bool online = 6;
|
||||
}
|
||||
|
||||
message PortForward {
|
||||
string host = 1;
|
||||
int32 port = 2;
|
||||
}
|
||||
|
||||
message FileTransfer {
|
||||
string dir = 1;
|
||||
bool show_hidden = 2;
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
bytes password = 2;
|
||||
string my_id = 4;
|
||||
string my_name = 5;
|
||||
OptionMessage option = 6;
|
||||
oneof union {
|
||||
FileTransfer file_transfer = 7;
|
||||
PortForward port_forward = 8;
|
||||
}
|
||||
bool video_ack_required = 9;
|
||||
}
|
||||
|
||||
message ChatMessage { string text = 1; }
|
||||
|
||||
message PeerInfo {
|
||||
string username = 1;
|
||||
string hostname = 2;
|
||||
string platform = 3;
|
||||
repeated DisplayInfo displays = 4;
|
||||
int32 current_display = 5;
|
||||
bool sas_enabled = 6;
|
||||
string version = 7;
|
||||
int32 conn_id = 8;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
oneof union {
|
||||
string error = 1;
|
||||
PeerInfo peer_info = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message MouseEvent {
|
||||
int32 mask = 1;
|
||||
sint32 x = 2;
|
||||
sint32 y = 3;
|
||||
repeated ControlKey modifiers = 4;
|
||||
}
|
||||
|
||||
enum ControlKey {
|
||||
Unknown = 0;
|
||||
Alt = 1;
|
||||
Backspace = 2;
|
||||
CapsLock = 3;
|
||||
Control = 4;
|
||||
Delete = 5;
|
||||
DownArrow = 6;
|
||||
End = 7;
|
||||
Escape = 8;
|
||||
F1 = 9;
|
||||
F10 = 10;
|
||||
F11 = 11;
|
||||
F12 = 12;
|
||||
F2 = 13;
|
||||
F3 = 14;
|
||||
F4 = 15;
|
||||
F5 = 16;
|
||||
F6 = 17;
|
||||
F7 = 18;
|
||||
F8 = 19;
|
||||
F9 = 20;
|
||||
Home = 21;
|
||||
LeftArrow = 22;
|
||||
/// meta key (also known as "windows"; "super"; and "command")
|
||||
Meta = 23;
|
||||
/// option key on macOS (alt key on Linux and Windows)
|
||||
Option = 24; // deprecated, use Alt instead
|
||||
PageDown = 25;
|
||||
PageUp = 26;
|
||||
Return = 27;
|
||||
RightArrow = 28;
|
||||
Shift = 29;
|
||||
Space = 30;
|
||||
Tab = 31;
|
||||
UpArrow = 32;
|
||||
Numpad0 = 33;
|
||||
Numpad1 = 34;
|
||||
Numpad2 = 35;
|
||||
Numpad3 = 36;
|
||||
Numpad4 = 37;
|
||||
Numpad5 = 38;
|
||||
Numpad6 = 39;
|
||||
Numpad7 = 40;
|
||||
Numpad8 = 41;
|
||||
Numpad9 = 42;
|
||||
Cancel = 43;
|
||||
Clear = 44;
|
||||
Menu = 45; // deprecated, use Alt instead
|
||||
Pause = 46;
|
||||
Kana = 47;
|
||||
Hangul = 48;
|
||||
Junja = 49;
|
||||
Final = 50;
|
||||
Hanja = 51;
|
||||
Kanji = 52;
|
||||
Convert = 53;
|
||||
Select = 54;
|
||||
Print = 55;
|
||||
Execute = 56;
|
||||
Snapshot = 57;
|
||||
Insert = 58;
|
||||
Help = 59;
|
||||
Sleep = 60;
|
||||
Separator = 61;
|
||||
Scroll = 62;
|
||||
NumLock = 63;
|
||||
RWin = 64;
|
||||
Apps = 65;
|
||||
Multiply = 66;
|
||||
Add = 67;
|
||||
Subtract = 68;
|
||||
Decimal = 69;
|
||||
Divide = 70;
|
||||
Equals = 71;
|
||||
NumpadEnter = 72;
|
||||
RShift = 73;
|
||||
RControl = 74;
|
||||
RAlt = 75;
|
||||
CtrlAltDel = 100;
|
||||
LockScreen = 101;
|
||||
}
|
||||
|
||||
message KeyEvent {
|
||||
bool down = 1;
|
||||
bool press = 2;
|
||||
oneof union {
|
||||
ControlKey control_key = 3;
|
||||
uint32 chr = 4;
|
||||
uint32 unicode = 5;
|
||||
string seq = 6;
|
||||
}
|
||||
repeated ControlKey modifiers = 8;
|
||||
}
|
||||
|
||||
message CursorData {
|
||||
uint64 id = 1;
|
||||
sint32 hotx = 2;
|
||||
sint32 hoty = 3;
|
||||
int32 width = 4;
|
||||
int32 height = 5;
|
||||
bytes colors = 6;
|
||||
}
|
||||
|
||||
message CursorPosition {
|
||||
sint32 x = 1;
|
||||
sint32 y = 2;
|
||||
}
|
||||
|
||||
message Hash {
|
||||
string salt = 1;
|
||||
string challenge = 2;
|
||||
}
|
||||
|
||||
message Clipboard {
|
||||
bool compress = 1;
|
||||
bytes content = 2;
|
||||
}
|
||||
|
||||
enum FileType {
|
||||
Dir = 0;
|
||||
DirLink = 2;
|
||||
DirDrive = 3;
|
||||
File = 4;
|
||||
FileLink = 5;
|
||||
}
|
||||
|
||||
message FileEntry {
|
||||
FileType entry_type = 1;
|
||||
string name = 2;
|
||||
bool is_hidden = 3;
|
||||
uint64 size = 4;
|
||||
uint64 modified_time = 5;
|
||||
}
|
||||
|
||||
message FileDirectory {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
repeated FileEntry entries = 3;
|
||||
}
|
||||
|
||||
message ReadDir {
|
||||
string path = 1;
|
||||
bool include_hidden = 2;
|
||||
}
|
||||
|
||||
message ReadAllFiles {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
bool include_hidden = 3;
|
||||
}
|
||||
|
||||
message FileAction {
|
||||
oneof union {
|
||||
ReadDir read_dir = 1;
|
||||
FileTransferSendRequest send = 2;
|
||||
FileTransferReceiveRequest receive = 3;
|
||||
FileDirCreate create = 4;
|
||||
FileRemoveDir remove_dir = 5;
|
||||
FileRemoveFile remove_file = 6;
|
||||
ReadAllFiles all_files = 7;
|
||||
FileTransferCancel cancel = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message FileTransferCancel { int32 id = 1; }
|
||||
|
||||
message FileResponse {
|
||||
oneof union {
|
||||
FileDirectory dir = 1;
|
||||
FileTransferBlock block = 2;
|
||||
FileTransferError error = 3;
|
||||
FileTransferDone done = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message FileTransferBlock {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
bytes data = 3;
|
||||
bool compressed = 4;
|
||||
}
|
||||
|
||||
message FileTransferError {
|
||||
int32 id = 1;
|
||||
string error = 2;
|
||||
sint32 file_num = 3;
|
||||
}
|
||||
|
||||
message FileTransferSendRequest {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
bool include_hidden = 3;
|
||||
}
|
||||
|
||||
message FileTransferDone {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
}
|
||||
|
||||
message FileTransferReceiveRequest {
|
||||
int32 id = 1;
|
||||
string path = 2; // path written to
|
||||
repeated FileEntry files = 3;
|
||||
}
|
||||
|
||||
message FileRemoveDir {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
bool recursive = 3;
|
||||
}
|
||||
|
||||
message FileRemoveFile {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
sint32 file_num = 3;
|
||||
}
|
||||
|
||||
message FileDirCreate {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
// 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;
|
||||
int32 n_position_low = 5;
|
||||
int32 n_position_high = 6;
|
||||
int32 cb_requested = 7;
|
||||
bool have_clip_data_id = 8;
|
||||
int32 clip_data_id = 9;
|
||||
}
|
||||
|
||||
message CliprdrFileContentsResponse {
|
||||
int32 conn_id = 1;
|
||||
int32 msg_flags = 3;
|
||||
int32 stream_id = 4;
|
||||
bytes requested_data = 5;
|
||||
}
|
||||
|
||||
message Cliprdr {
|
||||
oneof union {
|
||||
CliprdrMonitorReady ready = 1;
|
||||
CliprdrServerFormatList format_list = 2;
|
||||
CliprdrServerFormatListResponse format_list_response = 3;
|
||||
CliprdrServerFormatDataRequest format_data_request = 4;
|
||||
CliprdrServerFormatDataResponse format_data_response = 5;
|
||||
CliprdrFileContentsRequest file_contents_request = 6;
|
||||
CliprdrFileContentsResponse file_contents_response = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message SwitchDisplay {
|
||||
int32 display = 1;
|
||||
sint32 x = 2;
|
||||
sint32 y = 3;
|
||||
int32 width = 4;
|
||||
int32 height = 5;
|
||||
}
|
||||
|
||||
message PermissionInfo {
|
||||
enum Permission {
|
||||
Keyboard = 0;
|
||||
Clipboard = 2;
|
||||
Audio = 3;
|
||||
File = 4;
|
||||
}
|
||||
|
||||
Permission permission = 1;
|
||||
bool enabled = 2;
|
||||
}
|
||||
|
||||
enum ImageQuality {
|
||||
NotSet = 0;
|
||||
Low = 2;
|
||||
Balanced = 3;
|
||||
Best = 4;
|
||||
}
|
||||
|
||||
message OptionMessage {
|
||||
enum BoolOption {
|
||||
NotSet = 0;
|
||||
No = 1;
|
||||
Yes = 2;
|
||||
}
|
||||
ImageQuality image_quality = 1;
|
||||
BoolOption lock_after_session_end = 2;
|
||||
BoolOption show_remote_cursor = 3;
|
||||
BoolOption privacy_mode = 4;
|
||||
BoolOption block_input = 5;
|
||||
int32 custom_image_quality = 6;
|
||||
BoolOption disable_audio = 7;
|
||||
BoolOption disable_clipboard = 8;
|
||||
BoolOption enable_file_transfer = 9;
|
||||
}
|
||||
|
||||
message OptionResponse {
|
||||
OptionMessage opt = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message TestDelay {
|
||||
int64 time = 1;
|
||||
bool from_client = 2;
|
||||
}
|
||||
|
||||
message PublicKey {
|
||||
bytes asymmetric_value = 1;
|
||||
bytes symmetric_value = 2;
|
||||
}
|
||||
|
||||
message SignedId { bytes id = 1; }
|
||||
|
||||
message AudioFormat {
|
||||
uint32 sample_rate = 1;
|
||||
uint32 channels = 2;
|
||||
}
|
||||
|
||||
message AudioFrame {
|
||||
bytes data = 1;
|
||||
int64 timestamp = 2;
|
||||
}
|
||||
|
||||
message Misc {
|
||||
oneof union {
|
||||
ChatMessage chat_message = 4;
|
||||
SwitchDisplay switch_display = 5;
|
||||
PermissionInfo permission_info = 6;
|
||||
OptionMessage option = 7;
|
||||
AudioFormat audio_format = 8;
|
||||
string close_reason = 9;
|
||||
bool refresh_video = 10;
|
||||
OptionResponse option_response = 11;
|
||||
bool video_received = 12;
|
||||
}
|
||||
}
|
||||
|
||||
message Message {
|
||||
oneof union {
|
||||
SignedId signed_id = 3;
|
||||
PublicKey public_key = 4;
|
||||
TestDelay test_delay = 5;
|
||||
VideoFrame video_frame = 6;
|
||||
LoginRequest login_request = 7;
|
||||
LoginResponse login_response = 8;
|
||||
Hash hash = 9;
|
||||
MouseEvent mouse_event = 10;
|
||||
AudioFrame audio_frame = 11;
|
||||
CursorData cursor_data = 12;
|
||||
CursorPosition cursor_position = 13;
|
||||
uint64 cursor_id = 14;
|
||||
KeyEvent key_event = 15;
|
||||
Clipboard clipboard = 16;
|
||||
FileAction file_action = 17;
|
||||
FileResponse file_response = 18;
|
||||
Misc misc = 19;
|
||||
Cliprdr cliprdr = 20;
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
syntax = "proto3";
|
||||
package hbb;
|
||||
|
||||
message RegisterPeer {
|
||||
string id = 1;
|
||||
int32 serial = 2;
|
||||
}
|
||||
|
||||
enum ConnType {
|
||||
DEFAULT_CONN = 0;
|
||||
FILE_TRANSFER = 1;
|
||||
PORT_FORWARD = 2;
|
||||
RDP = 3;
|
||||
}
|
||||
|
||||
message RegisterPeerResponse { bool request_pk = 2; }
|
||||
|
||||
message PunchHoleRequest {
|
||||
string id = 1;
|
||||
NatType nat_type = 2;
|
||||
string licence_key = 3;
|
||||
ConnType conn_type = 4;
|
||||
string token = 5;
|
||||
}
|
||||
|
||||
message PunchHole {
|
||||
bytes socket_addr = 1;
|
||||
string relay_server = 2;
|
||||
NatType nat_type = 3;
|
||||
}
|
||||
|
||||
message TestNatRequest {
|
||||
int32 serial = 1;
|
||||
}
|
||||
|
||||
// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative
|
||||
message TestNatResponse {
|
||||
int32 port = 1;
|
||||
ConfigUpdate cu = 2; // for mobile
|
||||
}
|
||||
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0;
|
||||
ASYMMETRIC = 1;
|
||||
SYMMETRIC = 2;
|
||||
}
|
||||
|
||||
message PunchHoleSent {
|
||||
bytes socket_addr = 1;
|
||||
string id = 2;
|
||||
string relay_server = 3;
|
||||
NatType nat_type = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message RegisterPk {
|
||||
string id = 1;
|
||||
bytes uuid = 2;
|
||||
bytes pk = 3;
|
||||
string old_id = 4;
|
||||
}
|
||||
|
||||
message RegisterPkResponse {
|
||||
enum Result {
|
||||
OK = 0;
|
||||
UUID_MISMATCH = 2;
|
||||
ID_EXISTS = 3;
|
||||
TOO_FREQUENT = 4;
|
||||
INVALID_ID_FORMAT = 5;
|
||||
NOT_SUPPORT = 6;
|
||||
SERVER_ERROR = 7;
|
||||
}
|
||||
Result result = 1;
|
||||
}
|
||||
|
||||
message PunchHoleResponse {
|
||||
bytes socket_addr = 1;
|
||||
bytes pk = 2;
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 0;
|
||||
OFFLINE = 2;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
oneof union {
|
||||
NatType nat_type = 5;
|
||||
bool is_local = 6;
|
||||
}
|
||||
string other_failure = 7;
|
||||
}
|
||||
|
||||
message ConfigUpdate {
|
||||
int32 serial = 1;
|
||||
repeated string rendezvous_servers = 2;
|
||||
}
|
||||
|
||||
message RequestRelay {
|
||||
string id = 1;
|
||||
string uuid = 2;
|
||||
bytes socket_addr = 3;
|
||||
string relay_server = 4;
|
||||
bool secure = 5;
|
||||
string licence_key = 6;
|
||||
ConnType conn_type = 7;
|
||||
string token = 8;
|
||||
}
|
||||
|
||||
message RelayResponse {
|
||||
bytes socket_addr = 1;
|
||||
string uuid = 2;
|
||||
string relay_server = 3;
|
||||
oneof union {
|
||||
string id = 4;
|
||||
bytes pk = 5;
|
||||
}
|
||||
string refuse_reason = 6;
|
||||
string version = 7;
|
||||
}
|
||||
|
||||
message SoftwareUpdate { string url = 1; }
|
||||
|
||||
// if in same intranet, punch hole won't work both for udp and tcp,
|
||||
// even some router has below connection error if we connect itself,
|
||||
// { kind: Other, error: "could not resolve to any address" },
|
||||
// so we request local address to connect.
|
||||
message FetchLocalAddr {
|
||||
bytes socket_addr = 1;
|
||||
string relay_server = 2;
|
||||
}
|
||||
|
||||
message LocalAddr {
|
||||
bytes socket_addr = 1;
|
||||
bytes local_addr = 2;
|
||||
string relay_server = 3;
|
||||
string id = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message PeerDiscovery {
|
||||
string cmd = 1;
|
||||
string mac = 2;
|
||||
string id = 3;
|
||||
string username = 4;
|
||||
string hostname = 5;
|
||||
string platform = 6;
|
||||
string misc = 7;
|
||||
}
|
||||
|
||||
message OnlineRequest {
|
||||
string id = 1;
|
||||
repeated string peers = 2;
|
||||
}
|
||||
|
||||
message OnlineResponse {
|
||||
bytes states = 1;
|
||||
}
|
||||
|
||||
message RendezvousMessage {
|
||||
oneof union {
|
||||
RegisterPeer register_peer = 6;
|
||||
RegisterPeerResponse register_peer_response = 7;
|
||||
PunchHoleRequest punch_hole_request = 8;
|
||||
PunchHole punch_hole = 9;
|
||||
PunchHoleSent punch_hole_sent = 10;
|
||||
PunchHoleResponse punch_hole_response = 11;
|
||||
FetchLocalAddr fetch_local_addr = 12;
|
||||
LocalAddr local_addr = 13;
|
||||
ConfigUpdate configure_update = 14;
|
||||
RegisterPk register_pk = 15;
|
||||
RegisterPkResponse register_pk_response = 16;
|
||||
SoftwareUpdate software_update = 17;
|
||||
RequestRelay request_relay = 18;
|
||||
RelayResponse relay_response = 19;
|
||||
TestNatRequest test_nat_request = 20;
|
||||
TestNatResponse test_nat_response = 21;
|
||||
PeerDiscovery peer_discovery = 22;
|
||||
OnlineRequest online_request = 23;
|
||||
OnlineResponse online_response = 24;
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use std::io;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BytesCodec {
|
||||
state: DecodeState,
|
||||
raw: bool,
|
||||
max_packet_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DecodeState {
|
||||
Head,
|
||||
Data(usize),
|
||||
}
|
||||
|
||||
impl BytesCodec {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DecodeState::Head,
|
||||
raw: false,
|
||||
max_packet_length: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.raw = true;
|
||||
}
|
||||
|
||||
pub fn set_max_packet_length(&mut self, n: usize) {
|
||||
self.max_packet_length = n;
|
||||
}
|
||||
|
||||
fn decode_head(&mut self, src: &mut BytesMut) -> io::Result<Option<usize>> {
|
||||
if src.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let head_len = ((src[0] & 0x3) + 1) as usize;
|
||||
if src.len() < head_len {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut n = src[0] as usize;
|
||||
if head_len > 1 {
|
||||
n |= (src[1] as usize) << 8;
|
||||
}
|
||||
if head_len > 2 {
|
||||
n |= (src[2] as usize) << 16;
|
||||
}
|
||||
if head_len > 3 {
|
||||
n |= (src[3] as usize) << 24;
|
||||
}
|
||||
n >>= 2;
|
||||
if n > self.max_packet_length {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet"));
|
||||
}
|
||||
src.advance(head_len);
|
||||
src.reserve(n);
|
||||
return Ok(Some(n));
|
||||
}
|
||||
|
||||
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
|
||||
if src.len() < n {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(src.split_to(n)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for BytesCodec {
|
||||
type Item = BytesMut;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<BytesMut>, io::Error> {
|
||||
if self.raw {
|
||||
if !src.is_empty() {
|
||||
let len = src.len();
|
||||
return Ok(Some(src.split_to(len)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let n = match self.state {
|
||||
DecodeState::Head => match self.decode_head(src)? {
|
||||
Some(n) => {
|
||||
self.state = DecodeState::Data(n);
|
||||
n
|
||||
}
|
||||
None => return Ok(None),
|
||||
},
|
||||
DecodeState::Data(n) => n,
|
||||
};
|
||||
|
||||
match self.decode_data(n, src)? {
|
||||
Some(data) => {
|
||||
self.state = DecodeState::Head;
|
||||
Ok(Some(data))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<Bytes> for BytesCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> {
|
||||
if self.raw {
|
||||
buf.reserve(data.len());
|
||||
buf.put(data);
|
||||
return Ok(());
|
||||
}
|
||||
if data.len() <= 0x3F {
|
||||
buf.put_u8((data.len() << 2) as u8);
|
||||
} else if data.len() <= 0x3FFF {
|
||||
buf.put_u16_le((data.len() << 2) as u16 | 0x1);
|
||||
} else if data.len() <= 0x3FFFFF {
|
||||
let h = (data.len() << 2) as u32 | 0x2;
|
||||
buf.put_u16_le((h & 0xFFFF) as u16);
|
||||
buf.put_u8((h >> 16) as u8);
|
||||
} else if data.len() <= 0x3FFFFFFF {
|
||||
buf.put_u32_le((data.len() << 2) as u32 | 0x3);
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
|
||||
}
|
||||
buf.extend(data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_codec1() {
|
||||
let mut codec = BytesCodec::new();
|
||||
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());
|
||||
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);
|
||||
}
|
||||
let mut codec2 = BytesCodec::new();
|
||||
let mut buf2 = BytesMut::new();
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
buf2.extend(&buf_saved[0..1]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec2() {
|
||||
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_eq!(buf.len(), 1);
|
||||
bytes.resize(0x3F + 1, 2);
|
||||
assert!(!codec.encode(bytes.into(), &mut buf).is_err());
|
||||
assert_eq!(buf.len(), 0x3F + 2 + 2);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F + 1);
|
||||
assert_eq!(res[0], 2);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec3() {
|
||||
let mut codec = BytesCodec::new();
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_codec4() {
|
||||
let mut codec = BytesCodec::new();
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec5() {
|
||||
let mut codec = BytesCodec::new();
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec6() {
|
||||
let mut codec = BytesCodec::new();
|
||||
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());
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
buf2.extend(&buf_saved[1..6]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use zstd::block::{Compressor, Decompressor};
|
||||
|
||||
thread_local! {
|
||||
static COMPRESSOR: RefCell<Compressor> = RefCell::new(Compressor::new());
|
||||
static DECOMPRESSOR: RefCell<Decompressor> = RefCell::new(Decompressor::new());
|
||||
}
|
||||
|
||||
/// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
|
||||
/// which is currently 22. Levels >= 20
|
||||
/// Default level is ZSTD_CLEVEL_DEFAULT==3.
|
||||
/// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
|
||||
pub fn compress(data: &[u8], level: i32) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
COMPRESSOR.with(|c| {
|
||||
if let Ok(mut c) = c.try_borrow_mut() {
|
||||
match c.compress(data, level) {
|
||||
Ok(res) => out = res,
|
||||
Err(err) => {
|
||||
crate::log::debug!("Failed to compress: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
pub fn decompress(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
DECOMPRESSOR.with(|d| {
|
||||
if let Ok(mut d) = d.try_borrow_mut() {
|
||||
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;
|
||||
}
|
||||
match d.decompress(data, n) {
|
||||
Ok(res) => out = res,
|
||||
Err(err) => {
|
||||
crate::log::debug!("Failed to decompress: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
@@ -1,886 +0,0 @@
|
||||
use crate::log;
|
||||
use directories_next::ProjectDirs;
|
||||
use rand::Rng;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sodiumoxide::crypto::sign;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub const RENDEZVOUS_TIMEOUT: u64 = 12_000;
|
||||
pub const CONNECT_TIMEOUT: u64 = 18_000;
|
||||
pub const REG_INTERVAL: i64 = 12_000;
|
||||
pub const COMPRESS_LEVEL: i32 = 3;
|
||||
const SERIAL: i32 = 1;
|
||||
// 128x128
|
||||
#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding
|
||||
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII=
|
||||
";
|
||||
#[cfg(not(target_os = "macos"))] // 128x128 no padding
|
||||
pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA7VBMVEUAAAAAcf8Acf8Acf8Adf8Acf8Acf8AcP8Acv8AcP8Acf8Acf8Acf8Acv8Acf8Acf8Ab/8AcP8Acf8Acf8Acf/////7/f8Dc/8TfP/1+f/n8v9Hmf/u9v+Uw//Q5f9hp/8Yfv8Qev8Ld/+52P+z1f+s0f81j/8wjP8Hdf/3+/8mh/8fg//x9//h7//H4P9xsP9rrf9oq/8rif/r9P/D3v+92/+Duv9bpP/d7f/U5/9NnP8/lP8jhP/L4v/B3P+OwP9+t/95tf9Rn/8bgf/Z6v+Zx/90sv9lqf85kf+hy/9UoP+Wxf+kzP+dyP+Lvv/H4q8IAAAAFHRSTlMA+u6bB6x5XR4V0+S4i4k5N+a81W8MiAQAAAVcSURBVHjazdvpWtpAGIbhgEutdW3fL2GHsMsiq4KI+66t5384XahF/GbizJAy3j/1Ah5CJhNCxpm1vbryLRrBfxKJrq+sbjtSa5u7WIDdzTVH5PNSBAsSWfrsMJ+iWKDoJ2fW8hIWbGl55vW/YuE2XhUsb8CCr9OCJVix9G//gyWf/o6/KCyJfrbwAfAPYS0CayK/j4mbsGjrV8AXWLTrONuwasdZhVWrzgqsWnG+wap1Jwqrok4EVkUcmKhdVvBaOVnzYEY/oJpMD4mo6ONF/ZSIUsX2FZjQA7xRqUET+y/v2W/Sy59u62DCDMgdJmhqgIk7eqWQBBNWwPhmj147w8QTzTjKVsGEEBBLuzSrhIkivTF8DD/Aa6forQNMHBD/VyXkgHGfuBN5ALln1TADOnESyGCiT8L/1kILqD6Q0BEm9kkofhdSwNUJiV1jQvZ/SnthBNSaJJGZbgGJUnX+gEqCZPpsJ2T2Y/MGVBrE8eOAvCA/X8A4QXLnmEhTgIPqPAG5IQU4fhmkFOT7HAFenwIU8Jd/TUEODQIUtu1eOj/dUD9cknOTpgEDkup3YrOfVStDUomcWcBVisTiNxVw3TPpgCl4RgFFybZ/9iHmn8uS2yYBA8m7qUEu9oOEejH9gHxC+PazCHbcFM8K+gGHJNAs4z2xgnAkVHQDcnG1IzvnCSfvom7AM3EZ9voah4+KXoAvGFJHMSgqEfegF3BBTKoOVfkMMXFfJ8AT7MuXUDeOE9PWCUiKBpKOlmAP1gngH2LChw7vhJgr9YD8Hnt0BxrE27CtHnDJR4AHTX1+KFAP4Ef0LHTxN9HwlAMSbAjmoavKZ8ayakDXYAhwN3wzqgZk2UPvwRjshmeqATeCT09f3mWnEqoBGf4NxAB/moRqADuOtmDiid6KqQVcsQeOYOKW3uqqBRwL5nITj/yrlFpAVrDpTJT5llQLaLMHwshY7UDgvD+VujDC96WWWsBtSAE5FnChFnAeUkDMdAvw88EqTNT5SYXpTlgPaRQM1AIGorkolNnoUS1gJHigCX48SaoF3Asuspg4Mz0U8+FTgIkCG01V09kwBQP8xG5ofD5AXeirkPEJSUlwSVIfP5ykVQNaggvz+k7prTvVgDKF8BnUXP4kqgEe/257E8Ig7EE1gA8g2stBTz7FLxqrB3SIeYaeQ2IG6gE5l2+Cmt5MGOfP4KsGiH8DOYWOoujnDY2ALHF3810goZFOQDVBTFx9Uj7eI6bp6QTgnLjeGGq6KeJuoRUQixN3pDYWyz1Rva8XIL5UPFQZCsmG3gV7R+dieS+Jd3iHLglce7oBuCOhp3zwHLxPQpfQDvBOSKjZqUIml3ZJ6AD6AajFSZJwewWR8ZPsEY26SQDaJOMeZP23w6bTJ6kBjAJQILm9hzqm7otu4G+nhgGxIQUlPLKzL7GhbxqAboMCuN2XXd+lAL0ajAMwclV+FD6jAPEy5ghAlhfwX2FODX445gHKxyN++fs64PUHmDMAbbYN2DlKk2QaScwdgMs4SZxMv4OJJSoIIQBl2Qtk3gk4qiOUANRPJQHB+0A6j5AC4J27QQEZ4eZPAsYBXFk0N/YD7iUrxRBqALxOTzoMC3x8lCFlfkMjuz8iLfk6fzQCQgjg8q3ZEd8RzUVuKelBh96Nzcc3qelL1V+2zfRv1xc56Ino3tpdPT7cd//MspfTrD/7R6p4W4O2qLMObfnyIHvvYcrPtkZjDybW7d/eb32Bg/UlHnYXuXz5CMt8rC90sr7Uy/5iN+vL/ewveLS/5NNKwcbyR1r2a3/h8wdY+v3L2tZC5oUvW2uO1M7qyvp/Xv6/48z4CTxjJEfyjEaMAAAAAElFTkSuQmCC
|
||||
";
|
||||
#[cfg(target_os = "macos")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref ORG: Arc<RwLock<String>> = Arc::new(RwLock::new("com.carriez".to_owned()));
|
||||
}
|
||||
|
||||
type Size = (i32, i32, i32, i32);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CONFIG: Arc<RwLock<Config>> = Arc::new(RwLock::new(Config::load()));
|
||||
static ref CONFIG2: Arc<RwLock<Config2>> = Arc::new(RwLock::new(Config2::load()));
|
||||
static ref LOCAL_CONFIG: Arc<RwLock<LocalConfig>> = Arc::new(RwLock::new(LocalConfig::load()));
|
||||
pub static ref ONLINE: Arc<Mutex<HashMap<String, i64>>> = Default::default();
|
||||
pub static ref PROD_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Default::default();
|
||||
pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned()));
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref APP_DIR: Arc<RwLock<String>> = Default::default();
|
||||
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = Default::default();
|
||||
}
|
||||
const CHARS: &'static [char] = &[
|
||||
'2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
|
||||
'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[
|
||||
"rs-ny.rustdesk.com",
|
||||
"rs-sg.rustdesk.com",
|
||||
"rs-cn.rustdesk.com",
|
||||
];
|
||||
pub const RENDEZVOUS_PORT: i32 = 21116;
|
||||
pub const RELAY_PORT: i32 = 21117;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum NetworkType {
|
||||
Direct,
|
||||
ProxySocks,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
salt: String,
|
||||
#[serde(default)]
|
||||
pub key_pair: (Vec<u8>, Vec<u8>), // sk, pk
|
||||
#[serde(default)]
|
||||
key_confirmed: bool,
|
||||
#[serde(default)]
|
||||
keys_confirmed: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)]
|
||||
pub struct Socks5Server {
|
||||
#[serde(default)]
|
||||
pub proxy: String,
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// more variable configs
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct Config2 {
|
||||
#[serde(default)]
|
||||
rendezvous_server: String,
|
||||
#[serde(default)]
|
||||
nat_type: i32,
|
||||
#[serde(default)]
|
||||
serial: i32,
|
||||
|
||||
#[serde(default)]
|
||||
socks: Option<Socks5Server>,
|
||||
|
||||
// the other scalar value must before this
|
||||
#[serde(default)]
|
||||
pub options: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct PeerConfig {
|
||||
#[serde(default)]
|
||||
pub password: Vec<u8>,
|
||||
#[serde(default)]
|
||||
pub size: Size,
|
||||
#[serde(default)]
|
||||
pub size_ft: Size,
|
||||
#[serde(default)]
|
||||
pub size_pf: Size,
|
||||
#[serde(default)]
|
||||
pub view_style: String, // original (default), scale
|
||||
#[serde(default)]
|
||||
pub image_quality: String,
|
||||
#[serde(default)]
|
||||
pub custom_image_quality: Vec<i32>,
|
||||
#[serde(default)]
|
||||
pub show_remote_cursor: bool,
|
||||
#[serde(default)]
|
||||
pub lock_after_session_end: bool,
|
||||
#[serde(default)]
|
||||
pub privacy_mode: bool,
|
||||
#[serde(default)]
|
||||
pub port_forwards: Vec<(i32, String, i32)>,
|
||||
#[serde(default)]
|
||||
pub direct_failures: i32,
|
||||
#[serde(default)]
|
||||
pub disable_audio: bool,
|
||||
#[serde(default)]
|
||||
pub disable_clipboard: bool,
|
||||
#[serde(default)]
|
||||
pub enable_file_transfer: bool,
|
||||
|
||||
// the other scalar value must before this
|
||||
#[serde(default)]
|
||||
pub options: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub info: PeerInfoSerde,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct PeerInfoSerde {
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub hostname: String,
|
||||
#[serde(default)]
|
||||
pub platform: String,
|
||||
}
|
||||
|
||||
fn patch(path: PathBuf) -> PathBuf {
|
||||
if let Some(_tmp) = path.to_str() {
|
||||
#[cfg(windows)]
|
||||
return _tmp
|
||||
.replace(
|
||||
"system32\\config\\systemprofile",
|
||||
"ServiceProfiles\\LocalService",
|
||||
)
|
||||
.into();
|
||||
#[cfg(target_os = "macos")]
|
||||
return _tmp.replace("Application Support", "Preferences").into();
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if _tmp == "/root" {
|
||||
if let Ok(output) = std::process::Command::new("whoami").output() {
|
||||
let user = String::from_utf8_lossy(&output.stdout)
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_owned();
|
||||
if user != "root" {
|
||||
return format!("/home/{}", user).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
impl Config2 {
|
||||
fn load() -> Config2 {
|
||||
Config::load_::<Config2>("2")
|
||||
}
|
||||
|
||||
pub fn file() -> PathBuf {
|
||||
Config::file_("2")
|
||||
}
|
||||
|
||||
fn store(&self) {
|
||||
Config::store_(self, "2");
|
||||
}
|
||||
|
||||
pub fn get() -> Config2 {
|
||||
return CONFIG2.read().unwrap().clone();
|
||||
}
|
||||
|
||||
pub fn set(cfg: Config2) -> bool {
|
||||
let mut lock = CONFIG2.write().unwrap();
|
||||
if *lock == cfg {
|
||||
return false;
|
||||
}
|
||||
*lock = cfg;
|
||||
lock.store();
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
|
||||
file: PathBuf,
|
||||
) -> T {
|
||||
let cfg = match confy::load_path(&file) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
log::error!("Failed to load config: {}", err);
|
||||
T::default()
|
||||
}
|
||||
};
|
||||
cfg
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn load_<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
|
||||
suffix: &str,
|
||||
) -> T {
|
||||
let file = Self::file_(suffix);
|
||||
log::debug!("Configuration path: {}", file.display());
|
||||
let cfg = load_path(file);
|
||||
if suffix.is_empty() {
|
||||
log::debug!("{:?}", cfg);
|
||||
}
|
||||
cfg
|
||||
}
|
||||
|
||||
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
|
||||
let file = Self::file_(suffix);
|
||||
if let Err(err) = confy::store_path(file, config) {
|
||||
log::error!("Failed to store config: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn load() -> Config {
|
||||
Config::load_::<Config>("")
|
||||
}
|
||||
|
||||
fn store(&self) {
|
||||
Config::store_(self, "");
|
||||
}
|
||||
|
||||
pub fn file() -> PathBuf {
|
||||
Self::file_("")
|
||||
}
|
||||
|
||||
fn file_(suffix: &str) -> PathBuf {
|
||||
let name = format!("{}{}", *APP_NAME.read().unwrap(), suffix);
|
||||
Config::with_extension(Self::path(name))
|
||||
}
|
||||
|
||||
pub fn get_home() -> PathBuf {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return Self::path(APP_HOME_DIR.read().unwrap().as_str());
|
||||
if let Some(path) = dirs_next::home_dir() {
|
||||
patch(path)
|
||||
} else if let Ok(path) = std::env::current_dir() {
|
||||
path
|
||||
} else {
|
||||
std::env::temp_dir()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path<P: AsRef<Path>>(p: P) -> PathBuf {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let mut path: PathBuf = APP_DIR.read().unwrap().clone().into();
|
||||
path.push(p);
|
||||
return path;
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let org = "";
|
||||
#[cfg(target_os = "macos")]
|
||||
let org = ORG.read().unwrap().clone();
|
||||
// /var/root for root
|
||||
if let Some(project) = ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) {
|
||||
let mut path = patch(project.config_dir().to_path_buf());
|
||||
path.push(p);
|
||||
return path;
|
||||
}
|
||||
return "".into();
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
pub fn log_path() -> PathBuf {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(path) = dirs_next::home_dir().as_mut() {
|
||||
path.push(format!("Library/Logs/{}", *APP_NAME.read().unwrap()));
|
||||
return path.clone();
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mut path = Self::get_home();
|
||||
path.push(format!(".local/share/logs/{}", *APP_NAME.read().unwrap()));
|
||||
std::fs::create_dir_all(&path).ok();
|
||||
return path;
|
||||
}
|
||||
if let Some(path) = Self::path("").parent() {
|
||||
let mut path: PathBuf = path.into();
|
||||
path.push("log");
|
||||
return path;
|
||||
}
|
||||
"".into()
|
||||
}
|
||||
|
||||
pub fn ipc_path(postfix: &str) -> String {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// \\ServerName\pipe\PipeName
|
||||
// where ServerName is either the name of a remote computer or a period, to specify the local computer.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names
|
||||
format!(
|
||||
"\\\\.\\pipe\\{}\\query{}",
|
||||
*APP_NAME.read().unwrap(),
|
||||
postfix
|
||||
)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
#[cfg(target_os = "android")]
|
||||
let mut path: PathBuf =
|
||||
format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into();
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into();
|
||||
fs::create_dir(&path).ok();
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok();
|
||||
path.push(format!("ipc{}", postfix));
|
||||
path.to_str().unwrap_or("").to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_path() -> PathBuf {
|
||||
let mut path = Self::path("icons");
|
||||
if fs::create_dir_all(&path).is_err() {
|
||||
path = std::env::temp_dir();
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_any_listen_addr() -> SocketAddr {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)
|
||||
}
|
||||
|
||||
pub fn get_rendezvous_server() -> String {
|
||||
let mut rendezvous_server = Self::get_option("custom-rendezvous-server");
|
||||
if rendezvous_server.is_empty() {
|
||||
rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
|
||||
}
|
||||
if rendezvous_server.is_empty() {
|
||||
rendezvous_server = CONFIG2.read().unwrap().rendezvous_server.clone();
|
||||
}
|
||||
if rendezvous_server.is_empty() {
|
||||
rendezvous_server = Self::get_rendezvous_servers()
|
||||
.drain(..)
|
||||
.next()
|
||||
.unwrap_or("".to_owned());
|
||||
}
|
||||
if !rendezvous_server.contains(":") {
|
||||
rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT);
|
||||
}
|
||||
rendezvous_server
|
||||
}
|
||||
|
||||
pub fn get_rendezvous_servers() -> Vec<String> {
|
||||
let s = Self::get_option("custom-rendezvous-server");
|
||||
if !s.is_empty() {
|
||||
return vec![s];
|
||||
}
|
||||
let s = PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
|
||||
if !s.is_empty() {
|
||||
return vec![s];
|
||||
}
|
||||
let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL;
|
||||
if serial_obsolute {
|
||||
let ss: Vec<String> = Self::get_option("rendezvous-servers")
|
||||
.split(",")
|
||||
.filter(|x| x.contains("."))
|
||||
.map(|x| x.to_owned())
|
||||
.collect();
|
||||
if !ss.is_empty() {
|
||||
return ss;
|
||||
}
|
||||
}
|
||||
return RENDEZVOUS_SERVERS.iter().map(|x| x.to_string()).collect();
|
||||
}
|
||||
|
||||
pub fn reset_online() {
|
||||
*ONLINE.lock().unwrap() = Default::default();
|
||||
}
|
||||
|
||||
pub fn update_latency(host: &str, latency: i64) {
|
||||
ONLINE.lock().unwrap().insert(host.to_owned(), latency);
|
||||
let mut host = "".to_owned();
|
||||
let mut delay = i64::MAX;
|
||||
for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() {
|
||||
if tmp_delay > &0 && tmp_delay < &delay {
|
||||
delay = tmp_delay.clone();
|
||||
host = tmp_host.to_string();
|
||||
}
|
||||
}
|
||||
if !host.is_empty() {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if host != config.rendezvous_server {
|
||||
log::debug!("Update rendezvous_server in config to {}", host);
|
||||
log::debug!("{:?}", *ONLINE.lock().unwrap());
|
||||
config.rendezvous_server = host;
|
||||
config.store();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_id(id: &str) {
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if id == config.id {
|
||||
return;
|
||||
}
|
||||
config.id = id.into();
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn set_nat_type(nat_type: i32) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if nat_type == config.nat_type {
|
||||
return;
|
||||
}
|
||||
config.nat_type = nat_type;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_nat_type() -> i32 {
|
||||
CONFIG2.read().unwrap().nat_type
|
||||
}
|
||||
|
||||
pub fn set_serial(serial: i32) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if serial == config.serial {
|
||||
return;
|
||||
}
|
||||
config.serial = serial;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_serial() -> i32 {
|
||||
std::cmp::max(CONFIG2.read().unwrap().serial, SERIAL)
|
||||
}
|
||||
|
||||
fn get_auto_id() -> Option<String> {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
return Some(
|
||||
rand::thread_rng()
|
||||
.gen_range(1_000_000_000..2_000_000_000)
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let mut id = 0u32;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Ok(Some(ma)) = mac_address::get_mac_address() {
|
||||
for x in &ma.bytes()[2..] {
|
||||
id = (id << 8) | (*x as u32);
|
||||
}
|
||||
id = id & 0x1FFFFFFF;
|
||||
Some(id.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_auto_password() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..6)
|
||||
.map(|_| CHARS[rng.gen::<usize>() % CHARS.len()])
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_key_confirmed() -> bool {
|
||||
CONFIG.read().unwrap().key_confirmed
|
||||
}
|
||||
|
||||
pub fn set_key_confirmed(v: bool) {
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if config.key_confirmed == v {
|
||||
return;
|
||||
}
|
||||
config.key_confirmed = v;
|
||||
if !v {
|
||||
config.keys_confirmed = Default::default();
|
||||
}
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_host_key_confirmed(host: &str) -> bool {
|
||||
if let Some(true) = CONFIG.read().unwrap().keys_confirmed.get(host) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_host_key_confirmed(host: &str, v: bool) {
|
||||
if Self::get_host_key_confirmed(host) == v {
|
||||
return;
|
||||
}
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
config.keys_confirmed.insert(host.to_owned(), v);
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn set_key_pair(pair: (Vec<u8>, Vec<u8>)) {
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if config.key_pair == pair {
|
||||
return;
|
||||
}
|
||||
config.key_pair = pair;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_key_pair() -> (Vec<u8>, Vec<u8>) {
|
||||
// lock here to make sure no gen_keypair more than once
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if config.key_pair.0.is_empty() {
|
||||
let (pk, sk) = sign::gen_keypair();
|
||||
config.key_pair = (sk.0.to_vec(), pk.0.into());
|
||||
config.store();
|
||||
}
|
||||
config.key_pair.clone()
|
||||
}
|
||||
|
||||
pub fn get_id() -> String {
|
||||
let mut id = CONFIG.read().unwrap().id.clone();
|
||||
if id.is_empty() {
|
||||
if let Some(tmp) = Config::get_auto_id() {
|
||||
id = tmp;
|
||||
Config::set_id(&id);
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn get_id_or(b: String) -> String {
|
||||
let a = CONFIG.read().unwrap().id.clone();
|
||||
if a.is_empty() {
|
||||
b
|
||||
} else {
|
||||
a
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_options() -> HashMap<String, String> {
|
||||
CONFIG2.read().unwrap().options.clone()
|
||||
}
|
||||
|
||||
pub fn set_options(v: HashMap<String, String>) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if config.options == v {
|
||||
return;
|
||||
}
|
||||
config.options = v;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_option(k: &str) -> String {
|
||||
if let Some(v) = CONFIG2.read().unwrap().options.get(k) {
|
||||
v.clone()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
let v2 = if v.is_empty() { None } else { Some(&v) };
|
||||
if v2 != config.options.get(&k) {
|
||||
if v2.is_none() {
|
||||
config.options.remove(&k);
|
||||
} else {
|
||||
config.options.insert(k, v);
|
||||
}
|
||||
config.store();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_id() {
|
||||
// to-do: how about if one ip register a lot of ids?
|
||||
let id = Self::get_id();
|
||||
let mut rng = rand::thread_rng();
|
||||
let new_id = rng.gen_range(1_000_000_000..2_000_000_000).to_string();
|
||||
Config::set_id(&new_id);
|
||||
log::info!("id updated from {} to {}", id, new_id);
|
||||
}
|
||||
|
||||
pub fn set_password(password: &str) {
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if password == config.password {
|
||||
return;
|
||||
}
|
||||
config.password = password.into();
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_password() -> String {
|
||||
let mut password = CONFIG.read().unwrap().password.clone();
|
||||
if password.is_empty() {
|
||||
password = Config::get_auto_password();
|
||||
Config::set_password(&password);
|
||||
}
|
||||
password
|
||||
}
|
||||
|
||||
pub fn set_salt(salt: &str) {
|
||||
let mut config = CONFIG.write().unwrap();
|
||||
if salt == config.salt {
|
||||
return;
|
||||
}
|
||||
config.salt = salt.into();
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_salt() -> String {
|
||||
let mut salt = CONFIG.read().unwrap().salt.clone();
|
||||
if salt.is_empty() {
|
||||
salt = Config::get_auto_password();
|
||||
Config::set_salt(&salt);
|
||||
}
|
||||
salt
|
||||
}
|
||||
|
||||
pub fn set_socks(socks: Option<Socks5Server>) {
|
||||
let mut config = CONFIG2.write().unwrap();
|
||||
if config.socks == socks {
|
||||
return;
|
||||
}
|
||||
config.socks = socks;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_socks() -> Option<Socks5Server> {
|
||||
CONFIG2.read().unwrap().socks.clone()
|
||||
}
|
||||
|
||||
pub fn get_network_type() -> NetworkType {
|
||||
match &CONFIG2.read().unwrap().socks {
|
||||
None => NetworkType::Direct,
|
||||
Some(_) => NetworkType::ProxySocks,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get() -> Config {
|
||||
return CONFIG.read().unwrap().clone();
|
||||
}
|
||||
|
||||
pub fn set(cfg: Config) -> bool {
|
||||
let mut lock = CONFIG.write().unwrap();
|
||||
if *lock == cfg {
|
||||
return false;
|
||||
}
|
||||
*lock = cfg;
|
||||
lock.store();
|
||||
true
|
||||
}
|
||||
|
||||
fn with_extension(path: PathBuf) -> PathBuf {
|
||||
let ext = path.extension();
|
||||
if let Some(ext) = ext {
|
||||
let ext = format!("{}.toml", ext.to_string_lossy());
|
||||
path.with_extension(&ext)
|
||||
} else {
|
||||
path.with_extension("toml")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PEERS: &str = "peers";
|
||||
|
||||
impl PeerConfig {
|
||||
pub fn load(id: &str) -> PeerConfig {
|
||||
let _unused = CONFIG.read().unwrap(); // for lock
|
||||
match confy::load_path(&Self::path(id)) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
log::error!("Failed to load config: {}", err);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(&self, id: &str) {
|
||||
let _unused = CONFIG.read().unwrap(); // for lock
|
||||
if let Err(err) = confy::store_path(Self::path(id), self) {
|
||||
log::error!("Failed to store config: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(id: &str) {
|
||||
fs::remove_file(&Self::path(id)).ok();
|
||||
}
|
||||
|
||||
fn path(id: &str) -> PathBuf {
|
||||
let path: PathBuf = [PEERS, id].iter().collect();
|
||||
Config::with_extension(Config::path(path))
|
||||
}
|
||||
|
||||
pub fn peers() -> Vec<(String, SystemTime, PeerConfig)> {
|
||||
if let Ok(peers) = Config::path(PEERS).read_dir() {
|
||||
if let Ok(peers) = peers
|
||||
.map(|res| res.map(|e| e.path()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
{
|
||||
let mut peers: Vec<_> = peers
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.is_file()
|
||||
&& p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml")
|
||||
})
|
||||
.map(|p| {
|
||||
let t = crate::get_modified_time(&p);
|
||||
let id = p
|
||||
.file_stem()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let c = PeerConfig::load(&id);
|
||||
if c.info.platform.is_empty() {
|
||||
fs::remove_file(&p).ok();
|
||||
}
|
||||
(id, t, c)
|
||||
})
|
||||
.filter(|p| !p.2.info.platform.is_empty())
|
||||
.collect();
|
||||
peers.sort_unstable_by(|a, b| b.1.cmp(&a.1));
|
||||
return peers;
|
||||
}
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct LocalConfig {
|
||||
#[serde(default)]
|
||||
remote_id: String, // latest used one
|
||||
#[serde(default)]
|
||||
size: Size,
|
||||
#[serde(default)]
|
||||
pub fav: Vec<String>,
|
||||
#[serde(default)]
|
||||
options: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl LocalConfig {
|
||||
fn load() -> LocalConfig {
|
||||
Config::load_::<LocalConfig>("_local")
|
||||
}
|
||||
|
||||
fn store(&self) {
|
||||
Config::store_(self, "_local");
|
||||
}
|
||||
|
||||
pub fn get_size() -> Size {
|
||||
LOCAL_CONFIG.read().unwrap().size
|
||||
}
|
||||
|
||||
pub fn set_size(x: i32, y: i32, w: i32, h: i32) {
|
||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||
let size = (x, y, w, h);
|
||||
if size == config.size || size.2 < 300 || size.3 < 300 {
|
||||
return;
|
||||
}
|
||||
config.size = size;
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn set_remote_id(remote_id: &str) {
|
||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||
if remote_id == config.remote_id {
|
||||
return;
|
||||
}
|
||||
config.remote_id = remote_id.into();
|
||||
config.store();
|
||||
}
|
||||
|
||||
pub fn get_remote_id() -> String {
|
||||
LOCAL_CONFIG.read().unwrap().remote_id.clone()
|
||||
}
|
||||
|
||||
pub fn set_fav(fav: Vec<String>) {
|
||||
let mut lock = LOCAL_CONFIG.write().unwrap();
|
||||
if lock.fav == fav {
|
||||
return;
|
||||
}
|
||||
lock.fav = fav;
|
||||
lock.store();
|
||||
}
|
||||
|
||||
pub fn get_fav() -> Vec<String> {
|
||||
LOCAL_CONFIG.read().unwrap().fav.clone()
|
||||
}
|
||||
|
||||
pub fn get_option(k: &str) -> String {
|
||||
if let Some(v) = LOCAL_CONFIG.read().unwrap().options.get(k) {
|
||||
v.clone()
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_option(k: String, v: String) {
|
||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||
let v2 = if v.is_empty() { None } else { Some(&v) };
|
||||
if v2 != config.options.get(&k) {
|
||||
if v2.is_none() {
|
||||
config.options.remove(&k);
|
||||
} else {
|
||||
config.options.insert(k, v);
|
||||
}
|
||||
config.store();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct LanPeers {
|
||||
#[serde(default)]
|
||||
pub peers: String,
|
||||
}
|
||||
|
||||
impl LanPeers {
|
||||
pub fn load() -> LanPeers {
|
||||
let _unused = CONFIG.read().unwrap(); // for lock
|
||||
match confy::load_path(&Config::file_("_lan_peers")) {
|
||||
Ok(peers) => peers,
|
||||
Err(err) => {
|
||||
log::error!("Failed to load lan peers: {}", err);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(peers: String) {
|
||||
let f = LanPeers { peers };
|
||||
if let Err(err) = confy::store_path(Config::file_("_lan_peers"), f) {
|
||||
log::error!("Failed to store lan peers: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_time() -> crate::ResultType<u64> {
|
||||
let p = Config::file_("_lan_peers");
|
||||
Ok(fs::metadata(p)?
|
||||
.modified()?
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_millis() as _)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let cfg: Config = Default::default();
|
||||
let res = toml::to_string_pretty(&cfg);
|
||||
assert!(res.is_ok());
|
||||
let cfg: PeerConfig = Default::default();
|
||||
let res = toml::to_string_pretty(&cfg);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
use crate::{bail, message_proto::*, ResultType};
|
||||
use std::path::{Path, PathBuf};
|
||||
// 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> {
|
||||
let mut dir = FileDirectory {
|
||||
path: get_string(&path),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(windows)]
|
||||
if "/" == &get_string(&path) {
|
||||
let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() };
|
||||
for i in 0..32 {
|
||||
if drives & (1 << i) != 0 {
|
||||
let name = format!(
|
||||
"{}:",
|
||||
std::char::from_u32('A' as u32 + i as u32).unwrap_or('A')
|
||||
);
|
||||
dir.entries.push(FileEntry {
|
||||
name,
|
||||
entry_type: FileType::DirDrive.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
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()
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_file_name(p: &PathBuf) -> String {
|
||||
p.file_name()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_string(path: &PathBuf) -> String {
|
||||
path.to_str().unwrap_or("").to_owned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_path(path: &str) -> PathBuf {
|
||||
Path::new(path).to_path_buf()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_home_as_string() -> String {
|
||||
get_string(&Config::get_home())
|
||||
}
|
||||
|
||||
fn read_dir_recursive(
|
||||
path: &PathBuf,
|
||||
prefix: &PathBuf,
|
||||
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)?;
|
||||
for entry in fd.entries.iter() {
|
||||
match entry.entry_type.enum_value() {
|
||||
Ok(FileType::File) => {
|
||||
let mut entry = entry.clone();
|
||||
entry.name = get_string(&prefix.join(entry.name));
|
||||
files.push(entry);
|
||||
}
|
||||
Ok(FileType::Dir) => {
|
||||
if let Ok(mut tmp) = read_dir_recursive(
|
||||
&path.join(&entry.name),
|
||||
&prefix.join(&entry.name),
|
||||
include_hidden,
|
||||
) {
|
||||
for entry in tmp.drain(0..) {
|
||||
files.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
} else if path.is_file() {
|
||||
let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) {
|
||||
(
|
||||
meta.len(),
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
files.push(FileEntry {
|
||||
entry_type: FileType::File.into(),
|
||||
size,
|
||||
modified_time,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(files)
|
||||
} else {
|
||||
bail!("Not exists");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<FileEntry>> {
|
||||
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TransferJob {
|
||||
id: i32,
|
||||
path: PathBuf,
|
||||
files: Vec<FileEntry>,
|
||||
file_num: i32,
|
||||
file: Option<File>,
|
||||
total_size: u64,
|
||||
finished_size: u64,
|
||||
transferred: u64,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_ext(name: &str) -> &str {
|
||||
if let Some(i) = name.rfind(".") {
|
||||
return &name[i + 1..];
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_compressed_file(name: &str) -> bool {
|
||||
let ext = get_ext(name);
|
||||
ext == "xz"
|
||||
|| ext == "gz"
|
||||
|| ext == "zip"
|
||||
|| ext == "7z"
|
||||
|| ext == "rar"
|
||||
|| ext == "bz2"
|
||||
|| ext == "tgz"
|
||||
|| ext == "png"
|
||||
|| ext == "jpg"
|
||||
}
|
||||
|
||||
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();
|
||||
Self {
|
||||
id,
|
||||
path: get_path(&path),
|
||||
files,
|
||||
total_size,
|
||||
..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();
|
||||
Ok(Self {
|
||||
id,
|
||||
path: get_path(&path),
|
||||
files,
|
||||
total_size,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn files(&self) -> &Vec<FileEntry> {
|
||||
&self.files
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_files(&mut self, files: Vec<FileEntry>) {
|
||||
self.files = files;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn total_size(&self) -> u64 {
|
||||
self.total_size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finished_size(&self) -> u64 {
|
||||
self.finished_size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transferred(&self) -> u64 {
|
||||
self.transferred
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn file_num(&self) -> i32 {
|
||||
self.file_num
|
||||
}
|
||||
|
||||
pub fn modify_time(&self) {
|
||||
let file_num = self.file_num as usize;
|
||||
if file_num < self.files.len() {
|
||||
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();
|
||||
filetime::set_file_mtime(
|
||||
&path,
|
||||
filetime::FileTime::from_unix_time(entry.modified_time as _, 0),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_download_file(&self) {
|
||||
let file_num = self.file_num as usize;
|
||||
if file_num < self.files.len() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write(&mut self, block: FileTransferBlock, raw: Option<&[u8]>) -> ResultType<()> {
|
||||
if block.id != self.id {
|
||||
bail!("Wrong id");
|
||||
}
|
||||
let file_num = block.file_num as usize;
|
||||
if file_num >= self.files.len() {
|
||||
bail!("Wrong file number");
|
||||
}
|
||||
if file_num != self.file_num as usize || self.file.is_none() {
|
||||
self.modify_time();
|
||||
if let Some(file) = self.file.as_mut() {
|
||||
file.sync_all().await?;
|
||||
}
|
||||
self.file_num = block.file_num;
|
||||
let entry = &self.files[file_num];
|
||||
let path = self.join(&entry.name);
|
||||
if let Some(p) = path.parent() {
|
||||
std::fs::create_dir_all(p).ok();
|
||||
}
|
||||
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);
|
||||
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.transferred += data.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn join(&self, name: &str) -> PathBuf {
|
||||
if name.is_empty() {
|
||||
self.path.clone()
|
||||
} else {
|
||||
self.path.join(name)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read(&mut self) -> ResultType<Option<FileTransferBlock>> {
|
||||
let file_num = self.file_num as usize;
|
||||
if file_num >= self.files.len() {
|
||||
self.file.take();
|
||||
return Ok(None);
|
||||
}
|
||||
let name = &self.files[file_num].name;
|
||||
if self.file.is_none() {
|
||||
match File::open(self.join(&name)).await {
|
||||
Ok(file) => {
|
||||
self.file = Some(file);
|
||||
}
|
||||
Err(err) => {
|
||||
self.file_num += 1;
|
||||
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);
|
||||
}
|
||||
let mut compressed = false;
|
||||
let mut offset: usize = 0;
|
||||
loop {
|
||||
match self.file.as_mut().unwrap().read(&mut buf[offset..]).await {
|
||||
Err(err) => {
|
||||
self.file_num += 1;
|
||||
self.file = None;
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(n) => {
|
||||
offset += n;
|
||||
if n == 0 || offset == BUF_SIZE {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { buf.set_len(offset) };
|
||||
if offset == 0 {
|
||||
self.file_num += 1;
|
||||
self.file = None;
|
||||
} else {
|
||||
self.finished_size += offset as u64;
|
||||
if !is_compressed_file(name) {
|
||||
let tmp = compress(&buf, COMPRESS_LEVEL);
|
||||
if tmp.len() < buf.len() {
|
||||
buf = tmp;
|
||||
compressed = true;
|
||||
}
|
||||
}
|
||||
self.transferred += buf.len() as u64;
|
||||
}
|
||||
Ok(Some(FileTransferBlock {
|
||||
id: self.id,
|
||||
file_num: file_num as _,
|
||||
data: buf.into(),
|
||||
compressed,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_error<T: std::string::ToString>(id: i32, err: T, file_num: i32) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_error(FileTransferError {
|
||||
id,
|
||||
error: err.to_string(),
|
||||
file_num,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_dir(id: i32, path: String, files: Vec<FileEntry>) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_dir(FileDirectory {
|
||||
id,
|
||||
path,
|
||||
entries: files.into(),
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_block(block: FileTransferBlock) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_block(block);
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
|
||||
let mut action = FileAction::new();
|
||||
action.set_receive(FileTransferReceiveRequest {
|
||||
id,
|
||||
path,
|
||||
files: files.into(),
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_action(action);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message {
|
||||
let mut action = FileAction::new();
|
||||
action.set_send(FileTransferSendRequest {
|
||||
id,
|
||||
path,
|
||||
include_hidden,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_action(action);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_done(id: i32, file_num: i32) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_done(FileTransferDone {
|
||||
id,
|
||||
file_num,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove_job(id: i32, jobs: &mut Vec<TransferJob>) {
|
||||
*jobs = jobs.drain(0..).filter(|x| x.id() != id).collect();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_job(id: i32, jobs: &mut Vec<TransferJob>) -> Option<&mut TransferJob> {
|
||||
jobs.iter_mut().filter(|x| x.id() == id).next()
|
||||
}
|
||||
|
||||
pub async fn handle_read_jobs(
|
||||
jobs: &mut Vec<TransferJob>,
|
||||
stream: &mut crate::Stream,
|
||||
) -> ResultType<()> {
|
||||
let mut finished = Vec::new();
|
||||
for job in jobs.iter_mut() {
|
||||
match job.read().await {
|
||||
Err(err) => {
|
||||
stream
|
||||
.send(&new_error(job.id(), err, job.file_num()))
|
||||
.await?;
|
||||
}
|
||||
Ok(Some(block)) => {
|
||||
stream.send(&new_block(block)).await?;
|
||||
}
|
||||
Ok(None) => {
|
||||
finished.push(job.id());
|
||||
stream.send(&new_done(job.id(), job.file_num())).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in finished {
|
||||
remove_job(id, jobs);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
|
||||
let fd = read_dir(path, true)?;
|
||||
for entry in fd.entries.iter() {
|
||||
match entry.entry_type.enum_value() {
|
||||
Ok(FileType::Dir) => {
|
||||
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_dir(path).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove_file(file: &str) -> ResultType<()> {
|
||||
std::fs::remove_file(get_path(file))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn create_dir(dir: &str) -> ResultType<()> {
|
||||
std::fs::create_dir_all(get_path(dir))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
|
||||
for entry in entries {
|
||||
entry.name = entry.name.replace("\\", "/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
pub mod compress;
|
||||
#[path = "./protos/message.rs"]
|
||||
pub mod message_proto;
|
||||
#[path = "./protos/rendezvous.rs"]
|
||||
pub mod rendezvous_proto;
|
||||
pub use bytes;
|
||||
pub use futures;
|
||||
pub use protobuf;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead},
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
path::Path,
|
||||
time::{self, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
pub use tokio;
|
||||
pub use tokio_util;
|
||||
pub mod socket_client;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
pub use env_logger;
|
||||
pub use log;
|
||||
pub mod bytes_codec;
|
||||
#[cfg(feature = "quic")]
|
||||
pub mod quic;
|
||||
pub use anyhow::{self, bail};
|
||||
pub use futures_util;
|
||||
pub mod config;
|
||||
pub mod fs;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use mac_address;
|
||||
pub use rand;
|
||||
pub use regex;
|
||||
pub use sodiumoxide;
|
||||
pub use tokio_socks;
|
||||
pub use tokio_socks::IntoTargetAddr;
|
||||
pub use tokio_socks::TargetAddr;
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
pub type Stream = quic::Connection;
|
||||
#[cfg(not(feature = "quic"))]
|
||||
pub type Stream = tcp::FramedStream;
|
||||
|
||||
#[inline]
|
||||
pub async fn sleep(sec: f32) {
|
||||
tokio::time::sleep(time::Duration::from_secs_f32(sec)).await;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! allow_err {
|
||||
($e:expr) => {
|
||||
if let Err(err) = $e {
|
||||
log::debug!(
|
||||
"{:?}, {}:{}:{}:{}",
|
||||
err,
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn timeout<T: std::future::Future>(ms: u64, future: T) -> tokio::time::Timeout<T> {
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), future)
|
||||
}
|
||||
|
||||
pub type ResultType<F, E = anyhow::Error> = anyhow::Result<F, E>;
|
||||
|
||||
/// Certain router and firewalls scan the packet and if they
|
||||
/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address
|
||||
|
||||
pub struct AddrMangle();
|
||||
|
||||
impl AddrMangle {
|
||||
pub fn encode(addr: SocketAddr) -> Vec<u8> {
|
||||
match addr {
|
||||
SocketAddr::V4(addr_v4) => {
|
||||
let tm = (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_micros() as u32) as u128;
|
||||
let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128;
|
||||
let port = addr.port() as u128;
|
||||
let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF));
|
||||
let bytes = v.to_le_bytes();
|
||||
let mut n_padding = 0;
|
||||
for i in bytes.iter().rev() {
|
||||
if i == &0u8 {
|
||||
n_padding += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes[..(16 - n_padding)].to_vec()
|
||||
}
|
||||
_ => {
|
||||
panic!("Only support ipv4");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> SocketAddr {
|
||||
let mut padded = [0u8; 16];
|
||||
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();
|
||||
let port = (number & 0xFFFFFF) - (tm & 0xFFFF);
|
||||
SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
|
||||
port as u16,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if let Some(a) = a {
|
||||
let b = url
|
||||
.chars()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.filter(|(_, x)| x == &'.')
|
||||
.next()
|
||||
.map(|(i, _)| i);
|
||||
if let Some(b) = b {
|
||||
if a > b {
|
||||
if url
|
||||
.chars()
|
||||
.skip(n - b)
|
||||
.collect::<String>()
|
||||
.parse::<i32>()
|
||||
.is_ok()
|
||||
{
|
||||
return url.chars().skip(n - a).collect();
|
||||
} else {
|
||||
return url.chars().skip(n - a).take(a - b - 1).collect();
|
||||
}
|
||||
} else {
|
||||
return url.chars().skip(n - a).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
pub fn gen_version() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
pub fn is_valid_custom_id(id: &str) -> bool {
|
||||
regex::Regex::new(r"^[a-zA-Z]\w{5,15}$")
|
||||
.unwrap()
|
||||
.is_match(id)
|
||||
}
|
||||
|
||||
pub fn get_version_number(v: &str) -> i64 {
|
||||
let mut n = 0;
|
||||
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)
|
||||
.map(|m| m.modified().unwrap_or(UNIX_EPOCH))
|
||||
.unwrap_or(UNIX_EPOCH)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
use crate::{allow_err, anyhow::anyhow, ResultType};
|
||||
use protobuf::Message;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use tokio::{self, stream::StreamExt, sync::mpsc};
|
||||
|
||||
const QUIC_HBB: &[&[u8]] = &[b"hbb"];
|
||||
const SERVER_NAME: &str = "hbb";
|
||||
|
||||
type Sender = mpsc::UnboundedSender<Value>;
|
||||
type Receiver = mpsc::UnboundedReceiver<Value>;
|
||||
|
||||
pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> {
|
||||
let mut transport_config = quinn::TransportConfig::default();
|
||||
transport_config.stream_window_uni(0);
|
||||
let mut server_config = quinn::ServerConfig::default();
|
||||
server_config.transport = Arc::new(transport_config);
|
||||
let mut server_config = quinn::ServerConfigBuilder::new(server_config);
|
||||
server_config.protocols(QUIC_HBB);
|
||||
// server_config.enable_keylog();
|
||||
// server_config.use_stateless_retry(true);
|
||||
let mut endpoint = quinn::Endpoint::builder();
|
||||
endpoint.listen(server_config.build());
|
||||
let (end, incoming) = endpoint.with_socket(socket)?;
|
||||
Ok((Server { incoming }, end.local_addr()?))
|
||||
}
|
||||
|
||||
pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType<Connection> {
|
||||
let mut endpoint = quinn::Endpoint::builder();
|
||||
let mut client_config = quinn::ClientConfigBuilder::default();
|
||||
client_config.protocols(QUIC_HBB);
|
||||
//client_config.enable_keylog();
|
||||
endpoint.default_client_config(client_config.build());
|
||||
let (endpoint, _) = endpoint.bind(local_addr)?;
|
||||
let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?;
|
||||
Connection::new_for_client(new_conn.connection).await
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
incoming: quinn::Incoming,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> ResultType<Option<Connection>> {
|
||||
Connection::new_for_server(&mut self.incoming).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
conn: quinn::Connection,
|
||||
tx: quinn::SendStream,
|
||||
rx: Receiver,
|
||||
}
|
||||
|
||||
type Value = ResultType<Vec<u8>>;
|
||||
|
||||
impl Connection {
|
||||
async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType<Option<Self>> {
|
||||
if let Some(conn) = incoming.next().await {
|
||||
let quinn::NewConnection {
|
||||
connection: conn,
|
||||
// uni_streams,
|
||||
mut bi_streams,
|
||||
..
|
||||
} = conn.await?;
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Value>();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let stream = bi_streams.next().await;
|
||||
if let Some(stream) = stream {
|
||||
let stream = match stream {
|
||||
Err(e) => {
|
||||
tx.send(Err(e.into())).ok();
|
||||
break;
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let cloned = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(handle_request(stream.1, cloned).await);
|
||||
});
|
||||
} else {
|
||||
tx.send(Err(anyhow!("Reset by the peer"))).ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
log::info!("Exit connection outer loop");
|
||||
});
|
||||
let tx = conn.open_uni().await?;
|
||||
Ok(Some(Self { conn, tx, rx }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn new_for_client(conn: quinn::Connection) -> ResultType<Self> {
|
||||
let (tx, rx_quic) = conn.open_bi().await?;
|
||||
let (tx_mpsc, rx) = mpsc::unbounded_channel::<Value>();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(handle_request(rx_quic, tx_mpsc).await);
|
||||
});
|
||||
Ok(Self { conn, tx, rx })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Value> {
|
||||
// None is returned when all Sender halves have dropped,
|
||||
// indicating that no further values can be sent on the channel.
|
||||
self.rx.recv().await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remote_address(&self) -> SocketAddr {
|
||||
self.conn.remote_address()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> {
|
||||
self.tx.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> {
|
||||
match msg.write_to_bytes() {
|
||||
Ok(bytes) => self.send_raw(&bytes).await?,
|
||||
err => allow_err!(err),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use crate::{
|
||||
config::{Config, NetworkType},
|
||||
tcp::FramedStream,
|
||||
udp::FramedSocket,
|
||||
ResultType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
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")
|
||||
}
|
||||
|
||||
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()?,
|
||||
}
|
||||
.to_owned();
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: &str) -> String {
|
||||
let mut host = host.to_owned();
|
||||
if !host.contains(":") {
|
||||
host = format!("{}:{}", host, 0);
|
||||
}
|
||||
|
||||
match Config::get_network_type() {
|
||||
NetworkType::Direct => match to_socket_addr(&host) {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
},
|
||||
NetworkType::ProxySocks => match &host.into_target_addr() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>(
|
||||
target: T,
|
||||
local: SocketAddr,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<FramedStream> {
|
||||
let target_addr = target.into_target_addr()?;
|
||||
|
||||
if let Some(conf) = Config::get_socks() {
|
||||
FramedStream::connect(
|
||||
conf.proxy.as_str(),
|
||||
target_addr,
|
||||
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?)
|
||||
}
|
||||
}
|
||||
|
||||
pub 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) => {
|
||||
let socket = FramedSocket::new_proxy(
|
||||
conf.proxy.as_str(),
|
||||
local,
|
||||
conf.username.as_str(),
|
||||
conf.password.as_str(),
|
||||
ms_timeout,
|
||||
)
|
||||
.await?;
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
use crate::{bail, bytes_codec::BytesCodec, ResultType};
|
||||
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,
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
||||
};
|
||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs};
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
||||
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send + Sync>);
|
||||
|
||||
pub struct FramedStream(
|
||||
Framed<DynTcpStream, BytesCodec>,
|
||||
SocketAddr,
|
||||
Option<(Key, u64, u64)>,
|
||||
u64,
|
||||
);
|
||||
|
||||
impl Deref for FramedStream {
|
||||
type Target = Framed<DynTcpStream, BytesCodec>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for FramedStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DynTcpStream {
|
||||
type Target = Box<dyn TcpStreamTrait + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DynTcpStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
||||
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
|
||||
};
|
||||
if reuse {
|
||||
// windows has no reuse_port, but it's reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic behavior
|
||||
#[cfg(unix)]
|
||||
socket.set_reuseport(true)?;
|
||||
socket.set_reuseaddr(true)?;
|
||||
}
|
||||
socket.bind(addr)?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl FramedStream {
|
||||
pub async fn new<T1: ToSocketAddrs, T2: ToSocketAddrs>(
|
||||
remote_addr: T1,
|
||||
local_addr: T2,
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
bail!("could not resolve to any address");
|
||||
}
|
||||
|
||||
pub async fn connect<'a, 't, P, T1, T2>(
|
||||
proxy: P,
|
||||
target: T1,
|
||||
local: T2,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self>
|
||||
where
|
||||
P: ToProxyAddrs,
|
||||
T1: IntoTargetAddr<'t>,
|
||||
T2: ToSocketAddrs,
|
||||
{
|
||||
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,
|
||||
));
|
||||
};
|
||||
};
|
||||
bail!("could not resolve to any address");
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
self.3 = ms;
|
||||
}
|
||||
|
||||
pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self {
|
||||
Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.0.codec_mut().set_raw();
|
||||
self.2 = None;
|
||||
}
|
||||
|
||||
pub fn is_secured(&self) -> bool {
|
||||
self.2.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
|
||||
self.send_raw(msg.write_to_bytes()?).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
|
||||
let mut msg = msg;
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
key.1 += 1;
|
||||
let nonce = Self::get_nonce(key.1);
|
||||
msg = secretbox::seal(&msg, &nonce, &key.0);
|
||||
}
|
||||
self.send_bytes(bytes::Bytes::from(msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
|
||||
if self.3 > 0 {
|
||||
super::timeout(self.3, self.0.send(bytes)).await??;
|
||||
} else {
|
||||
self.0.send(bytes).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
let mut res = self.0.next().await;
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
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) {
|
||||
Ok(res) => {
|
||||
bytes.clear();
|
||||
bytes.put_slice(&res);
|
||||
}
|
||||
Err(()) => {
|
||||
return Some(Err(Error::new(ErrorKind::Other, "decryption error")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
|
||||
if let Ok(res) = super::timeout(ms, self.next()).await {
|
||||
res
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, key: Key) {
|
||||
self.2 = Some((key, 0, 0));
|
||||
}
|
||||
|
||||
fn get_nonce(seqnum: u64) -> Nonce {
|
||||
let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]);
|
||||
nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes());
|
||||
nonce
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
impl Unpin for DynTcpStream {}
|
||||
|
||||
impl AsyncRead for DynTcpStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for DynTcpStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + AsyncWrite + Unpin> TcpStreamTrait for R {}
|
||||
@@ -1,167 +0,0 @@
|
||||
use crate::{bail, ResultType};
|
||||
use anyhow::anyhow;
|
||||
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_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs};
|
||||
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
|
||||
|
||||
pub enum FramedSocket {
|
||||
Direct(UdpFramed<BytesCodec>),
|
||||
ProxySocks(Socks5UdpFramed),
|
||||
}
|
||||
|
||||
fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None),
|
||||
SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None),
|
||||
}?;
|
||||
if reuse {
|
||||
// windows has no reuse_port, but it's reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic behavior
|
||||
#[cfg(unix)]
|
||||
socket.set_reuse_port(true)?;
|
||||
socket.set_reuse_address(true)?;
|
||||
}
|
||||
// only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full
|
||||
socket.set_nonblocking(true)?;
|
||||
if buf_size > 0 {
|
||||
socket.set_recv_buffer_size(buf_size).ok();
|
||||
}
|
||||
log::info!(
|
||||
"Receive buf size of udp {}: {:?}",
|
||||
addr,
|
||||
socket.recv_buffer_size()
|
||||
);
|
||||
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())))
|
||||
}
|
||||
|
||||
#[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>(
|
||||
addr: T,
|
||||
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");
|
||||
}
|
||||
|
||||
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
|
||||
proxy: P,
|
||||
local: T,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
let framed = if username.trim().is_empty() {
|
||||
super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await??
|
||||
} else {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
log::trace!(
|
||||
"Socks5 udp connected, local addr: {:?}, target addr: {}",
|
||||
framed.local_addr(),
|
||||
framed.socks_addr()
|
||||
);
|
||||
Ok(Self::ProxySocks(framed))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
msg: &impl Message,
|
||||
addr: impl IntoTargetAddr<'_>,
|
||||
) -> 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?,
|
||||
_ => {}
|
||||
},
|
||||
Self::ProxySocks(f) => f.send((send_data, addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/68733302/1926020
|
||||
#[inline]
|
||||
pub async fn send_raw(
|
||||
&mut self,
|
||||
msg: &'static [u8],
|
||||
addr: impl IntoTargetAddr<'static>,
|
||||
) -> 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?,
|
||||
_ => {}
|
||||
},
|
||||
Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
match self {
|
||||
Self::Direct(f) => match f.next().await {
|
||||
Some(Ok((data, addr))) => {
|
||||
Some(Ok((data, addr.into_target_addr().ok()?.to_owned())))
|
||||
}
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
Self::ProxySocks(f) => match f.next().await {
|
||||
Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))),
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(
|
||||
&mut self,
|
||||
ms: u64,
|
||||
) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
if let Ok(res) =
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await
|
||||
{
|
||||
res
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
65
rcd/rustdesk-hbbr
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/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 rustdesk_hbbr.
|
||||
# rustdesk_hbbr_args (string): Set extra arguments to pass to rustdesk_hbbr
|
||||
# Default is "-k _".
|
||||
# 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="-k _"}
|
||||
: ${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/db/rustdesk-server
|
||||
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"
|
||||
68
rcd/rustdesk-hbbs
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/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 rustdesk_hbbs.
|
||||
# rustdesk_hbbs_ip (string): Set IP address/hostname of relay server to use
|
||||
# Defaults to "127.0.0.1", please replace with your server hostname/IP.
|
||||
# rustdesk_hbbs_args (string): Set extra arguments to pass to rustdesk_hbbs
|
||||
# Default is "-r ${rustdesk_hbbs_ip} -k _".
|
||||
# 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_ip:=127.0.0.1}
|
||||
: ${rustdesk_hbbs_args="-r ${rustdesk_hbbs_ip} -k _"}
|
||||
: ${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/db/rustdesk-server
|
||||
command_args="-p ${pidfile} -o /var/log/rustdesk-hbbs.log ${procname} ${rustdesk_hbbs_args}"
|
||||
## If you want the daemon to 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"
|
||||
@@ -1,5 +1,7 @@
|
||||
use clap::App;
|
||||
use hbb_common::{anyhow::Context, log, ResultType};
|
||||
use hbb_common::{
|
||||
allow_err, anyhow::{Context, Result}, get_version_number, log, tokio, ResultType
|
||||
};
|
||||
use ini::Ini;
|
||||
use sodiumoxide::crypto::sign;
|
||||
use std::{
|
||||
@@ -9,15 +11,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 +35,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 +46,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>) {
|
||||
@@ -70,22 +77,25 @@ pub fn init_args(args: &str, name: &str, about: &str) {
|
||||
}
|
||||
}
|
||||
for (k, v) in matches.args {
|
||||
if let Some(v) = v.vals.get(0) {
|
||||
if let Some(v) = v.vals.first() {
|
||||
std::env::set_var(arg_name(k), v.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
@@ -102,13 +112,18 @@ pub fn gen_sk(wait: u64) -> (String, Option<sign::SecretKey>) {
|
||||
if let Ok(mut file) = std::fs::File::open(sk_file) {
|
||||
let mut contents = String::new();
|
||||
if file.read_to_string(&mut contents).is_ok() {
|
||||
let sk = base64::decode(&contents).unwrap_or_default();
|
||||
let contents = contents.trim();
|
||||
let sk = base64::decode(contents).unwrap_or_default();
|
||||
if sk.len() == sign::SECRETKEYBYTES {
|
||||
let mut tmp = [0u8; sign::SECRETKEYBYTES];
|
||||
tmp[..].copy_from_slice(&sk);
|
||||
let pk = base64::encode(&tmp[sign::SECRETKEYBYTES / 2..]);
|
||||
log::info!("Private key comes from {}", sk_file);
|
||||
return (pk, Some(sign::SecretKey(tmp)));
|
||||
} else {
|
||||
// don't use log here, since it is async
|
||||
println!("Fatal error: malformed private key in {sk_file}.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -118,12 +133,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 +153,66 @@ 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::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! {
|
||||
_ = 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!();
|
||||
}
|
||||
|
||||
|
||||
pub fn check_software_update() {
|
||||
const ONE_DAY_IN_SECONDS: u64 = 60 * 60 * 24;
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::spawn(move || allow_err!(check_software_update_()));
|
||||
std::thread::sleep(std::time::Duration::from_secs(ONE_DAY_IN_SECONDS));
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn check_software_update_() -> hbb_common::ResultType<()> {
|
||||
let (request, url) = hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_SERVER.to_string());
|
||||
let latest_release_response = reqwest::Client::builder().build()?
|
||||
.post(url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let bytes = latest_release_response.bytes().await?;
|
||||
let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?;
|
||||
let response_url = resp.url;
|
||||
let latest_release_version = response_url.rsplit('/').next().unwrap_or_default();
|
||||
if get_version_number(&latest_release_version) > get_version_number(crate::version::VERSION) {
|
||||
log::info!("new version is available: {}", latest_release_version);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
16
src/hbbr.rs
@@ -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(())
|
||||
}
|
||||
|
||||
13
src/main.rs
@@ -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'
|
||||
-R, --rendezvous-servers=[HOSTS] 'Sets rendezvous servers, separated by comma'
|
||||
-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'
|
||||
-r, --relay-servers=[HOST] 'Sets the default relay servers, separated by comma'
|
||||
-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>()?;
|
||||
@@ -33,6 +31,7 @@ fn main() -> ResultType<()> {
|
||||
}
|
||||
let rmem = get_arg("rmem").parse::<usize>().unwrap_or(RMEM);
|
||||
let serial: i32 = get_arg("serial").parse().unwrap_or(0);
|
||||
RendezvousServer::start(port, serial, &get_arg("key"), rmem)?;
|
||||
crate::common::check_software_update();
|
||||
RendezvousServer::start(port, serial, &get_arg_or("key", "-".to_owned()), rmem)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
62
src/peer.rs
@@ -1,24 +1,27 @@
|
||||
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;
|
||||
pub static DAY_SECONDS: u64 = 3600 * 24;
|
||||
pub static IP_BLOCK_DUR: u64 = 60;
|
||||
pub const IP_CHANGE_DUR: u64 = 180;
|
||||
pub const IP_CHANGE_DUR_X2: u64 = IP_CHANGE_DUR * 2;
|
||||
pub const DAY_SECONDS: u64 = 3600 * 24;
|
||||
pub const IP_BLOCK_DUR: u64 = 60;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct PeerInfo {
|
||||
@@ -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]
|
||||
|
||||
@@ -8,7 +8,7 @@ use hbb_common::{
|
||||
protobuf::Message as _,
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
tcp::{new_listener, FramedStream},
|
||||
tcp::{listen_any, FramedStream},
|
||||
timeout,
|
||||
tokio::{
|
||||
self,
|
||||
@@ -25,6 +25,7 @@ use std::{
|
||||
io::prelude::*,
|
||||
io::Error,
|
||||
net::SocketAddr,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
type Usage = (usize, usize, usize, usize);
|
||||
@@ -36,13 +37,13 @@ lazy_static::lazy_static! {
|
||||
static ref BLOCKLIST: RwLock<HashSet<String>> = Default::default();
|
||||
}
|
||||
|
||||
static mut DOWNGRADE_THRESHOLD: f64 = 0.66;
|
||||
static mut DOWNGRADE_START_CHECK: usize = 1800_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";
|
||||
static DOWNGRADE_THRESHOLD_100: AtomicUsize = AtomicUsize::new(66); // 0.66
|
||||
static DOWNGRADE_START_CHECK: AtomicUsize = AtomicUsize::new(1_800_000); // in ms
|
||||
static LIMIT_SPEED: AtomicUsize = AtomicUsize::new(32 * 1024 * 1024); // in bit/s
|
||||
static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024); // in bit/s
|
||||
static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024); // in bit/s
|
||||
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 +51,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 +66,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 +78,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() {
|
||||
@@ -97,62 +100,60 @@ fn check_params() {
|
||||
.map(|x| x.parse::<f64>().unwrap_or(0.))
|
||||
.unwrap_or(0.);
|
||||
if tmp > 0. {
|
||||
unsafe {
|
||||
DOWNGRADE_THRESHOLD = tmp;
|
||||
}
|
||||
DOWNGRADE_THRESHOLD_100.store((tmp * 100.) as _, Ordering::SeqCst);
|
||||
}
|
||||
unsafe { log::info!("DOWNGRADE_THRESHOLD: {}", DOWNGRADE_THRESHOLD) };
|
||||
log::info!(
|
||||
"DOWNGRADE_THRESHOLD: {}",
|
||||
DOWNGRADE_THRESHOLD_100.load(Ordering::SeqCst) as f64 / 100.
|
||||
);
|
||||
let tmp = std::env::var("DOWNGRADE_START_CHECK")
|
||||
.map(|x| x.parse::<usize>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
if tmp > 0 {
|
||||
unsafe {
|
||||
DOWNGRADE_START_CHECK = tmp * 1000;
|
||||
}
|
||||
DOWNGRADE_START_CHECK.store(tmp * 1000, Ordering::SeqCst);
|
||||
}
|
||||
unsafe { log::info!("DOWNGRADE_START_CHECK: {}s", DOWNGRADE_START_CHECK / 1000) };
|
||||
log::info!(
|
||||
"DOWNGRADE_START_CHECK: {}s",
|
||||
DOWNGRADE_START_CHECK.load(Ordering::SeqCst) / 1000
|
||||
);
|
||||
let tmp = std::env::var("LIMIT_SPEED")
|
||||
.map(|x| x.parse::<f64>().unwrap_or(0.))
|
||||
.unwrap_or(0.);
|
||||
if tmp > 0. {
|
||||
unsafe {
|
||||
LIMIT_SPEED = (tmp * 1024. * 1024.) as usize;
|
||||
}
|
||||
LIMIT_SPEED.store((tmp * 1024. * 1024.) as usize, Ordering::SeqCst);
|
||||
}
|
||||
unsafe { log::info!("LIMIT_SPEED: {}Mb/s", LIMIT_SPEED as f64 / 1024. / 1024.) };
|
||||
log::info!(
|
||||
"LIMIT_SPEED: {}Mb/s",
|
||||
LIMIT_SPEED.load(Ordering::SeqCst) as f64 / 1024. / 1024.
|
||||
);
|
||||
let tmp = std::env::var("TOTAL_BANDWIDTH")
|
||||
.map(|x| x.parse::<f64>().unwrap_or(0.))
|
||||
.unwrap_or(0.);
|
||||
if tmp > 0. {
|
||||
unsafe {
|
||||
TOTAL_BANDWIDTH = (tmp * 1024. * 1024.) as usize;
|
||||
}
|
||||
TOTAL_BANDWIDTH.store((tmp * 1024. * 1024.) as usize, Ordering::SeqCst);
|
||||
}
|
||||
unsafe {
|
||||
log::info!(
|
||||
"TOTAL_BANDWIDTH: {}Mb/s",
|
||||
TOTAL_BANDWIDTH as f64 / 1024. / 1024.
|
||||
)
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"TOTAL_BANDWIDTH: {}Mb/s",
|
||||
TOTAL_BANDWIDTH.load(Ordering::SeqCst) as f64 / 1024. / 1024.
|
||||
);
|
||||
let tmp = std::env::var("SINGLE_BANDWIDTH")
|
||||
.map(|x| x.parse::<f64>().unwrap_or(0.))
|
||||
.unwrap_or(0.);
|
||||
if tmp > 0. {
|
||||
unsafe {
|
||||
SINGLE_BANDWIDTH = (tmp * 1024. * 1024.) as usize;
|
||||
}
|
||||
SINGLE_BANDWIDTH.store((tmp * 1024. * 1024.) as usize, Ordering::SeqCst);
|
||||
}
|
||||
unsafe {
|
||||
log::info!(
|
||||
"SINGLE_BANDWIDTH: {}Mb/s",
|
||||
SINGLE_BANDWIDTH as f64 / 1024. / 1024.
|
||||
)
|
||||
};
|
||||
log::info!(
|
||||
"SINGLE_BANDWIDTH: {}Mb/s",
|
||||
SINGLE_BANDWIDTH.load(Ordering::SeqCst) as f64 / 1024. / 1024.
|
||||
)
|
||||
}
|
||||
|
||||
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 +174,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 +184,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 +195,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 +211,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 +222,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,76 +230,68 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
|
||||
if let Some(v) = fds.next() {
|
||||
if let Ok(v) = v.parse::<f64>() {
|
||||
if v > 0. {
|
||||
unsafe {
|
||||
DOWNGRADE_THRESHOLD = v;
|
||||
}
|
||||
DOWNGRADE_THRESHOLD_100.store((v * 100.) as _, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
res = format!("{}\n", DOWNGRADE_THRESHOLD);
|
||||
}
|
||||
res = format!(
|
||||
"{}\n",
|
||||
DOWNGRADE_THRESHOLD_100.load(Ordering::SeqCst) as f64 / 100.
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("downgrade-start-check" | "t") => {
|
||||
if let Some(v) = fds.next() {
|
||||
if let Ok(v) = v.parse::<usize>() {
|
||||
if v > 0 {
|
||||
unsafe {
|
||||
DOWNGRADE_START_CHECK = v * 1000;
|
||||
}
|
||||
DOWNGRADE_START_CHECK.store(v * 1000, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
res = format!("{}s\n", DOWNGRADE_START_CHECK / 1000);
|
||||
}
|
||||
res = format!("{}s\n", DOWNGRADE_START_CHECK.load(Ordering::SeqCst) / 1000);
|
||||
}
|
||||
}
|
||||
Some("limit-speed" | "ls") => {
|
||||
if let Some(v) = fds.next() {
|
||||
if let Ok(v) = v.parse::<f64>() {
|
||||
if v > 0. {
|
||||
unsafe {
|
||||
LIMIT_SPEED = (v * 1024. * 1024.) as _;
|
||||
}
|
||||
LIMIT_SPEED.store((v * 1024. * 1024.) as _, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
res = format!("{}Mb/s\n", LIMIT_SPEED as f64 / 1024. / 1024.);
|
||||
}
|
||||
res = format!(
|
||||
"{}Mb/s\n",
|
||||
LIMIT_SPEED.load(Ordering::SeqCst) as f64 / 1024. / 1024.
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("total-bandwidth" | "tb") => {
|
||||
if let Some(v) = fds.next() {
|
||||
if let Ok(v) = v.parse::<f64>() {
|
||||
if v > 0. {
|
||||
unsafe {
|
||||
TOTAL_BANDWIDTH = (v * 1024. * 1024.) as _;
|
||||
limiter.set_speed_limit(TOTAL_BANDWIDTH as _);
|
||||
}
|
||||
TOTAL_BANDWIDTH.store((v * 1024. * 1024.) as _, Ordering::SeqCst);
|
||||
limiter.set_speed_limit(TOTAL_BANDWIDTH.load(Ordering::SeqCst) as _);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
res = format!("{}Mb/s\n", TOTAL_BANDWIDTH as f64 / 1024. / 1024.);
|
||||
}
|
||||
res = format!(
|
||||
"{}Mb/s\n",
|
||||
TOTAL_BANDWIDTH.load(Ordering::SeqCst) as f64 / 1024. / 1024.
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("single-bandwidth" | "sb") => {
|
||||
if let Some(v) = fds.next() {
|
||||
if let Ok(v) = v.parse::<f64>() {
|
||||
if v > 0. {
|
||||
unsafe {
|
||||
SINGLE_BANDWIDTH = (v * 1024. * 1024.) as _;
|
||||
}
|
||||
SINGLE_BANDWIDTH.store((v * 1024. * 1024.) as _, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
res = format!("{}Mb/s\n", SINGLE_BANDWIDTH as f64 / 1024. / 1024.);
|
||||
}
|
||||
res = format!(
|
||||
"{}Mb/s\n",
|
||||
SINGLE_BANDWIDTH.load(Ordering::SeqCst) as f64 / 1024. / 1024.
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("usage" | "u") => {
|
||||
@@ -306,15 +299,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.,
|
||||
@@ -331,7 +325,7 @@ async fn check_cmd(cmd: &str, limiter: Limiter) -> String {
|
||||
|
||||
async fn io_loop(listener: TcpListener, listener2: TcpListener, key: &str) {
|
||||
check_params();
|
||||
let limiter = <Limiter>::new(unsafe { TOTAL_BANDWIDTH as _ });
|
||||
let limiter = <Limiter>::new(TOTAL_BANDWIDTH.load(Ordering::SeqCst) as _);
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = listener.accept() => {
|
||||
@@ -369,12 +363,12 @@ async fn handle_connection(
|
||||
key: &str,
|
||||
ws: bool,
|
||||
) {
|
||||
let ip = addr.ip().to_string();
|
||||
if !ws && ip == "127.0.0.1" {
|
||||
let ip = hbb_common::try_into_v4(addr).ip();
|
||||
if !ws && ip.is_loopback() {
|
||||
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;
|
||||
@@ -384,6 +378,7 @@ async fn handle_connection(
|
||||
});
|
||||
return;
|
||||
}
|
||||
let ip = ip.to_string();
|
||||
if BLOCKLIST.read().await.get(&ip).is_some() {
|
||||
log::info!("{} blocked", ip);
|
||||
return;
|
||||
@@ -397,19 +392,30 @@ async fn handle_connection(
|
||||
|
||||
async fn make_pair(
|
||||
stream: TcpStream,
|
||||
addr: SocketAddr,
|
||||
mut addr: SocketAddr,
|
||||
key: &str,
|
||||
limiter: Limiter,
|
||||
ws: bool,
|
||||
) -> ResultType<()> {
|
||||
if ws {
|
||||
make_pair_(
|
||||
tokio_tungstenite::accept_async(stream).await?,
|
||||
addr,
|
||||
key,
|
||||
limiter,
|
||||
)
|
||||
.await;
|
||||
use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
|
||||
let callback = |req: &Request, response: Response| {
|
||||
let headers = req.headers();
|
||||
let real_ip = headers
|
||||
.get("X-Real-IP")
|
||||
.or_else(|| headers.get("X-Forwarded-For"))
|
||||
.and_then(|header_value| header_value.to_str().ok());
|
||||
if let Some(ip) = real_ip {
|
||||
if ip.contains('.') {
|
||||
addr = format!("{ip}:0").parse().unwrap_or(addr);
|
||||
} else {
|
||||
addr = format!("[{ip}]:0").parse().unwrap_or(addr);
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
};
|
||||
let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?;
|
||||
make_pair_(ws_stream, addr, key, limiter).await;
|
||||
} else {
|
||||
make_pair_(FramedStream::from(stream, addr), addr, key, limiter).await;
|
||||
}
|
||||
@@ -422,6 +428,7 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit
|
||||
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
|
||||
if let Some(rendezvous_message::Union::RequestRelay(rf)) = msg_in.union {
|
||||
if !key.is_empty() && rf.licence_key != key {
|
||||
log::warn!("Relay authentication failed from {} - invalid key", addr);
|
||||
return;
|
||||
}
|
||||
if !rf.uuid.is_empty() {
|
||||
@@ -469,10 +476,11 @@ async fn relay(
|
||||
let mut highest_s = 0;
|
||||
let mut downgrade: bool = false;
|
||||
let mut blacked: bool = false;
|
||||
let limiter = <Limiter>::new(unsafe { SINGLE_BANDWIDTH as _ });
|
||||
let blacklist_limiter = <Limiter>::new(unsafe { LIMIT_SPEED as _ });
|
||||
let sb = SINGLE_BANDWIDTH.load(Ordering::SeqCst) as f64;
|
||||
let limiter = <Limiter>::new(sb);
|
||||
let blacklist_limiter = <Limiter>::new(LIMIT_SPEED.load(Ordering::SeqCst) as _);
|
||||
let downgrade_threshold =
|
||||
(unsafe { SINGLE_BANDWIDTH as f64 * DOWNGRADE_THRESHOLD } / 1000.) as usize; // in bit/ms
|
||||
(sb * DOWNGRADE_THRESHOLD_100.load(Ordering::SeqCst) as f64 / 100. / 1000.) as usize; // in bit/ms
|
||||
let mut timer = interval(Duration::from_secs(3));
|
||||
let mut last_recv_time = std::time::Instant::now();
|
||||
loop {
|
||||
@@ -489,7 +497,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 +516,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 +538,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 +548,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 > DOWNGRADE_START_CHECK.load(Ordering::SeqCst)
|
||||
&& !downgrade
|
||||
&& total > elapsed * downgrade_threshold
|
||||
{
|
||||
downgrade = true;
|
||||
log::info!(
|
||||
"Downgrade {}, exceed downgrade threshold {}bit/ms in {}ms",
|
||||
id,
|
||||
downgrade_threshold,
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::common::*;
|
||||
use crate::peer::*;
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
allow_err, bail,
|
||||
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,15 @@ use ipnetwork::Ipv4Network;
|
||||
use sodiumoxide::crypto::sign;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
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),
|
||||
}
|
||||
@@ -54,10 +56,18 @@ enum Sink {
|
||||
}
|
||||
type Sender = mpsc::UnboundedSender<Data>;
|
||||
type Receiver = mpsc::UnboundedReceiver<Data>;
|
||||
static mut ROTATION_RELAY_SERVER: usize = 0;
|
||||
static ROTATION_RELAY_SERVER: AtomicUsize = AtomicUsize::new(0);
|
||||
type RelayServers = Vec<String>;
|
||||
static CHECK_RELAY_TIMEOUT: u64 = 3_000;
|
||||
static mut ALWAYS_USE_RELAY: bool = false;
|
||||
const CHECK_RELAY_TIMEOUT: u64 = 3_000;
|
||||
static ALWAYS_USE_RELAY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// Store punch hole requests
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::Mutex as TokioMutex; // differentiate if needed
|
||||
#[derive(Clone)]
|
||||
struct PunchReqEntry { tm: Instant, from_ip: String, to_ip: String, to_id: String }
|
||||
static PUNCH_REQS: Lazy<TokioMutex<Vec<PunchReqEntry>>> = Lazy::new(|| TokioMutex::new(Vec::new()));
|
||||
const PUNCH_REQ_DEDUPE_SEC: u64 = 60;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Inner {
|
||||
@@ -91,16 +101,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,69 +147,85 @@ 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()
|
||||
.to_uppercase()
|
||||
== "Y"
|
||||
{
|
||||
unsafe {
|
||||
ALWAYS_USE_RELAY = true;
|
||||
}
|
||||
ALWAYS_USE_RELAY.store(true, Ordering::SeqCst);
|
||||
}
|
||||
log::info!(
|
||||
"ALWAYS_USE_RELAY={}",
|
||||
if unsafe { ALWAYS_USE_RELAY } {
|
||||
if ALWAYS_USE_RELAY.load(Ordering::SeqCst) {
|
||||
"Y"
|
||||
} else {
|
||||
"N"
|
||||
}
|
||||
);
|
||||
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);
|
||||
if let Err(err) = test_hbbs(test_addr).await {
|
||||
if test_addr.is_ipv6() && test_addr.ip().is_unspecified() {
|
||||
let mut test_addr = test_addr;
|
||||
test_addr.set_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||
if let Err(err) = test_hbbs(test_addr).await {
|
||||
log::error!("Failed to run hbbs test with {test_addr}: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to run hbbs test with {test_addr}: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
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 +251,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 +321,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 +402,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 +443,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 try_into_v4(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 +488,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 +582,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;
|
||||
@@ -665,6 +688,7 @@ impl RendezvousServer {
|
||||
) -> ResultType<(RendezvousMessage, Option<SocketAddr>)> {
|
||||
let mut ph = ph;
|
||||
if !key.is_empty() && ph.licence_key != key {
|
||||
log::warn!("Authentication failed from {} for peer {} - invalid key", addr, ph.id);
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_punch_hole_response(PunchHoleResponse {
|
||||
failure: punch_hole_response::Failure::LICENSE_MISMATCH.into(),
|
||||
@@ -691,28 +715,42 @@ impl RendezvousServer {
|
||||
});
|
||||
return Ok((msg_out, None));
|
||||
}
|
||||
|
||||
// record punch hole request (from addr -> peer id/peer_addr)
|
||||
{
|
||||
let from_ip = try_into_v4(addr).ip().to_string();
|
||||
let to_ip = try_into_v4(peer_addr).ip().to_string();
|
||||
let to_id_clone = id.clone();
|
||||
let mut lock = PUNCH_REQS.lock().await;
|
||||
let mut dup = false;
|
||||
for e in lock.iter().rev().take(30) { // only check recent tail subset for speed
|
||||
if e.from_ip == from_ip && e.to_id == to_id_clone {
|
||||
if e.tm.elapsed().as_secs() < PUNCH_REQ_DEDUPE_SEC { dup = true; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !dup { lock.push(PunchReqEntry { tm: Instant::now(), from_ip, to_ip, to_id: to_id_clone }); }
|
||||
}
|
||||
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
let peer_is_lan = self.is_lan(peer_addr);
|
||||
let is_lan = self.is_lan(addr);
|
||||
let mut relay_server = self.get_relay_server(addr.ip(), peer_addr.ip());
|
||||
if unsafe { ALWAYS_USE_RELAY } || (peer_is_lan ^ is_lan) {
|
||||
if ALWAYS_USE_RELAY.load(Ordering::SeqCst) || (peer_is_lan ^ is_lan) {
|
||||
if peer_is_lan {
|
||||
// https://github.com/rustdesk/rustdesk-server/issues/24
|
||||
relay_server = self.inner.local_ip.clone()
|
||||
}
|
||||
ph.nat_type = NatType::SYMMETRIC.into(); // will force relay
|
||||
}
|
||||
let same_intranet = !ws
|
||||
&& match peer_addr {
|
||||
SocketAddr::V4(a) => match addr {
|
||||
SocketAddr::V4(b) => a.ip() == b.ip(),
|
||||
let same_intranet: bool = !ws
|
||||
&& (peer_is_lan && is_lan || {
|
||||
match (peer_addr, addr) {
|
||||
(SocketAddr::V4(a), SocketAddr::V4(b)) => a.ip() == b.ip(),
|
||||
(SocketAddr::V6(a), SocketAddr::V6(b)) => a.ip() == b.ip(),
|
||||
_ => false,
|
||||
},
|
||||
SocketAddr::V6(a) => match addr {
|
||||
SocketAddr::V6(b) => a.ip() == b.ip(),
|
||||
_ => false,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
let socket_addr = AddrMangle::encode(addr).into();
|
||||
if same_intranet {
|
||||
log::debug!(
|
||||
@@ -740,14 +778,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 +796,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 +820,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 +848,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 +863,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 +879,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,
|
||||
@@ -892,24 +930,24 @@ impl RendezvousServer {
|
||||
} else if self.relay_servers.len() == 1 {
|
||||
return self.relay_servers[0].clone();
|
||||
}
|
||||
let i = unsafe {
|
||||
ROTATION_RELAY_SERVER += 1;
|
||||
ROTATION_RELAY_SERVER % self.relay_servers.len()
|
||||
};
|
||||
let i = ROTATION_RELAY_SERVER.fetch_add(1, Ordering::SeqCst) % self.relay_servers.len();
|
||||
self.relay_servers[i].clone()
|
||||
}
|
||||
|
||||
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!(
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n",
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
|
||||
"relay-servers(rs) <separated by ,>",
|
||||
"reload-geo(rg)",
|
||||
"ip-blocker(ib) [<ip>|<number>] [-]",
|
||||
"ip-changes(ic) [<id>|<number>] [-]",
|
||||
"punch-requests(pr) [<number>] [-]",
|
||||
"always-use-relay(aur)",
|
||||
"test-geo(tg) <ip1> <ip2>"
|
||||
)
|
||||
@@ -919,7 +957,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 +973,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 +1000,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 +1019,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,21 +1042,43 @@ 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,);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("punch-requests" | "pr") => {
|
||||
use std::fmt::Write as _;
|
||||
let mut lock = PUNCH_REQS.lock().await;
|
||||
let arg = fds.next();
|
||||
if let Some("-") = arg { lock.clear(); }
|
||||
else {
|
||||
let mut start = arg.and_then(|x| x.parse::<usize>().ok()).unwrap_or(0);
|
||||
let mut page_size = fds.next().and_then(|x| x.parse::<usize>().ok()).unwrap_or(10);
|
||||
if page_size == 0 { page_size = 10; }
|
||||
for (_, e) in lock.iter().enumerate().skip(start).take(page_size) {
|
||||
let age = e.tm.elapsed();
|
||||
let event_system = std::time::SystemTime::now() - age;
|
||||
let event_iso = chrono::DateTime::<chrono::Utc>::from(event_system)
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let _ = writeln!(res, "{} {} -> {}@{}", event_iso, e.from_ip, e.to_id, e.to_ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("always-use-relay" | "aur") => {
|
||||
if let Some(rs) = fds.next() {
|
||||
if rs.to_uppercase() == "Y" {
|
||||
unsafe { ALWAYS_USE_RELAY = true };
|
||||
ALWAYS_USE_RELAY.store(true, Ordering::SeqCst);
|
||||
} else {
|
||||
unsafe { ALWAYS_USE_RELAY = false };
|
||||
ALWAYS_USE_RELAY.store(false, Ordering::SeqCst);
|
||||
}
|
||||
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: {:?}",
|
||||
ALWAYS_USE_RELAY.load(Ordering::SeqCst)
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("test-geo" | "tg") => {
|
||||
@@ -1039,10 +1101,11 @@ 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" {
|
||||
let ip = try_into_v4(addr).ip();
|
||||
if 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;
|
||||
@@ -1089,23 +1152,36 @@ impl RendezvousServer {
|
||||
async fn handle_listener_inner(
|
||||
&mut self,
|
||||
stream: TcpStream,
|
||||
addr: SocketAddr,
|
||||
mut addr: SocketAddr,
|
||||
key: &str,
|
||||
ws: bool,
|
||||
) -> ResultType<()> {
|
||||
let mut sink;
|
||||
if ws {
|
||||
let ws_stream = tokio_tungstenite::accept_async(stream).await?;
|
||||
use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
|
||||
let callback = |req: &Request, response: Response| {
|
||||
let headers = req.headers();
|
||||
let real_ip = headers
|
||||
.get("X-Real-IP")
|
||||
.or_else(|| headers.get("X-Forwarded-For"))
|
||||
.and_then(|header_value| header_value.to_str().ok());
|
||||
if let Some(ip) = real_ip {
|
||||
if ip.contains('.') {
|
||||
addr = format!("{ip}:0").parse().unwrap_or(addr);
|
||||
} else {
|
||||
addr = format!("[{ip}]:0").parse().unwrap_or(addr);
|
||||
}
|
||||
}
|
||||
Ok(response)
|
||||
};
|
||||
let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?;
|
||||
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 +1194,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 +1207,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 +1216,7 @@ impl RendezvousServer {
|
||||
}
|
||||
.write_to_bytes()
|
||||
.unwrap_or_default(),
|
||||
&self.inner.sk.as_ref().unwrap(),
|
||||
self.inner.sk.as_ref().unwrap(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
@@ -1168,14 +1244,11 @@ impl RendezvousServer {
|
||||
out_sk = sk;
|
||||
if !key.is_empty() {
|
||||
key = pk;
|
||||
} else {
|
||||
std::env::set_var("KEY_FOR_API", pk);
|
||||
}
|
||||
}
|
||||
|
||||
if !key.is_empty() {
|
||||
log::info!("Key: {}", key);
|
||||
std::env::set_var("KEY_FOR_API", key.clone());
|
||||
}
|
||||
(key, out_sk)
|
||||
}
|
||||
@@ -1183,8 +1256,16 @@ impl RendezvousServer {
|
||||
#[inline]
|
||||
fn is_lan(&self, addr: SocketAddr) -> bool {
|
||||
if let Some(network) = &self.inner.mask {
|
||||
if let SocketAddr::V4(addr) = addr {
|
||||
return network.contains(*addr.ip());
|
||||
match addr {
|
||||
SocketAddr::V4(v4_socket_addr) => {
|
||||
return network.contains(*v4_socket_addr.ip());
|
||||
}
|
||||
|
||||
SocketAddr::V6(v6_socket_addr) => {
|
||||
if let Some(v4_addr) = v6_socket_addr.ip().to_ipv4() {
|
||||
return network.contains(v4_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -1196,13 +1277,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 +1293,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 +1301,16 @@ 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 addr = addr;
|
||||
if addr.ip().is_unspecified() {
|
||||
addr.set_ip(if addr.is_ipv4() {
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST)
|
||||
} else {
|
||||
IpAddr::V6(Ipv6Addr::LOCALHOST)
|
||||
});
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -1233,8 +1323,7 @@ async fn test_hbbs(addr: SocketAddr) -> ResultType<()> {
|
||||
tokio::select! {
|
||||
_ = timer.tick() => {
|
||||
if last_time_recv.elapsed().as_secs() > 12 {
|
||||
log::error!("Timeout of test_hbbs");
|
||||
std::process::exit(1);
|
||||
bail!("Timeout of test_hbbs");
|
||||
}
|
||||
socket.send(&msg_out, addr).await?;
|
||||
}
|
||||
@@ -1261,3 +1350,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, true, 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, true, 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)
|
||||
}
|
||||
|
||||
43
src/utils.rs
@@ -10,7 +10,7 @@ use std::{
|
||||
fn print_help() {
|
||||
println!(
|
||||
"Usage:
|
||||
rustdesk-util [command]\n
|
||||
rustdesk-utils [command]\n
|
||||
Available Commands:
|
||||
genkeypair Generate a new keypair
|
||||
validatekeypair [public key] [secret key] Validate an existing keypair
|
||||
@@ -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");
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub const VERSION: &str = "1.1.6";
|
||||
@@ -10,6 +10,8 @@ WorkingDirectory=/var/lib/rustdesk-server/
|
||||
User=
|
||||
Group=
|
||||
Restart=always
|
||||
StandardOutput=append:/var/log/rustdesk-server/hbbr.log
|
||||
StandardError=append:/var/log/rustdesk-server/hbbr.error
|
||||
# Restart service after 10 seconds if node service crashes
|
||||
RestartSec=10
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ WorkingDirectory=/var/lib/rustdesk-server/
|
||||
User=
|
||||
Group=
|
||||
Restart=always
|
||||
StandardOutput=append:/var/log/rustdesk-server/hbbs.log
|
||||
StandardError=append:/var/log/rustdesk-server/hbbs.error
|
||||
# Restart service after 10 seconds if node service crashes
|
||||
RestartSec=10
|
||||
|
||||
|
||||
8
ui/.cargo/config.toml
Normal 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
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
3787
ui/Cargo.lock
generated
Normal file
31
ui/Cargo.toml
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: '5177',
|
||||
strictPort: true
|
||||
}
|
||||
});
|
||||
BIN
ui/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
ui/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
ui/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
ui/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
ui/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
ui/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
ui/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
ui/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
ui/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
ui/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
ui/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
ui/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
ui/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
ui/icons/icon.icns
Normal file
BIN
ui/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
ui/icons/icon.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
178
ui/setup.nsi
Normal 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.15"
|
||||
|
||||
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\bin\hbbs.exe" enable=yes'
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\bin\hbbs.exe" enable=yes'
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\bin\hbbr.exe" enable=yes'
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\bin\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
23
ui/setup/service/run.cmd
Normal 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
@@ -0,0 +1,5 @@
|
||||
pub mod view;
|
||||
pub mod service;
|
||||
|
||||
pub use view::*;
|
||||
pub use service::*;
|
||||
3
ui/src/adapter/service/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod windows;
|
||||
|
||||
pub use windows::*;
|
||||
130
ui/src/adapter/service/windows.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
221
ui/src/adapter/view/desktop.rs
Normal 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()
|
||||
}
|
||||
3
ui/src/adapter/view/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod desktop;
|
||||
|
||||
pub use desktop::*;
|
||||
17
ui/src/lib.rs
Normal 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
@@ -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
@@ -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::*;
|
||||
59
ui/src/usecase/presenter.rs
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||