Browse Source

Merge branch 'master' into listern-for-unix-signal

RustDesk 3 years ago
parent
commit
75a40412b4

+ 62 - 18
.github/workflows/build.yaml

@@ -44,9 +44,11 @@ jobs:
44 44
       - name: Install toolchain
45 45
         uses: actions-rs/toolchain@v1
46 46
         with:
47
-          toolchain: nightly
47
+          toolchain: "1.62"
48 48
           override: true
49 49
           default: true
50
+          components: rustfmt
51
+          profile: minimal
50 52
           target: ${{ matrix.job.target }}
51 53
 
52 54
       - name: Build
@@ -56,53 +58,95 @@ jobs:
56 58
           args: --release --all-features --target=${{ matrix.job.target }}
57 59
           use-cross: true  
58 60
 
59
-      # - name: Run tests
60
-      #   run: cargo test --verbose
61
+      - name: Exec chmod
62
+        run: chmod -v a+x target/${{ matrix.job.target }}/release/*
61 63
 
62 64
       - name: Publish Artifacts
63 65
         uses: actions/upload-artifact@v3
64 66
         with:
65
-          name: binaries-${{ matrix.job.name }}
67
+          name: binaries-linux-${{ matrix.job.name }}
66 68
           path: |
67 69
             target/${{ matrix.job.target }}/release/hbbr
68 70
             target/${{ matrix.job.target }}/release/hbbs
69 71
             target/${{ matrix.job.target }}/release/rustdesk-utils
70 72
           if-no-files-found: error
71 73
 
74
+  build-win:
75
+    name: Build - windows
76
+    runs-on: windows-2019
77
+
78
+    steps:
79
+      
80
+      - name: Checkout
81
+        uses: actions/checkout@v3
82
+
83
+      - name: Install toolchain
84
+        uses: actions-rs/toolchain@v1
85
+        with:
86
+          toolchain: "1.62"
87
+          override: true
88
+          default: true
89
+          components: rustfmt
90
+          profile: minimal
91
+          target: x86_64-pc-windows-msvc
92
+
93
+      - name: Build
94
+        uses: actions-rs/cargo@v1
95
+        with:
96
+          command: build
97
+          args: --release --all-features --target=x86_64-pc-windows-msvc
98
+          use-cross: true  
99
+
100
+      - name: Publish Artifacts
101
+        uses: actions/upload-artifact@v3
102
+        with:
103
+          name: binaries-windows-x86_64
104
+          path: |
105
+            target\x86_64-pc-windows-msvc\release\hbbr.exe
106
+            target\x86_64-pc-windows-msvc\release\hbbs.exe
107
+            target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe 
108
+          if-no-files-found: error
109
+
72 110
   # github (draft) release with all binaries
73 111
   release:
74 112
 
75 113
     name: Github release
76
-    needs: build
114
+    needs: 
115
+      - build
116
+      - build-win
77 117
     runs-on: ubuntu-22.04
78 118
     strategy:
79 119
       fail-fast: false
80 120
       matrix:
81 121
         job:
82
-          - { name: "amd64" }
83
-          - { name: "arm64v8" }
84
-          - { name: "armv7" }
85
-          - { name: "i386" }
122
+          - { os: "linux", name: "amd64" }
123
+          - { os: "linux", name: "arm64v8" }
124
+          - { os: "linux", name: "armv7" }
125
+          - { os: "linux", name: "i386" }
126
+          - { os: "windows", name: "x86_64" }
86 127
 
87 128
     steps:
88 129
 
89
-      - name: Download binaries (${{ matrix.job.name }})
130
+      - name: Download binaries (${{ matrix.job.os }} - ${{ matrix.job.name }})
90 131
         uses: actions/download-artifact@v3
91 132
         with:
92
-          name: binaries-${{ matrix.job.name }}
133
+          name: binaries-${{ matrix.job.os }}-${{ matrix.job.name }}
93 134
           path: ${{ matrix.job.name }}
94 135
 
95
-      - name: Pack files (${{ matrix.job.name }})
136
+      - name: Exec chmod
137
+        run: chmod -v a+x ${{ matrix.job.name }}/*
138
+
139
+      - name: Pack files (${{ matrix.job.os }} - ${{ matrix.job.name }})
96 140
         run: |
97 141
           sudo apt update
98 142
           DEBIAN_FRONTEND=noninteractive sudo apt install -y zip
99
-          zip ${{ matrix.job.name }}/rustdesk-server-linux-${{ matrix.job.name }}.zip ${{ matrix.job.name }}/hbbr ${{ matrix.job.name }}/hbbs ${{ matrix.job.name }}/rustdesk-utils
143
+          zip ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}.zip ${{ matrix.job.name }}/*
100 144
 
101
-      - name: Create Release (${{ matrix.job.name }})
145
+      - name: Create Release (${{ matrix.job.os }} - (${{ matrix.job.name }})
102 146
         uses: softprops/action-gh-release@v1
103 147
         with:
104 148
           draft: true
105
-          files: ${{ matrix.job.name }}/rustdesk-server-linux-${{ matrix.job.name }}.zip
149
+          files: ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}.zip
106 150
             
107 151
   # docker build and push of single-arch images
108 152
   docker:
@@ -127,7 +171,7 @@ jobs:
127 171
       - name: Download binaries
128 172
         uses: actions/download-artifact@v3
129 173
         with:
130
-          name: binaries-${{ matrix.job.name }}
174
+          name: binaries-linux-${{ matrix.job.name }}
131 175
           path: docker/rootfs/usr/bin
132 176
 
133 177
       - name: Make binaries executable
@@ -247,7 +291,7 @@ jobs:
247 291
       - name: Download binaries
248 292
         uses: actions/download-artifact@v3
249 293
         with:
250
-          name: binaries-${{ matrix.job.name }}
294
+          name: binaries-linux-${{ matrix.job.name }}
251 295
           path: docker-classic/
252 296
 
253 297
       - name: Make binaries executable
@@ -314,7 +358,7 @@ jobs:
314 358
       - name: Download binaries
315 359
         uses: actions/download-artifact@v3
316 360
         with:
317
-          name: binaries-${{ matrix.job.name }}
361
+          name: binaries-linux-${{ matrix.job.name }}
318 362
           path: debian-build/${{ matrix.job.name }}/bin
319 363
 
320 364
       - name: Build package for ${{ matrix.job.name }} arch

+ 30 - 0
Cargo.lock

@@ -197,6 +197,9 @@ name = "bytes"
197 197
 version = "1.2.0"
198 198
 source = "registry+https://github.com/rust-lang/crates.io-index"
199 199
 checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e"
200
+dependencies = [
201
+ "serde",
202
+]
200 203
 
201 204
 [[package]]
202 205
 name = "cc"
@@ -458,6 +461,18 @@ version = "0.3.0"
458 461
 source = "registry+https://github.com/rust-lang/crates.io-index"
459 462
 checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
460 463
 
464
+[[package]]
465
+name = "dns-lookup"
466
+version = "1.0.8"
467
+source = "registry+https://github.com/rust-lang/crates.io-index"
468
+checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872"
469
+dependencies = [
470
+ "cfg-if",
471
+ "libc",
472
+ "socket2 0.4.4",
473
+ "winapi",
474
+]
475
+
461 476
 [[package]]
462 477
 name = "dotenv"
463 478
 version = "0.15.0"
@@ -738,6 +753,7 @@ version = "0.1.0"
738 753
 dependencies = [
739 754
  "anyhow",
740 755
  "bytes",
756
+ "chrono",
741 757
  "confy",
742 758
  "directories-next",
743 759
  "dirs-next",
@@ -748,6 +764,7 @@ dependencies = [
748 764
  "lazy_static",
749 765
  "log",
750 766
  "mac_address",
767
+ "machine-uid",
751 768
  "protobuf",
752 769
  "protobuf-codegen",
753 770
  "quinn",
@@ -778,6 +795,7 @@ dependencies = [
778 795
  "chrono",
779 796
  "clap",
780 797
  "deadpool",
798
+ "dns-lookup",
781 799
  "flexi_logger",
782 800
  "hbb_common",
783 801
  "headers",
@@ -790,6 +808,7 @@ dependencies = [
790 808
  "machine-uid",
791 809
  "minreq",
792 810
  "once_cell",
811
+ "ping",
793 812
  "regex",
794 813
  "rust-ini",
795 814
  "serde",
@@ -1440,6 +1459,17 @@ version = "0.1.0"
1440 1459
 source = "registry+https://github.com/rust-lang/crates.io-index"
1441 1460
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
1442 1461
 
1462
+[[package]]
1463
+name = "ping"
1464
+version = "0.4.0"
1465
+source = "registry+https://github.com/rust-lang/crates.io-index"
1466
+checksum = "69044d1c00894fc1f43d9485aadb6ab6e68df90608fa52cf1074cda6420c6b76"
1467
+dependencies = [
1468
+ "rand",
1469
+ "socket2 0.4.4",
1470
+ "thiserror",
1471
+]
1472
+
1443 1473
 [[package]]
1444 1474
 name = "pkg-config"
1445 1475
 version = "0.3.25"

+ 2 - 0
Cargo.toml

@@ -49,6 +49,8 @@ http = "0.2"
49 49
 flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
50 50
 ipnetwork = "0.20"
51 51
 local-ip-address = "0.4"
52
+dns-lookup = "1.0.8"
53
+ping = "0.4.0"
52 54
 
53 55
 [build-dependencies]
54 56
 hbb_common = { path = "libs/hbb_common" }

+ 28 - 2
README.md

@@ -48,6 +48,8 @@ docker run --name hbbr --net=host -v "$PWD/data:/root" -d rustdesk/rustdesk-serv
48 48
 
49 49
 or without --net=host, but P2P direct connection can not work.
50 50
 
51
+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`.
52
+
51 53
 ```bash
52 54
 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]> 
53 55
 docker run --name hbbr -p 21117:21117 -p 21119:21119 -v "$PWD/data:/root" -d rustdesk/rustdesk-server:latest hbbr 
@@ -171,13 +173,12 @@ services:
171 173
     restart: unless-stopped
172 174
 ```
173 175
 
174
-We use these environment variables:
176
+For this container image, you can use these environment variables, **in addition** to the ones specified in the following **ENV variables** section:
175 177
 
176 178
 | variable | optional | description |
177 179
 | --- | --- | --- |
178 180
 | RELAY | no | the IP address/DNS name of the machine running this container |
179 181
 | ENCRYPTED_ONLY | yes | if set to **"1"** unencrypted connection will not be accepted |
180
-| DB_URL | yes | path for database file |
181 182
 | KEY_PUB | yes | public part of the key pair |
182 183
 | KEY_PRIV | yes | private part of the key pair |
183 184
 
@@ -313,3 +314,28 @@ These packages are meant for the following distributions:
313 314
 - Ubuntu 18.04 LTS
314 315
 - Debian 11 bullseye
315 316
 - Debian 10 buster
317
+
318
+## ENV variables
319
+
320
+hbbs and hbbr can be configured using these ENV variables.
321
+You can specify the variables as usual or use an `.env` file.
322
+
323
+| variable | binary | description |
324
+| --- | --- | --- |
325
+| ALWAYS_USE_RELAY | hbbs | if set to **"Y"** disallows direct peer connection |
326
+| DB_URL | hbbs | path for database file |
327
+| DOWNGRADE_START_CHECK | hbbr | delay (in seconds) before downgrade check |
328
+| DOWNGRADE_THRESHOLD | hbbr | threshold of downgrade check (bit/ms) |
329
+| KEY | hbbs/hbbr | if set force the use of a specific key, if set to **"_"** force the use of any key |
330
+| LIMIT_SPEED | hbbr | speed limit (in Mb/s) |
331
+| LOCAL_IP | hbbs | hbbs local IP address used together with MASK for solving relay failure between LAN and WAN |
332
+| MASK | hbbs | network+mask of LAN IPs |
333
+| PORT | hbbs/hbbr | listening port (21116 for hbbs - 21117 for hbbr) |
334
+| RELAY_SERVERS | hbbs | IP address/DNS name of the machines running hbbr (separated by comma) |
335
+| RENDEZVOUS_SERVERS | hbbs | IP address/DNS name of the machines running hbbs (separated by comma) |
336
+| RMEM | hbbs | UDP recv buffer size |
337
+| RUST_LOG | all | set debug level (error|warn|info|debug|trace) |
338
+| SINGLE_BANDWIDTH | hbbr | max bandwidth for a single connection (in Mb/s) |
339
+| SOFTWARE_URL hbbs | hbbs | download url of RustDesk newest version |
340
+| TEST_HBBS | hbbs | IP address of hbbs for avoiding udp socket failure error |
341
+| TOTAL_BANDWIDTH | hbbr | max total bandwidth (in Mb/s) |

+ 2 - 2
docker/rootfs/etc/s6-overlay/s6-rc.d/hbbr/run

@@ -1,3 +1,3 @@
1
-#!/command/execlineb -P
2
-posix-cd /data
1
+#!/command/with-contenv sh
2
+cd /data
3 3
 /usr/bin/hbbr

+ 5 - 2
libs/hbb_common/Cargo.toml

@@ -11,7 +11,7 @@ protobuf = { version = "3.1", features = ["with-bytes"] }
11 11
 tokio = { version = "1.20", features = ["full"] }
12 12
 tokio-util = { version = "0.7", features = ["full"] }
13 13
 futures = "0.3"
14
-bytes = "1.2"
14
+bytes = { version = "1.2", features = ["serde"] }
15 15
 log = "0.4"
16 16
 env_logger = "0.9"
17 17
 socket2 = { version = "0.3", features = ["reuseport"] }
@@ -30,15 +30,18 @@ filetime = "0.2"
30 30
 sodiumoxide = "0.2"
31 31
 regex = "1.4"
32 32
 tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
33
+chrono = "0.4"
33 34
 
34 35
 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
35 36
 mac_address = "1.1"
37
+machine-uid = "0.2"
36 38
 
37 39
 [features]
38 40
 quic = []
41
+flatpak = []
39 42
 
40 43
 [build-dependencies]
41
-protobuf-codegen = "3.1"
44
+protobuf-codegen = { version = "3.1" }
42 45
 
43 46
 [target.'cfg(target_os = "windows")'.dependencies]
44 47
 winapi = { version = "0.3", features = ["winuser"] }

+ 127 - 17
libs/hbb_common/protos/message.proto

@@ -1,13 +1,13 @@
1 1
 syntax = "proto3";
2 2
 package hbb;
3 3
 
4
-message VP9 {
4
+message EncodedVideoFrame {
5 5
   bytes data = 1;
6 6
   bool key = 2;
7 7
   int64 pts = 3;
8 8
 }
9 9
 
10
-message VP9s { repeated VP9 frames = 1; }
10
+message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
11 11
 
12 12
 message RGB { bool compress = 1; }
13 13
 
@@ -19,9 +19,11 @@ message YUV {
19 19
 
20 20
 message VideoFrame {
21 21
   oneof union {
22
-    VP9s vp9s = 6;
22
+    EncodedVideoFrames vp9s = 6;
23 23
     RGB rgb = 7;
24 24
     YUV yuv = 8;
25
+    EncodedVideoFrames h264s = 10;
26
+    EncodedVideoFrames h265s = 11;
25 27
   }
26 28
   int64 timestamp = 9;
27 29
 }
@@ -38,6 +40,7 @@ message DisplayInfo {
38 40
   int32 height = 4;
39 41
   string name = 5;
40 42
   bool online = 6;
43
+  bool cursor_embedded = 7;
41 44
 }
42 45
 
43 46
 message PortForward {
@@ -61,10 +64,21 @@ message LoginRequest {
61 64
     PortForward port_forward = 8;
62 65
   }
63 66
   bool video_ack_required = 9;
67
+  uint64 session_id = 10;
68
+  string version = 11;
64 69
 }
65 70
 
66 71
 message ChatMessage { string text = 1; }
67 72
 
73
+message Features {
74
+  bool privacy_mode = 1;
75
+}
76
+
77
+message SupportedEncoding {
78
+  bool h264 = 1;
79
+  bool h265 = 2;
80
+}
81
+
68 82
 message PeerInfo {
69 83
   string username = 1;
70 84
   string hostname = 2;
@@ -74,6 +88,8 @@ message PeerInfo {
74 88
   bool sas_enabled = 6;
75 89
   string version = 7;
76 90
   int32 conn_id = 8;
91
+  Features features = 9;
92
+  SupportedEncoding encoding = 10;
77 93
 }
78 94
 
79 95
 message LoginResponse {
@@ -90,6 +106,13 @@ message MouseEvent {
90 106
   repeated ControlKey modifiers = 4;
91 107
 }
92 108
 
109
+enum KeyboardMode{
110
+  Legacy = 0;
111
+  Map = 1;
112
+  Translate = 2;
113
+  Auto = 3;
114
+}
115
+
93 116
 enum ControlKey {
94 117
   Unknown = 0;
95 118
   Alt = 1;
@@ -183,6 +206,7 @@ message KeyEvent {
183 206
     string seq = 6;
184 207
   }
185 208
   repeated ControlKey modifiers = 8;
209
+  KeyboardMode mode = 9;
186 210
 }
187 211
 
188 212
 message CursorData {
@@ -252,6 +276,7 @@ message FileAction {
252 276
     FileRemoveFile remove_file = 6;
253 277
     ReadAllFiles all_files = 7;
254 278
     FileTransferCancel cancel = 8;
279
+    FileTransferSendConfirmRequest send_confirm = 9;
255 280
   }
256 281
 }
257 282
 
@@ -263,14 +288,24 @@ message FileResponse {
263 288
     FileTransferBlock block = 2;
264 289
     FileTransferError error = 3;
265 290
     FileTransferDone done = 4;
291
+    FileTransferDigest digest = 5;
266 292
   }
267 293
 }
268 294
 
295
+message FileTransferDigest {
296
+  int32 id = 1;
297
+  sint32 file_num = 2;
298
+  uint64 last_modified = 3;
299
+  uint64 file_size = 4;
300
+  bool is_upload = 5;
301
+}
302
+
269 303
 message FileTransferBlock {
270 304
   int32 id = 1;
271 305
   sint32 file_num = 2;
272 306
   bytes data = 3;
273 307
   bool compressed = 4;
308
+  uint32 blk_id = 5;
274 309
 }
275 310
 
276 311
 message FileTransferError {
@@ -283,6 +318,16 @@ message FileTransferSendRequest {
283 318
   int32 id = 1;
284 319
   string path = 2;
285 320
   bool include_hidden = 3;
321
+  int32 file_num = 4;
322
+}
323
+
324
+message FileTransferSendConfirmRequest {
325
+  int32 id = 1;
326
+  sint32 file_num = 2;
327
+  oneof union {
328
+    bool skip = 3;
329
+    uint32 offset_blk = 4;
330
+  }
286 331
 }
287 332
 
288 333
 message FileTransferDone {
@@ -294,6 +339,7 @@ message FileTransferReceiveRequest {
294 339
   int32 id = 1;
295 340
   string path = 2; // path written to
296 341
   repeated FileEntry files = 3;
342
+  int32 file_num = 4;
297 343
 }
298 344
 
299 345
 message FileRemoveDir {
@@ -315,38 +361,31 @@ message FileDirCreate {
315 361
 
316 362
 // main logic from freeRDP
317 363
 message CliprdrMonitorReady {
318
-  int32 conn_id = 1;
319 364
 }
320 365
 
321 366
 message CliprdrFormat {
322
-  int32 conn_id = 1;
323 367
   int32 id = 2;
324 368
   string format = 3;
325 369
 }
326 370
 
327 371
 message CliprdrServerFormatList {
328
-  int32 conn_id = 1;
329 372
   repeated CliprdrFormat formats = 2;
330 373
 }
331 374
 
332 375
 message CliprdrServerFormatListResponse {
333
-  int32 conn_id = 1;
334 376
   int32 msg_flags = 2;
335 377
 }
336 378
 
337 379
 message CliprdrServerFormatDataRequest {
338
-  int32 conn_id = 1;
339 380
   int32 requested_format_id = 2;
340 381
 }
341 382
 
342 383
 message CliprdrServerFormatDataResponse {
343
-  int32 conn_id = 1;
344 384
   int32 msg_flags = 2;
345 385
   bytes format_data = 3;
346 386
 }
347 387
 
348 388
 message CliprdrFileContentsRequest {
349
-  int32 conn_id = 1;
350 389
   int32 stream_id = 2;
351 390
   int32 list_index = 3;
352 391
   int32 dw_flags = 4;
@@ -358,7 +397,6 @@ message CliprdrFileContentsRequest {
358 397
 }
359 398
 
360 399
 message CliprdrFileContentsResponse {
361
-  int32 conn_id = 1;
362 400
   int32 msg_flags = 3;
363 401
   int32 stream_id = 4;
364 402
   bytes requested_data = 5;
@@ -382,6 +420,7 @@ message SwitchDisplay {
382 420
   sint32 y = 3;
383 421
   int32 width = 4;
384 422
   int32 height = 5;
423
+  bool cursor_embedded = 6;
385 424
 }
386 425
 
387 426
 message PermissionInfo {
@@ -390,6 +429,8 @@ message PermissionInfo {
390 429
     Clipboard = 2;
391 430
     Audio = 3;
392 431
     File = 4;
432
+    Restart = 5;
433
+    Recording = 6;
393 434
   }
394 435
 
395 436
   Permission permission = 1;
@@ -403,6 +444,20 @@ enum ImageQuality {
403 444
   Best = 4;
404 445
 }
405 446
 
447
+message VideoCodecState {
448
+  enum PerferCodec {
449
+    Auto = 0;
450
+    VPX = 1;
451
+    H264 = 2;
452
+    H265 = 3;
453
+  }
454
+
455
+  int32 score_vpx = 1;
456
+  int32 score_h264 = 2;
457
+  int32 score_h265 = 3;
458
+  PerferCodec perfer = 4;
459
+}
460
+
406 461
 message OptionMessage {
407 462
   enum BoolOption {
408 463
     NotSet = 0;
@@ -418,16 +473,15 @@ message OptionMessage {
418 473
   BoolOption disable_audio = 7;
419 474
   BoolOption disable_clipboard = 8;
420 475
   BoolOption enable_file_transfer = 9;
421
-}
422
-
423
-message OptionResponse {
424
-  OptionMessage opt = 1;
425
-  string error = 2;
476
+  VideoCodecState video_codec_state = 10;
477
+  int32 custom_fps = 11;
426 478
 }
427 479
 
428 480
 message TestDelay {
429 481
   int64 time = 1;
430 482
   bool from_client = 2;
483
+  uint32 last_delay = 3;
484
+  uint32 target_bitrate = 4;
431 485
 }
432 486
 
433 487
 message PublicKey {
@@ -447,6 +501,57 @@ message AudioFrame {
447 501
   int64 timestamp = 2;
448 502
 }
449 503
 
504
+// Notify peer to show message box.
505
+message MessageBox {
506
+  // Message type. Refer to flutter/lib/commom.dart/msgBox().
507
+  string msgtype = 1;
508
+  string title = 2;
509
+  // English
510
+  string text = 3;
511
+  // If not empty, msgbox provides a button to following the link.
512
+  // The link here can't be directly http url.
513
+  // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
514
+  string link = 4;
515
+}
516
+
517
+message BackNotification {
518
+  // no need to consider block input by someone else
519
+  enum BlockInputState {
520
+    BlkStateUnknown = 0;
521
+    BlkOnSucceeded = 2;
522
+    BlkOnFailed = 3;
523
+    BlkOffSucceeded = 4;
524
+    BlkOffFailed = 5;
525
+  }
526
+  enum PrivacyModeState {
527
+    PrvStateUnknown = 0;
528
+    // Privacy mode on by someone else
529
+    PrvOnByOther = 2;
530
+    // Privacy mode is not supported on the remote side
531
+    PrvNotSupported = 3;
532
+    // Privacy mode on by self
533
+    PrvOnSucceeded = 4;
534
+    // Privacy mode on by self, but denied
535
+    PrvOnFailedDenied = 5;
536
+    // Some plugins are not found
537
+    PrvOnFailedPlugin = 6;
538
+    // Privacy mode on by self, but failed
539
+    PrvOnFailed = 7;
540
+    // Privacy mode off by self
541
+    PrvOffSucceeded = 8;
542
+    // Ctrl + P
543
+    PrvOffByPeer = 9;
544
+    // Privacy mode off by self, but failed
545
+    PrvOffFailed = 10;
546
+    PrvOffUnknown = 11;
547
+  }
548
+
549
+  oneof union {
550
+    PrivacyModeState privacy_mode_state = 1;
551
+    BlockInputState block_input_state = 2;
552
+  }
553
+}
554
+
450 555
 message Misc {
451 556
   oneof union {
452 557
     ChatMessage chat_message = 4;
@@ -456,8 +561,12 @@ message Misc {
456 561
     AudioFormat audio_format = 8;
457 562
     string close_reason = 9;
458 563
     bool refresh_video = 10;
459
-    OptionResponse option_response = 11;
460 564
     bool video_received = 12;
565
+    BackNotification back_notification = 13;
566
+    bool restart_remote_device = 14;
567
+    bool uac = 15;
568
+    bool foreground_window_elevated = 16;
569
+    bool stop_service = 17;
461 570
   }
462 571
 }
463 572
 
@@ -481,5 +590,6 @@ message Message {
481 590
     FileResponse file_response = 18;
482 591
     Misc misc = 19;
483 592
     Cliprdr cliprdr = 20;
593
+    MessageBox message_box = 21;
484 594
   }
485 595
 }

File diff suppressed because it is too large
+ 413 - 85
libs/hbb_common/src/config.rs


+ 311 - 28
libs/hbb_common/src/fs.rs

@@ -1,13 +1,17 @@
1
-use crate::{bail, message_proto::*, ResultType};
1
+#[cfg(windows)]
2
+use std::os::windows::prelude::*;
2 3
 use std::path::{Path, PathBuf};
4
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
+
6
+use serde_derive::{Deserialize, Serialize};
7
+use tokio::{fs::File, io::*};
8
+
9
+use crate::{bail, get_version_number, message_proto::*, ResultType, Stream};
3 10
 // https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
4 11
 use crate::{
5 12
     compress::{compress, decompress},
6 13
     config::{Config, COMPRESS_LEVEL},
7 14
 };
8
-#[cfg(windows)]
9
-use std::os::windows::prelude::*;
10
-use tokio::{fs::File, io::*};
11 15
 
12 16
 pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType<FileDirectory> {
13 17
     let mut dir = FileDirectory {
@@ -184,16 +188,63 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<F
184 188
     read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
185 189
 }
186 190
 
191
+#[inline]
192
+pub fn is_file_exists(file_path: &str) -> bool {
193
+    return Path::new(file_path).exists();
194
+}
195
+
196
+#[inline]
197
+pub fn can_enable_overwrite_detection(version: i64) -> bool {
198
+    version >= get_version_number("1.1.10")
199
+}
200
+
187 201
 #[derive(Default)]
188 202
 pub struct TransferJob {
189
-    id: i32,
190
-    path: PathBuf,
191
-    files: Vec<FileEntry>,
192
-    file_num: i32,
203
+    pub id: i32,
204
+    pub remote: String,
205
+    pub path: PathBuf,
206
+    pub show_hidden: bool,
207
+    pub is_remote: bool,
208
+    pub is_last_job: bool,
209
+    pub file_num: i32,
210
+    pub files: Vec<FileEntry>,
211
+
193 212
     file: Option<File>,
194 213
     total_size: u64,
195 214
     finished_size: u64,
196 215
     transferred: u64,
216
+    enable_overwrite_detection: bool,
217
+    file_confirmed: bool,
218
+    // indicating the last file is skipped
219
+    file_skipped: bool,
220
+    file_is_waiting: bool,
221
+    default_overwrite_strategy: Option<bool>,
222
+}
223
+
224
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
225
+pub struct TransferJobMeta {
226
+    #[serde(default)]
227
+    pub id: i32,
228
+    #[serde(default)]
229
+    pub remote: String,
230
+    #[serde(default)]
231
+    pub to: String,
232
+    #[serde(default)]
233
+    pub show_hidden: bool,
234
+    #[serde(default)]
235
+    pub file_num: i32,
236
+    #[serde(default)]
237
+    pub is_remote: bool,
238
+}
239
+
240
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
241
+pub struct RemoveJobMeta {
242
+    #[serde(default)]
243
+    pub path: String,
244
+    #[serde(default)]
245
+    pub is_remote: bool,
246
+    #[serde(default)]
247
+    pub no_confirm: bool,
197 248
 }
198 249
 
199 250
 #[inline]
@@ -219,25 +270,54 @@ fn is_compressed_file(name: &str) -> bool {
219 270
 }
220 271
 
221 272
 impl TransferJob {
222
-    pub fn new_write(id: i32, path: String, files: Vec<FileEntry>) -> Self {
273
+    pub fn new_write(
274
+        id: i32,
275
+        remote: String,
276
+        path: String,
277
+        file_num: i32,
278
+        show_hidden: bool,
279
+        is_remote: bool,
280
+        files: Vec<FileEntry>,
281
+        enable_overwrite_detection: bool,
282
+    ) -> Self {
283
+        log::info!("new write {}", path);
223 284
         let total_size = files.iter().map(|x| x.size as u64).sum();
224 285
         Self {
225 286
             id,
287
+            remote,
226 288
             path: get_path(&path),
289
+            file_num,
290
+            show_hidden,
291
+            is_remote,
227 292
             files,
228 293
             total_size,
294
+            enable_overwrite_detection,
229 295
             ..Default::default()
230 296
         }
231 297
     }
232 298
 
233
-    pub fn new_read(id: i32, path: String, include_hidden: bool) -> ResultType<Self> {
234
-        let files = get_recursive_files(&path, include_hidden)?;
299
+    pub fn new_read(
300
+        id: i32,
301
+        remote: String,
302
+        path: String,
303
+        file_num: i32,
304
+        show_hidden: bool,
305
+        is_remote: bool,
306
+        enable_overwrite_detection: bool,
307
+    ) -> ResultType<Self> {
308
+        log::info!("new read {}", path);
309
+        let files = get_recursive_files(&path, show_hidden)?;
235 310
         let total_size = files.iter().map(|x| x.size as u64).sum();
236 311
         Ok(Self {
237 312
             id,
313
+            remote,
238 314
             path: get_path(&path),
315
+            file_num,
316
+            show_hidden,
317
+            is_remote,
239 318
             files,
240 319
             total_size,
320
+            enable_overwrite_detection,
241 321
             ..Default::default()
242 322
         })
243 323
     }
@@ -302,7 +382,7 @@ impl TransferJob {
302 382
         }
303 383
     }
304 384
 
305
-    pub async fn write(&mut self, block: FileTransferBlock, raw: Option<&[u8]>) -> ResultType<()> {
385
+    pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> {
306 386
         if block.id != self.id {
307 387
             bail!("Wrong id");
308 388
         }
@@ -324,25 +404,20 @@ impl TransferJob {
324 404
             let path = format!("{}.download", get_string(&path));
325 405
             self.file = Some(File::create(&path).await?);
326 406
         }
327
-        let data = if let Some(data) = raw {
328
-            data
329
-        } else {
330
-            &block.data
331
-        };
332 407
         if block.compressed {
333
-            let tmp = decompress(data);
408
+            let tmp = decompress(&block.data);
334 409
             self.file.as_mut().unwrap().write_all(&tmp).await?;
335 410
             self.finished_size += tmp.len() as u64;
336 411
         } else {
337
-            self.file.as_mut().unwrap().write_all(data).await?;
338
-            self.finished_size += data.len() as u64;
412
+            self.file.as_mut().unwrap().write_all(&block.data).await?;
413
+            self.finished_size += block.data.len() as u64;
339 414
         }
340
-        self.transferred += data.len() as u64;
415
+        self.transferred += block.data.len() as u64;
341 416
         Ok(())
342 417
     }
343 418
 
344 419
     #[inline]
345
-    fn join(&self, name: &str) -> PathBuf {
420
+    pub fn join(&self, name: &str) -> PathBuf {
346 421
         if name.is_empty() {
347 422
             self.path.clone()
348 423
         } else {
@@ -350,7 +425,7 @@ impl TransferJob {
350 425
         }
351 426
     }
352 427
 
353
-    pub async fn read(&mut self) -> ResultType<Option<FileTransferBlock>> {
428
+    pub async fn read(&mut self, stream: &mut Stream) -> ResultType<Option<FileTransferBlock>> {
354 429
         let file_num = self.file_num as usize;
355 430
         if file_num >= self.files.len() {
356 431
             self.file.take();
@@ -361,13 +436,26 @@ impl TransferJob {
361 436
             match File::open(self.join(&name)).await {
362 437
                 Ok(file) => {
363 438
                     self.file = Some(file);
439
+                    self.file_confirmed = false;
440
+                    self.file_is_waiting = false;
364 441
                 }
365 442
                 Err(err) => {
366 443
                     self.file_num += 1;
444
+                    self.file_confirmed = false;
445
+                    self.file_is_waiting = false;
367 446
                     return Err(err.into());
368 447
                 }
369 448
             }
370 449
         }
450
+        if self.enable_overwrite_detection {
451
+            if !self.file_confirmed() {
452
+                if !self.file_is_waiting() {
453
+                    self.send_current_digest(stream).await?;
454
+                    self.set_file_is_waiting(true);
455
+                }
456
+                return Ok(None);
457
+            }
458
+        }
371 459
         const BUF_SIZE: usize = 128 * 1024;
372 460
         let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
373 461
         unsafe {
@@ -380,6 +468,8 @@ impl TransferJob {
380 468
                 Err(err) => {
381 469
                     self.file_num += 1;
382 470
                     self.file = None;
471
+                    self.file_confirmed = false;
472
+                    self.file_is_waiting = false;
383 473
                     return Err(err.into());
384 474
                 }
385 475
                 Ok(n) => {
@@ -394,6 +484,8 @@ impl TransferJob {
394 484
         if offset == 0 {
395 485
             self.file_num += 1;
396 486
             self.file = None;
487
+            self.file_confirmed = false;
488
+            self.file_is_waiting = false;
397 489
         } else {
398 490
             self.finished_size += offset as u64;
399 491
             if !is_compressed_file(name) {
@@ -413,6 +505,139 @@ impl TransferJob {
413 505
             ..Default::default()
414 506
         }))
415 507
     }
508
+
509
+    async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> {
510
+        let mut msg = Message::new();
511
+        let mut resp = FileResponse::new();
512
+        let meta = self.file.as_ref().unwrap().metadata().await?;
513
+        let last_modified = meta
514
+            .modified()?
515
+            .duration_since(SystemTime::UNIX_EPOCH)?
516
+            .as_secs();
517
+        resp.set_digest(FileTransferDigest {
518
+            id: self.id,
519
+            file_num: self.file_num,
520
+            last_modified,
521
+            file_size: meta.len(),
522
+            ..Default::default()
523
+        });
524
+        msg.set_file_response(resp);
525
+        stream.send(&msg).await?;
526
+        log::info!(
527
+            "id: {}, file_num:{}, digest message is sent. waiting for confirm. msg: {:?}",
528
+            self.id,
529
+            self.file_num,
530
+            msg
531
+        );
532
+        Ok(())
533
+    }
534
+
535
+    pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option<bool>) {
536
+        self.default_overwrite_strategy = overwrite_strategy;
537
+    }
538
+
539
+    pub fn default_overwrite_strategy(&self) -> Option<bool> {
540
+        self.default_overwrite_strategy
541
+    }
542
+
543
+    pub fn set_file_confirmed(&mut self, file_confirmed: bool) {
544
+        log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed);
545
+        self.file_confirmed = file_confirmed;
546
+        self.file_skipped = false;
547
+    }
548
+
549
+    pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) {
550
+        self.file_is_waiting = file_is_waiting;
551
+    }
552
+
553
+    #[inline]
554
+    pub fn file_is_waiting(&self) -> bool {
555
+        self.file_is_waiting
556
+    }
557
+
558
+    #[inline]
559
+    pub fn file_confirmed(&self) -> bool {
560
+        self.file_confirmed
561
+    }
562
+
563
+    /// Indicating whether the last file is skipped
564
+    #[inline]
565
+    pub fn file_skipped(&self) -> bool {
566
+        self.file_skipped
567
+    }
568
+
569
+    /// Indicating whether the whole task is skipped
570
+    #[inline]
571
+    pub fn job_skipped(&self) -> bool {
572
+        self.file_skipped() && self.files.len() == 1
573
+    }
574
+
575
+    /// Check whether the job is completed after `read` returns `None`
576
+    /// This is a helper function which gives additional lifecycle when the job reads `None`.
577
+    /// If returns `true`, it means we can delete the job automatically. `False` otherwise.
578
+    ///
579
+    /// [`Note`]
580
+    /// Conditions:
581
+    /// 1. Files are not waiting for confirmation by peers.
582
+    #[inline]
583
+    pub fn job_completed(&self) -> bool {
584
+        // has no error, Condition 2
585
+        if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) {
586
+            return true;
587
+        }
588
+        return false;
589
+    }
590
+
591
+    /// Get job error message, useful for getting status when job had finished
592
+    pub fn job_error(&self) -> Option<String> {
593
+        if self.job_skipped() {
594
+            return Some("skipped".to_string());
595
+        }
596
+        None
597
+    }
598
+
599
+    pub fn set_file_skipped(&mut self) -> bool {
600
+        log::debug!("skip file {} in job {}", self.file_num, self.id);
601
+        self.file.take();
602
+        self.set_file_confirmed(false);
603
+        self.set_file_is_waiting(false);
604
+        self.file_num += 1;
605
+        self.file_skipped = true;
606
+        true
607
+    }
608
+
609
+    pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool {
610
+        if self.file_num() != r.file_num {
611
+            log::info!("file num truncated, ignoring");
612
+        } else {
613
+            match r.union {
614
+                Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
615
+                    if s {
616
+                        self.set_file_skipped();
617
+                    } else {
618
+                        self.set_file_confirmed(true);
619
+                    }
620
+                }
621
+                Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => {
622
+                    self.set_file_confirmed(true);
623
+                }
624
+                _ => {}
625
+            }
626
+        }
627
+        true
628
+    }
629
+
630
+    #[inline]
631
+    pub fn gen_meta(&self) -> TransferJobMeta {
632
+        TransferJobMeta {
633
+            id: self.id,
634
+            remote: self.remote.to_string(),
635
+            to: self.path.to_string_lossy().to_string(),
636
+            file_num: self.file_num,
637
+            show_hidden: self.show_hidden,
638
+            is_remote: self.is_remote,
639
+        }
640
+    }
416 641
 }
417 642
 
418 643
 #[inline]
@@ -453,12 +678,22 @@ pub fn new_block(block: FileTransferBlock) -> Message {
453 678
 }
454 679
 
455 680
 #[inline]
456
-pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
681
+pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
682
+    let mut msg_out = Message::new();
683
+    let mut action = FileAction::new();
684
+    action.set_send_confirm(r);
685
+    msg_out.set_file_action(action);
686
+    msg_out
687
+}
688
+
689
+#[inline]
690
+pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message {
457 691
     let mut action = FileAction::new();
458 692
     action.set_receive(FileTransferReceiveRequest {
459 693
         id,
460 694
         path,
461 695
         files: files.into(),
696
+        file_num,
462 697
         ..Default::default()
463 698
     });
464 699
     let mut msg_out = Message::new();
@@ -467,12 +702,14 @@ pub fn new_receive(id: i32, path: String, files: Vec<FileEntry>) -> Message {
467 702
 }
468 703
 
469 704
 #[inline]
470
-pub fn new_send(id: i32, path: String, include_hidden: bool) -> Message {
705
+pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message {
706
+    log::info!("new send: {},id : {}", path, id);
471 707
     let mut action = FileAction::new();
472 708
     action.set_send(FileTransferSendRequest {
473 709
         id,
474 710
         path,
475 711
         include_hidden,
712
+        file_num,
476 713
         ..Default::default()
477 714
     });
478 715
     let mut msg_out = Message::new();
@@ -509,7 +746,10 @@ pub async fn handle_read_jobs(
509 746
 ) -> ResultType<()> {
510 747
     let mut finished = Vec::new();
511 748
     for job in jobs.iter_mut() {
512
-        match job.read().await {
749
+        if job.is_last_job {
750
+            continue;
751
+        }
752
+        match job.read(stream).await {
513 753
             Err(err) => {
514 754
                 stream
515 755
                     .send(&new_error(job.id(), err, job.file_num()))
@@ -519,8 +759,19 @@ pub async fn handle_read_jobs(
519 759
                 stream.send(&new_block(block)).await?;
520 760
             }
521 761
             Ok(None) => {
522
-                finished.push(job.id());
523
-                stream.send(&new_done(job.id(), job.file_num())).await?;
762
+                if job.job_completed() {
763
+                    finished.push(job.id());
764
+                    let err = job.job_error();
765
+                    if err.is_some() {
766
+                        stream
767
+                            .send(&new_error(job.id(), err.unwrap(), job.file_num()))
768
+                            .await?;
769
+                    } else {
770
+                        stream.send(&new_done(job.id(), job.file_num())).await?;
771
+                    }
772
+                } else {
773
+                    // waiting confirmation.
774
+                }
524 775
             }
525 776
         }
526 777
     }
@@ -565,3 +816,35 @@ pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
565 816
         entry.name = entry.name.replace("\\", "/");
566 817
     }
567 818
 }
819
+
820
+pub enum DigestCheckResult {
821
+    IsSame,
822
+    NeedConfirm(FileTransferDigest),
823
+    NoSuchFile,
824
+}
825
+
826
+#[inline]
827
+pub fn is_write_need_confirmation(
828
+    file_path: &str,
829
+    digest: &FileTransferDigest,
830
+) -> ResultType<DigestCheckResult> {
831
+    let path = Path::new(file_path);
832
+    if path.exists() && path.is_file() {
833
+        let metadata = std::fs::metadata(path)?;
834
+        let modified_time = metadata.modified()?;
835
+        let remote_mt = Duration::from_secs(digest.last_modified);
836
+        let local_mt = modified_time.duration_since(UNIX_EPOCH)?;
837
+        if remote_mt == local_mt && digest.file_size == metadata.len() {
838
+            return Ok(DigestCheckResult::IsSame);
839
+        }
840
+        Ok(DigestCheckResult::NeedConfirm(FileTransferDigest {
841
+            id: digest.id,
842
+            file_num: digest.file_num,
843
+            last_modified: local_mt.as_secs(),
844
+            file_size: metadata.len(),
845
+            ..Default::default()
846
+        }))
847
+    } else {
848
+        Ok(DigestCheckResult::NoSuchFile)
849
+    }
850
+}

+ 142 - 10
libs/hbb_common/src/lib.rs

@@ -1,15 +1,16 @@
1 1
 pub mod compress;
2
-#[path = "./protos/message.rs"]
3
-pub mod message_proto;
4
-#[path = "./protos/rendezvous.rs"]
5
-pub mod rendezvous_proto;
2
+pub mod platform;
3
+pub mod protos;
6 4
 pub use bytes;
5
+use config::Config;
7 6
 pub use futures;
8 7
 pub use protobuf;
8
+pub use protos::message as message_proto;
9
+pub use protos::rendezvous as rendezvous_proto;
9 10
 use std::{
10 11
     fs::File,
11 12
     io::{self, BufRead},
12
-    net::{Ipv4Addr, SocketAddr, SocketAddrV4},
13
+    net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
13 14
     path::Path,
14 15
     time::{self, SystemTime, UNIX_EPOCH},
15 16
 };
@@ -27,6 +28,7 @@ pub use anyhow::{self, bail};
27 28
 pub use futures_util;
28 29
 pub mod config;
29 30
 pub mod fs;
31
+pub use lazy_static;
30 32
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
31 33
 pub use mac_address;
32 34
 pub use rand;
@@ -35,6 +37,9 @@ pub use sodiumoxide;
35 37
 pub use tokio_socks;
36 38
 pub use tokio_socks::IntoTargetAddr;
37 39
 pub use tokio_socks::TargetAddr;
40
+pub mod password_security;
41
+pub use chrono;
42
+pub use directories_next;
38 43
 
39 44
 #[cfg(feature = "quic")]
40 45
 pub type Stream = quic::Connection;
@@ -61,6 +66,21 @@ macro_rules! allow_err {
61 66
         } else {
62 67
         }
63 68
     };
69
+
70
+    ($e:expr, $($arg:tt)*) => {
71
+        if let Err(err) = $e {
72
+            log::debug!(
73
+                "{:?}, {}, {}:{}:{}:{}",
74
+                err,
75
+                format_args!($($arg)*),
76
+                module_path!(),
77
+                file!(),
78
+                line!(),
79
+                column!()
80
+            );
81
+        } else {
82
+        }
83
+    };
64 84
 }
65 85
 
66 86
 #[inline]
@@ -97,13 +117,31 @@ impl AddrMangle {
97 117
                 }
98 118
                 bytes[..(16 - n_padding)].to_vec()
99 119
             }
100
-            _ => {
101
-                panic!("Only support ipv4");
120
+            SocketAddr::V6(addr_v6) => {
121
+                let mut x = addr_v6.ip().octets().to_vec();
122
+                let port: [u8; 2] = addr_v6.port().to_le_bytes();
123
+                x.push(port[0]);
124
+                x.push(port[1]);
125
+                x
102 126
             }
103 127
         }
104 128
     }
105 129
 
106 130
     pub fn decode(bytes: &[u8]) -> SocketAddr {
131
+        if bytes.len() > 16 {
132
+            if bytes.len() != 18 {
133
+                return Config::get_any_listen_addr(false);
134
+            }
135
+            #[allow(invalid_value)]
136
+            let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
137
+            tmp.copy_from_slice(&bytes[16..]);
138
+            let port = u16::from_le_bytes(tmp);
139
+            #[allow(invalid_value)]
140
+            let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() };
141
+            tmp.copy_from_slice(&bytes[..16]);
142
+            let ip = std::net::Ipv6Addr::from(tmp);
143
+            return SocketAddr::new(IpAddr::V6(ip), port);
144
+        }
107 145
         let mut padded = [0u8; 16];
108 146
         padded[..bytes.len()].copy_from_slice(&bytes);
109 147
         let number = u128::from_le_bytes(padded);
@@ -156,19 +194,23 @@ pub fn get_version_from_url(url: &str) -> String {
156 194
 }
157 195
 
158 196
 pub fn gen_version() {
197
+    use std::io::prelude::*;
159 198
     let mut file = File::create("./src/version.rs").unwrap();
160 199
     for line in read_lines("Cargo.toml").unwrap() {
161 200
         if let Ok(line) = line {
162 201
             let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect();
163 202
             if ab.len() == 2 && ab[0] == "version" {
164
-                use std::io::prelude::*;
165
-                file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes())
203
+                file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes())
166 204
                     .ok();
167
-                file.sync_all().ok();
168 205
                 break;
169 206
             }
170 207
         }
171 208
     }
209
+    // generate build date
210
+    let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
211
+    file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes())
212
+        .ok();
213
+    file.sync_all().ok();
172 214
 }
173 215
 
174 216
 fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
@@ -199,6 +241,40 @@ pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
199 241
         .unwrap_or(UNIX_EPOCH)
200 242
 }
201 243
 
244
+pub fn get_created_time(path: &std::path::Path) -> SystemTime {
245
+    std::fs::metadata(&path)
246
+        .map(|m| m.created().unwrap_or(UNIX_EPOCH))
247
+        .unwrap_or(UNIX_EPOCH)
248
+}
249
+
250
+pub fn get_exe_time() -> SystemTime {
251
+    std::env::current_exe().map_or(UNIX_EPOCH, |path| {
252
+        let m = get_modified_time(&path);
253
+        let c = get_created_time(&path);
254
+        if m > c {
255
+            m
256
+        } else {
257
+            c
258
+        }
259
+    })
260
+}
261
+
262
+pub fn get_uuid() -> Vec<u8> {
263
+    #[cfg(not(any(target_os = "android", target_os = "ios")))]
264
+    if let Ok(id) = machine_uid::get() {
265
+        return id.into();
266
+    }
267
+    Config::get_key_pair().1
268
+}
269
+
270
+#[inline]
271
+pub fn get_time() -> i64 {
272
+    std::time::SystemTime::now()
273
+        .duration_since(std::time::UNIX_EPOCH)
274
+        .map(|d| d.as_millis())
275
+        .unwrap_or(0) as _
276
+}
277
+
202 278
 #[cfg(test)]
203 279
 mod tests {
204 280
     use super::*;
@@ -206,5 +282,61 @@ mod tests {
206 282
     fn test_mangle() {
207 283
         let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116));
208 284
         assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
285
+
286
+        let addr = "[2001:db8::1]:8080".parse::<SocketAddr>().unwrap();
287
+        assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
288
+
289
+        let addr = "[2001:db8:ff::1111]:80".parse::<SocketAddr>().unwrap();
290
+        assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
291
+    }
292
+
293
+    #[test]
294
+    fn test_allow_err() {
295
+        allow_err!(Err("test err") as Result<(), &str>);
296
+        allow_err!(
297
+            Err("test err with msg") as Result<(), &str>,
298
+            "prompt {}",
299
+            "failed"
300
+        );
301
+    }
302
+}
303
+
304
+#[inline]
305
+pub fn is_ipv4_str(id: &str) -> bool {
306
+    regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$")
307
+        .unwrap()
308
+        .is_match(id)
309
+}
310
+
311
+#[inline]
312
+pub fn is_ipv6_str(id: &str) -> bool {
313
+    regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$")
314
+        .unwrap()
315
+        .is_match(id)
316
+}
317
+
318
+#[inline]
319
+pub fn is_ip_str(id: &str) -> bool {
320
+    is_ipv4_str(id) || is_ipv6_str(id)
321
+}
322
+
323
+#[cfg(test)]
324
+mod test_lib {
325
+    use super::*;
326
+
327
+    #[test]
328
+    fn test_ipv6() {
329
+        assert_eq!(is_ipv6_str("1:2:3"), true);
330
+        assert_eq!(is_ipv6_str("[ab:2:3]:12"), true);
331
+        assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true);
332
+        assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false);
333
+        assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false);
334
+        assert_eq!(is_ipv6_str("1.1.1.1"), false);
335
+        assert_eq!(is_ip_str("1.1.1.1"), true);
336
+        assert_eq!(is_ipv6_str("1:2:"), false);
337
+        assert_eq!(is_ipv6_str("1:2::0"), true);
338
+        assert_eq!(is_ipv6_str("[1:2::0]:1"), true);
339
+        assert_eq!(is_ipv6_str("[1:2::0]:"), false);
340
+        assert_eq!(is_ipv6_str("1:2::0]:1"), false);
209 341
     }
210 342
 }

+ 242 - 0
libs/hbb_common/src/password_security.rs

@@ -0,0 +1,242 @@
1
+use crate::config::Config;
2
+use sodiumoxide::base64;
3
+use std::sync::{Arc, RwLock};
4
+
5
+lazy_static::lazy_static! {
6
+    pub static ref TEMPORARY_PASSWORD:Arc<RwLock<String>> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length())));
7
+}
8
+
9
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10
+enum VerificationMethod {
11
+    OnlyUseTemporaryPassword,
12
+    OnlyUsePermanentPassword,
13
+    UseBothPasswords,
14
+}
15
+
16
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
+pub enum ApproveMode {
18
+    Both,
19
+    Password,
20
+    Click,
21
+}
22
+
23
+// Should only be called in server
24
+pub fn update_temporary_password() {
25
+    *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length());
26
+}
27
+
28
+// Should only be called in server
29
+pub fn temporary_password() -> String {
30
+    TEMPORARY_PASSWORD.read().unwrap().clone()
31
+}
32
+
33
+fn verification_method() -> VerificationMethod {
34
+    let method = Config::get_option("verification-method");
35
+    if method == "use-temporary-password" {
36
+        VerificationMethod::OnlyUseTemporaryPassword
37
+    } else if method == "use-permanent-password" {
38
+        VerificationMethod::OnlyUsePermanentPassword
39
+    } else {
40
+        VerificationMethod::UseBothPasswords // default
41
+    }
42
+}
43
+
44
+pub fn temporary_password_length() -> usize {
45
+    let length = Config::get_option("temporary-password-length");
46
+    if length == "8" {
47
+        8
48
+    } else if length == "10" {
49
+        10
50
+    } else {
51
+        6 // default
52
+    }
53
+}
54
+
55
+pub fn temporary_enabled() -> bool {
56
+    verification_method() != VerificationMethod::OnlyUsePermanentPassword
57
+}
58
+
59
+pub fn permanent_enabled() -> bool {
60
+    verification_method() != VerificationMethod::OnlyUseTemporaryPassword
61
+}
62
+
63
+pub fn has_valid_password() -> bool {
64
+    temporary_enabled() && !temporary_password().is_empty()
65
+        || permanent_enabled() && !Config::get_permanent_password().is_empty()
66
+}
67
+
68
+pub fn approve_mode() -> ApproveMode {
69
+    let mode = Config::get_option("approve-mode");
70
+    if mode == "password" {
71
+        ApproveMode::Password
72
+    } else if mode == "click" {
73
+        ApproveMode::Click
74
+    } else {
75
+        ApproveMode::Both
76
+    }
77
+}
78
+
79
+pub fn hide_cm() -> bool {
80
+    approve_mode() == ApproveMode::Password
81
+        && verification_method() == VerificationMethod::OnlyUsePermanentPassword
82
+        && !Config::get_option("allow-hide-cm").is_empty()
83
+}
84
+
85
+const VERSION_LEN: usize = 2;
86
+
87
+pub fn encrypt_str_or_original(s: &str, version: &str) -> String {
88
+    if decrypt_str_or_original(s, version).1 {
89
+        log::error!("Duplicate encryption!");
90
+        return s.to_owned();
91
+    }
92
+    if version == "00" {
93
+        if let Ok(s) = encrypt(s.as_bytes()) {
94
+            return version.to_owned() + &s;
95
+        }
96
+    }
97
+    s.to_owned()
98
+}
99
+
100
+// String: password
101
+// bool: whether decryption is successful
102
+// bool: whether should store to re-encrypt when load
103
+pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
104
+    if s.len() > VERSION_LEN {
105
+        let version = &s[..VERSION_LEN];
106
+        if version == "00" {
107
+            if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) {
108
+                return (
109
+                    String::from_utf8_lossy(&v).to_string(),
110
+                    true,
111
+                    version != current_version,
112
+                );
113
+            }
114
+        }
115
+    }
116
+
117
+    (s.to_owned(), false, !s.is_empty())
118
+}
119
+
120
+pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec<u8> {
121
+    if decrypt_vec_or_original(v, version).1 {
122
+        log::error!("Duplicate encryption!");
123
+        return v.to_owned();
124
+    }
125
+    if version == "00" {
126
+        if let Ok(s) = encrypt(v) {
127
+            let mut version = version.to_owned().into_bytes();
128
+            version.append(&mut s.into_bytes());
129
+            return version;
130
+        }
131
+    }
132
+    v.to_owned()
133
+}
134
+
135
+// Vec<u8>: password
136
+// bool: whether decryption is successful
137
+// bool: whether should store to re-encrypt when load
138
+pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, bool, bool) {
139
+    if v.len() > VERSION_LEN {
140
+        let version = String::from_utf8_lossy(&v[..VERSION_LEN]);
141
+        if version == "00" {
142
+            if let Ok(v) = decrypt(&v[VERSION_LEN..]) {
143
+                return (v, true, version != current_version);
144
+            }
145
+        }
146
+    }
147
+
148
+    (v.to_owned(), false, !v.is_empty())
149
+}
150
+
151
+fn encrypt(v: &[u8]) -> Result<String, ()> {
152
+    if v.len() > 0 {
153
+        symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
154
+    } else {
155
+        Err(())
156
+    }
157
+}
158
+
159
+fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
160
+    if v.len() > 0 {
161
+        base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
162
+    } else {
163
+        Err(())
164
+    }
165
+}
166
+
167
+fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
168
+    use sodiumoxide::crypto::secretbox;
169
+    use std::convert::TryInto;
170
+
171
+    let mut keybuf = crate::get_uuid();
172
+    keybuf.resize(secretbox::KEYBYTES, 0);
173
+    let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
174
+    let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
175
+
176
+    if encrypt {
177
+        Ok(secretbox::seal(data, &nonce, &key))
178
+    } else {
179
+        secretbox::open(data, &nonce, &key)
180
+    }
181
+}
182
+
183
+mod test {
184
+
185
+    #[test]
186
+    fn test() {
187
+        use super::*;
188
+
189
+        let version = "00";
190
+
191
+        println!("test str");
192
+        let data = "Hello World";
193
+        let encrypted = encrypt_str_or_original(data, version);
194
+        let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
195
+        println!("data: {}", data);
196
+        println!("encrypted: {}", encrypted);
197
+        println!("decrypted: {}", decrypted);
198
+        assert_eq!(data, decrypted);
199
+        assert_eq!(version, &encrypted[..2]);
200
+        assert_eq!(succ, true);
201
+        assert_eq!(store, false);
202
+        let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
203
+        assert_eq!(store, true);
204
+        assert_eq!(decrypt_str_or_original(&decrypted, version).1, false);
205
+        assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted);
206
+
207
+        println!("test vec");
208
+        let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6];
209
+        let encrypted = encrypt_vec_or_original(&data, version);
210
+        let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
211
+        println!("data: {:?}", data);
212
+        println!("encrypted: {:?}", encrypted);
213
+        println!("decrypted: {:?}", decrypted);
214
+        assert_eq!(data, decrypted);
215
+        assert_eq!(version.as_bytes(), &encrypted[..2]);
216
+        assert_eq!(store, false);
217
+        assert_eq!(succ, true);
218
+        let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
219
+        assert_eq!(store, true);
220
+        assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false);
221
+        assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted);
222
+
223
+        println!("test original");
224
+        let data = version.to_string() + "Hello World";
225
+        let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
226
+        assert_eq!(data, decrypted);
227
+        assert_eq!(store, true);
228
+        assert_eq!(succ, false);
229
+        let verbytes = version.as_bytes();
230
+        let data: Vec<u8> = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6];
231
+        let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
232
+        assert_eq!(data, decrypted);
233
+        assert_eq!(store, true);
234
+        assert_eq!(succ, false);
235
+        let (_, succ, store) = decrypt_str_or_original("", version);
236
+        assert_eq!(store, false);
237
+        assert_eq!(succ, false);
238
+        let (_, succ, store) = decrypt_vec_or_original(&vec![], version);
239
+        assert_eq!(store, false);
240
+        assert_eq!(succ, false);
241
+    }
242
+}

+ 157 - 0
libs/hbb_common/src/platform/linux.rs

@@ -0,0 +1,157 @@
1
+use crate::ResultType;
2
+
3
+lazy_static::lazy_static! {
4
+    pub static ref DISTRO: Disto = Disto::new();
5
+}
6
+
7
+pub struct Disto {
8
+    pub name: String,
9
+    pub version_id: String,
10
+}
11
+
12
+impl Disto {
13
+    fn new() -> Self {
14
+        let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned())
15
+            .unwrap_or_default()
16
+            .trim()
17
+            .trim_matches('"')
18
+            .to_string();
19
+        let version_id =
20
+            run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned())
21
+                .unwrap_or_default()
22
+                .trim()
23
+                .trim_matches('"')
24
+                .to_string();
25
+        Self { name, version_id }
26
+    }
27
+}
28
+
29
+pub fn get_display_server() -> String {
30
+    let mut session = get_values_of_seat0([0].to_vec())[0].clone();
31
+    if session.is_empty() {
32
+        // loginctl has not given the expected output.  try something else.
33
+        if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
34
+            // could also execute "cat /proc/self/sessionid"
35
+            session = sid.to_owned();
36
+        }
37
+        if session.is_empty() {
38
+            session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default();
39
+        }
40
+    }
41
+
42
+    get_display_server_of_session(&session)
43
+}
44
+
45
+fn get_display_server_of_session(session: &str) -> String {
46
+    let mut display_server = if let Ok(output) =
47
+        run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
48
+    // Check session type of the session
49
+    {
50
+        let display_server = String::from_utf8_lossy(&output.stdout)
51
+            .replace("Type=", "")
52
+            .trim_end()
53
+            .into();
54
+        if display_server == "tty" {
55
+            // If the type is tty...
56
+            if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session]))
57
+            // Get the tty number
58
+            {
59
+                let tty: String = String::from_utf8_lossy(&output.stdout)
60
+                    .replace("TTY=", "")
61
+                    .trim_end()
62
+                    .into();
63
+                if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty))
64
+                // And check if Xorg is running on that tty
65
+                {
66
+                    if xorg_results.trim_end().to_string() != "" {
67
+                        // If it is, manually return "x11", otherwise return tty
68
+                        return "x11".to_owned();
69
+                    }
70
+                }
71
+            }
72
+        }
73
+        display_server
74
+    } else {
75
+        "".to_owned()
76
+    };
77
+    if display_server.is_empty() {
78
+        // loginctl has not given the expected output.  try something else.
79
+        if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
80
+            display_server = sestype;
81
+        }
82
+    }
83
+    // If the session is not a tty, then just return the type as usual
84
+    display_server
85
+}
86
+
87
+pub fn get_values_of_seat0(indices: Vec<usize>) -> Vec<String> {
88
+    if let Ok(output) = run_loginctl(None) {
89
+        for line in String::from_utf8_lossy(&output.stdout).lines() {
90
+            if line.contains("seat0") {
91
+                if let Some(sid) = line.split_whitespace().nth(0) {
92
+                    if is_active(sid) {
93
+                        return indices
94
+                            .into_iter()
95
+                            .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
96
+                            .collect::<Vec<String>>();
97
+                    }
98
+                }
99
+            }
100
+        }
101
+    }
102
+
103
+    // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
104
+    if let Ok(output) = run_loginctl(None) {
105
+        for line in String::from_utf8_lossy(&output.stdout).lines() {
106
+            if let Some(sid) = line.split_whitespace().nth(0) {
107
+                let d = get_display_server_of_session(sid);
108
+                if is_active(sid) && d != "tty" {
109
+                    return indices
110
+                        .into_iter()
111
+                        .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned())
112
+                        .collect::<Vec<String>>();
113
+                }
114
+            }
115
+        }
116
+    }
117
+
118
+    return indices
119
+        .iter()
120
+        .map(|_x| "".to_owned())
121
+        .collect::<Vec<String>>();
122
+}
123
+
124
+fn is_active(sid: &str) -> bool {
125
+    if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
126
+        String::from_utf8_lossy(&output.stdout).contains("active")
127
+    } else {
128
+        false
129
+    }
130
+}
131
+
132
+pub fn run_cmds(cmds: String) -> ResultType<String> {
133
+    let output = std::process::Command::new("sh")
134
+        .args(vec!["-c", &cmds])
135
+        .output()?;
136
+    Ok(String::from_utf8_lossy(&output.stdout).to_string())
137
+}
138
+
139
+#[cfg(not(feature = "flatpak"))]
140
+fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
141
+    let mut cmd = std::process::Command::new("loginctl");
142
+    if let Some(a) = args {
143
+        return cmd.args(a).output();
144
+    }
145
+    cmd.output()
146
+}
147
+
148
+#[cfg(feature = "flatpak")]
149
+fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
150
+    let mut l_args = String::from("loginctl");
151
+    if let Some(a) = args {
152
+        l_args = format!("{} {}", l_args, a.join(" "));
153
+    }
154
+    std::process::Command::new("flatpak-spawn")
155
+        .args(vec![String::from("--host"), l_args])
156
+        .output()
157
+}

+ 2 - 0
libs/hbb_common/src/platform/mod.rs

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

+ 159 - 35
libs/hbb_common/src/socket_client.rs

@@ -9,31 +9,15 @@ use std::net::SocketAddr;
9 9
 use tokio::net::ToSocketAddrs;
10 10
 use tokio_socks::{IntoTargetAddr, TargetAddr};
11 11
 
12
-fn to_socket_addr(host: &str) -> ResultType<SocketAddr> {
13
-    use std::net::ToSocketAddrs;
14
-    host.to_socket_addrs()?
15
-        .filter(|x| x.is_ipv4())
16
-        .next()
17
-        .context("Failed to solve")
18
-}
19
-
20
-pub fn get_target_addr(host: &str) -> ResultType<TargetAddr<'static>> {
21
-    let addr = match Config::get_network_type() {
22
-        NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?,
23
-        NetworkType::ProxySocks => host.into_target_addr()?,
24
-    }
25
-    .to_owned();
26
-    Ok(addr)
27
-}
28
-
29 12
 pub fn test_if_valid_server(host: &str) -> String {
30 13
     let mut host = host.to_owned();
31 14
     if !host.contains(":") {
32 15
         host = format!("{}:{}", host, 0);
33 16
     }
34 17
 
18
+    use std::net::ToSocketAddrs;
35 19
     match Config::get_network_type() {
36
-        NetworkType::Direct => match to_socket_addr(&host) {
20
+        NetworkType::Direct => match host.to_socket_addrs() {
37 21
             Err(err) => err.to_string(),
38 22
             Ok(_) => "".to_owned(),
39 23
         },
@@ -44,33 +28,126 @@ pub fn test_if_valid_server(host: &str) -> String {
44 28
     }
45 29
 }
46 30
 
47
-pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>(
31
+pub trait IsResolvedSocketAddr {
32
+    fn resolve(&self) -> Option<&SocketAddr>;
33
+}
34
+
35
+impl IsResolvedSocketAddr for SocketAddr {
36
+    fn resolve(&self) -> Option<&SocketAddr> {
37
+        Some(&self)
38
+    }
39
+}
40
+
41
+impl IsResolvedSocketAddr for String {
42
+    fn resolve(&self) -> Option<&SocketAddr> {
43
+        None
44
+    }
45
+}
46
+
47
+impl IsResolvedSocketAddr for &str {
48
+    fn resolve(&self) -> Option<&SocketAddr> {
49
+        None
50
+    }
51
+}
52
+
53
+#[inline]
54
+pub async fn connect_tcp<
55
+    't,
56
+    T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
57
+>(
48 58
     target: T,
49
-    local: SocketAddr,
50 59
     ms_timeout: u64,
51 60
 ) -> ResultType<FramedStream> {
52
-    let target_addr = target.into_target_addr()?;
61
+    connect_tcp_local(target, None, ms_timeout).await
62
+}
53 63
 
64
+pub async fn connect_tcp_local<
65
+    't,
66
+    T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
67
+>(
68
+    target: T,
69
+    local: Option<SocketAddr>,
70
+    ms_timeout: u64,
71
+) -> ResultType<FramedStream> {
54 72
     if let Some(conf) = Config::get_socks() {
55
-        FramedStream::connect(
73
+        return FramedStream::connect(
56 74
             conf.proxy.as_str(),
57
-            target_addr,
75
+            target,
58 76
             local,
59 77
             conf.username.as_str(),
60 78
             conf.password.as_str(),
61 79
             ms_timeout,
62 80
         )
63
-        .await
64
-    } else {
65
-        let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)?
66
-            .filter(|x| x.is_ipv4())
67
-            .next()
68
-            .context("Invalid target addr, no valid ipv4 address can be resolved.")?;
69
-        Ok(FramedStream::new(addr, local, ms_timeout).await?)
81
+        .await;
70 82
     }
83
+    if let Some(target) = target.resolve() {
84
+        if let Some(local) = local {
85
+            if local.is_ipv6() && target.is_ipv4() {
86
+                let target = query_nip_io(&target).await?;
87
+                return Ok(FramedStream::new(target, Some(local), ms_timeout).await?);
88
+            }
89
+        }
90
+    }
91
+    Ok(FramedStream::new(target, local, ms_timeout).await?)
71 92
 }
72 93
 
73
-pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
94
+#[inline]
95
+pub fn is_ipv4(target: &TargetAddr<'_>) -> bool {
96
+    match target {
97
+        TargetAddr::Ip(addr) => addr.is_ipv4(),
98
+        _ => true,
99
+    }
100
+}
101
+
102
+#[inline]
103
+pub async fn query_nip_io(addr: &SocketAddr) -> ResultType<SocketAddr> {
104
+    tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port()))
105
+        .await?
106
+        .filter(|x| x.is_ipv6())
107
+        .next()
108
+        .context("Failed to get ipv6 from nip.io")
109
+}
110
+
111
+#[inline]
112
+pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String {
113
+    if !ipv4 && crate::is_ipv4_str(&addr) {
114
+        if let Some(ip) = addr.split(":").next() {
115
+            return addr.replace(ip, &format!("{}.nip.io", ip));
116
+        }
117
+    }
118
+    addr
119
+}
120
+
121
+async fn test_target(target: &str) -> ResultType<SocketAddr> {
122
+    if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await {
123
+        if let Ok(addr) = s.peer_addr() {
124
+            return Ok(addr);
125
+        }
126
+    }
127
+    tokio::net::lookup_host(target)
128
+        .await?
129
+        .next()
130
+        .context(format!("Failed to look up host for {}", target))
131
+}
132
+
133
+#[inline]
134
+pub async fn new_udp_for(
135
+    target: &str,
136
+    ms_timeout: u64,
137
+) -> ResultType<(FramedSocket, TargetAddr<'static>)> {
138
+    let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() {
139
+        let addr = test_target(target).await?;
140
+        (addr.is_ipv4(), addr.into_target_addr()?)
141
+    } else {
142
+        (true, target.into_target_addr()?)
143
+    };
144
+    Ok((
145
+        new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?,
146
+        target.to_owned(),
147
+    ))
148
+}
149
+
150
+async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
74 151
     match Config::get_socks() {
75 152
         None => Ok(FramedSocket::new(local).await?),
76 153
         Some(conf) => {
@@ -87,9 +164,56 @@ pub async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<
87 164
     }
88 165
 }
89 166
 
90
-pub async fn rebind_udp<T: ToSocketAddrs>(local: T) -> ResultType<Option<FramedSocket>> {
91
-    match Config::get_network_type() {
92
-        NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)),
93
-        _ => Ok(None),
167
+pub async fn rebind_udp_for(
168
+    target: &str,
169
+) -> ResultType<Option<(FramedSocket, TargetAddr<'static>)>> {
170
+    if Config::get_network_type() != NetworkType::Direct {
171
+        return Ok(None);
172
+    }
173
+    let addr = test_target(target).await?;
174
+    let v4 = addr.is_ipv4();
175
+    Ok(Some((
176
+        FramedSocket::new(Config::get_any_listen_addr(v4)).await?,
177
+        addr.into_target_addr()?.to_owned(),
178
+    )))
179
+}
180
+
181
+#[cfg(test)]
182
+mod tests {
183
+    use std::net::ToSocketAddrs;
184
+
185
+    use super::*;
186
+
187
+    #[test]
188
+    fn test_nat64() {
189
+        test_nat64_async();
190
+    }
191
+
192
+    #[tokio::main(flavor = "current_thread")]
193
+    async fn test_nat64_async() {
194
+        assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1");
195
+        assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io");
196
+        assert_eq!(
197
+            ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false),
198
+            "1.1.1.1.nip.io:8080"
199
+        );
200
+        assert_eq!(
201
+            ipv4_to_ipv6("rustdesk.com".to_owned(), false),
202
+            "rustdesk.com"
203
+        );
204
+        if ("rustdesk.com:80")
205
+            .to_socket_addrs()
206
+            .unwrap()
207
+            .next()
208
+            .unwrap()
209
+            .is_ipv6()
210
+        {
211
+            assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap())
212
+                .await
213
+                .unwrap()
214
+                .is_ipv6());
215
+            return;
216
+        }
217
+        assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err());
94 218
     }
95 219
 }

+ 91 - 53
libs/hbb_common/src/tcp.rs

@@ -5,7 +5,7 @@ use protobuf::Message;
5 5
 use sodiumoxide::crypto::secretbox::{self, Key, Nonce};
6 6
 use std::{
7 7
     io::{self, Error, ErrorKind},
8
-    net::SocketAddr,
8
+    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
9 9
     ops::{Deref, DerefMut},
10 10
     pin::Pin,
11 11
     task::{Context, Poll},
@@ -73,73 +73,79 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std:
73 73
 }
74 74
 
75 75
 impl FramedStream {
76
-    pub async fn new<T1: ToSocketAddrs, T2: ToSocketAddrs>(
77
-        remote_addr: T1,
78
-        local_addr: T2,
76
+    pub async fn new<T: ToSocketAddrs + std::fmt::Display>(
77
+        remote_addr: T,
78
+        local_addr: Option<SocketAddr>,
79 79
         ms_timeout: u64,
80 80
     ) -> ResultType<Self> {
81
-        for local_addr in lookup_host(&local_addr).await? {
82
-            for remote_addr in lookup_host(&remote_addr).await? {
83
-                let stream = super::timeout(
84
-                    ms_timeout,
85
-                    new_socket(local_addr, true)?.connect(remote_addr),
86
-                )
87
-                .await??;
88
-                stream.set_nodelay(true).ok();
89
-                let addr = stream.local_addr()?;
90
-                return Ok(Self(
91
-                    Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
92
-                    addr,
93
-                    None,
94
-                    0,
95
-                ));
81
+        for remote_addr in lookup_host(&remote_addr).await? {
82
+            let local = if let Some(addr) = local_addr {
83
+                addr
84
+            } else {
85
+                crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4())
86
+            };
87
+            if let Ok(socket) = new_socket(local, true) {
88
+                if let Ok(Ok(stream)) =
89
+                    super::timeout(ms_timeout, socket.connect(remote_addr)).await
90
+                {
91
+                    stream.set_nodelay(true).ok();
92
+                    let addr = stream.local_addr()?;
93
+                    return Ok(Self(
94
+                        Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
95
+                        addr,
96
+                        None,
97
+                        0,
98
+                    ));
99
+                }
96 100
             }
97 101
         }
98
-        bail!("could not resolve to any address");
102
+        bail!(format!("Failed to connect to {}", remote_addr));
99 103
     }
100 104
 
101
-    pub async fn connect<'a, 't, P, T1, T2>(
105
+    pub async fn connect<'a, 't, P, T>(
102 106
         proxy: P,
103
-        target: T1,
104
-        local: T2,
107
+        target: T,
108
+        local_addr: Option<SocketAddr>,
105 109
         username: &'a str,
106 110
         password: &'a str,
107 111
         ms_timeout: u64,
108 112
     ) -> ResultType<Self>
109 113
     where
110 114
         P: ToProxyAddrs,
111
-        T1: IntoTargetAddr<'t>,
112
-        T2: ToSocketAddrs,
115
+        T: IntoTargetAddr<'t>,
113 116
     {
114
-        if let Some(local) = lookup_host(&local).await?.next() {
115
-            if let Some(proxy) = proxy.to_proxy_addrs().next().await {
116
-                let stream =
117
-                    super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??;
118
-                stream.set_nodelay(true).ok();
119
-                let stream = if username.trim().is_empty() {
120
-                    super::timeout(
121
-                        ms_timeout,
122
-                        Socks5Stream::connect_with_socket(stream, target),
123
-                    )
124
-                    .await??
125
-                } else {
126
-                    super::timeout(
127
-                        ms_timeout,
128
-                        Socks5Stream::connect_with_password_and_socket(
129
-                            stream, target, username, password,
130
-                        ),
131
-                    )
132
-                    .await??
133
-                };
134
-                let addr = stream.local_addr()?;
135
-                return Ok(Self(
136
-                    Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
137
-                    addr,
138
-                    None,
139
-                    0,
140
-                ));
117
+        if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
118
+            let local = if let Some(addr) = local_addr {
119
+                addr
120
+            } else {
121
+                crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
122
+            };
123
+            let stream =
124
+                super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
125
+            stream.set_nodelay(true).ok();
126
+            let stream = if username.trim().is_empty() {
127
+                super::timeout(
128
+                    ms_timeout,
129
+                    Socks5Stream::connect_with_socket(stream, target),
130
+                )
131
+                .await??
132
+            } else {
133
+                super::timeout(
134
+                    ms_timeout,
135
+                    Socks5Stream::connect_with_password_and_socket(
136
+                        stream, target, username, password,
137
+                    ),
138
+                )
139
+                .await??
141 140
             };
142
-        };
141
+            let addr = stream.local_addr()?;
142
+            return Ok(Self(
143
+                Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
144
+                addr,
145
+                None,
146
+                0,
147
+            ));
148
+        }
143 149
         bail!("could not resolve to any address");
144 150
     }
145 151
 
@@ -252,6 +258,38 @@ pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<
252 258
     }
253 259
 }
254 260
 
261
+pub async fn listen_any(port: u16) -> ResultType<TcpListener> {
262
+    if let Ok(mut socket) = TcpSocket::new_v6() {
263
+        #[cfg(unix)]
264
+        {
265
+            use std::os::unix::io::{FromRawFd, IntoRawFd};
266
+            let raw_fd = socket.into_raw_fd();
267
+            let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) };
268
+            sock2.set_only_v6(false).ok();
269
+            socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) };
270
+        }
271
+        #[cfg(windows)]
272
+        {
273
+            use std::os::windows::prelude::{FromRawSocket, IntoRawSocket};
274
+            let raw_socket = socket.into_raw_socket();
275
+            let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) };
276
+            sock2.set_only_v6(false).ok();
277
+            socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) };
278
+        }
279
+        if socket
280
+            .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port))
281
+            .is_ok()
282
+        {
283
+            if let Ok(l) = socket.listen(DEFAULT_BACKLOG) {
284
+                return Ok(l);
285
+            }
286
+        }
287
+    }
288
+    let s = TcpSocket::new_v4()?;
289
+    s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
290
+    Ok(s.listen(DEFAULT_BACKLOG)?)
291
+}
292
+
255 293
 impl Unpin for DynTcpStream {}
256 294
 
257 295
 impl AsyncRead for DynTcpStream {

+ 11 - 2
libs/hbb_common/src/udp.rs

@@ -49,7 +49,7 @@ impl FramedSocket {
49 49
 
50 50
     #[allow(clippy::never_loop)]
51 51
     pub async fn new_reuse<T: std::net::ToSocketAddrs>(addr: T) -> ResultType<Self> {
52
-        for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) {
52
+        for addr in addr.to_socket_addrs()? {
53 53
             let socket = new_socket(addr, true, 0)?.into_udp_socket();
54 54
             return Ok(Self::Direct(UdpFramed::new(
55 55
                 UdpSocket::from_std(socket)?,
@@ -63,7 +63,7 @@ impl FramedSocket {
63 63
         addr: T,
64 64
         buf_size: usize,
65 65
     ) -> ResultType<Self> {
66
-        for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) {
66
+        for addr in addr.to_socket_addrs()? {
67 67
             return Ok(Self::Direct(UdpFramed::new(
68 68
                 UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?,
69 69
                 BytesCodec::new(),
@@ -164,4 +164,13 @@ impl FramedSocket {
164 164
             None
165 165
         }
166 166
     }
167
+
168
+    pub fn is_ipv4(&self) -> bool {
169
+        if let FramedSocket::Direct(x) = self {
170
+            if let Ok(v) = x.get_ref().local_addr() {
171
+                return v.is_ipv4();
172
+            }
173
+        }
174
+        true
175
+    }
167 176
 }

+ 6 - 10
src/rendezvous_server.rs

@@ -766,15 +766,11 @@ impl RendezvousServer {
766 766
     ) -> ResultType<()> {
767 767
         let mut states = BytesMut::zeroed((peers.len() + 7) / 8);
768 768
         for i in 0..peers.len() {
769
-            let peer_id = &peers[i];
770
-            // bytes index from left to right
771
-            let states_idx = i / 8;
772
-            let bit_idx = 7 - i % 8;
773
-            if let Some(peer) = self.pm.get_in_memory(&peer_id).await {
774
-                let (elapsed, _) = {
775
-                    let r = peer.read().await;
776
-                    (r.last_reg_time.elapsed().as_millis() as i32, r.socket_addr)
777
-                };
769
+            if let Some(peer) = self.pm.get_in_memory(&peers[i]).await {
770
+                let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i32;
771
+                // bytes index from left to right
772
+                let states_idx = i / 8;
773
+                let bit_idx = 7 - i % 8;
778 774
                 if elapsed < REG_TIMEOUT {
779 775
                     states[states_idx] |= 0x01 << bit_idx;
780 776
                 }
@@ -1213,7 +1209,7 @@ async fn check_relay_servers(rs0: Arc<RelayServers>, tx: Sender) {
1213 1209
         let rs = rs.clone();
1214 1210
         let x = x.clone();
1215 1211
         futs.push(tokio::spawn(async move {
1216
-            if FramedStream::new(&host, "0.0.0.0:0", CHECK_RELAY_TIMEOUT)
1212
+            if FramedStream::new(&host, None, CHECK_RELAY_TIMEOUT)
1217 1213
                 .await
1218 1214
                 .is_ok()
1219 1215
             {

+ 84 - 4
src/utils.rs

@@ -1,8 +1,11 @@
1
+use dns_lookup::{lookup_addr, lookup_host};
1 2
 use hbb_common::{bail, ResultType};
2 3
 use sodiumoxide::crypto::sign;
3
-use std::env;
4
-use std::process;
5
-use std::str;
4
+use std::{
5
+    env,
6
+    net::{IpAddr, TcpStream},
7
+    process, str,
8
+};
6 9
 
7 10
 fn print_help() {
8 11
     println!(
@@ -10,7 +13,8 @@ fn print_help() {
10 13
     rustdesk-util [command]\n
11 14
 Available Commands:
12 15
     genkeypair                                   Generate a new keypair
13
-    validatekeypair [public key] [secret key]    Validate an existing keypair"
16
+    validatekeypair [public key] [secret key]    Validate an existing keypair
17
+    doctor [rustdesk-server]                     Check for server connection problems"
14 18
     );
15 19
     process::exit(0x0001);
16 20
 }
@@ -68,6 +72,76 @@ fn validate_keypair(pk: &str, sk: &str) -> ResultType<()> {
68 72
     Ok(())
69 73
 }
70 74
 
75
+fn doctor_tcp(address: std::net::IpAddr, port: &str, desc: &str) {
76
+    let start = std::time::Instant::now();
77
+    let conn = format!("{}:{}", address, port);
78
+    if let Ok(_stream) = TcpStream::connect(conn.as_str()) {
79
+        let elapsed = std::time::Instant::now().duration_since(start);
80
+        println!(
81
+            "TCP Port {} ({}): OK in {} ms",
82
+            port,
83
+            desc,
84
+            elapsed.as_millis()
85
+        );
86
+    } else {
87
+        println!("TCP Port {} ({}): ERROR", port, desc);
88
+    }
89
+}
90
+
91
+fn doctor_ip(server_ip_address: std::net::IpAddr, server_address: Option<&str>) {
92
+    println!("\nChecking IP address: {}", server_ip_address);
93
+    println!("Is IPV4: {}", server_ip_address.is_ipv4());
94
+    println!("Is IPV6: {}", server_ip_address.is_ipv6());
95
+
96
+    // reverse dns lookup
97
+    // TODO: (check) doesn't seem to do reverse lookup on OSX...
98
+    let reverse = lookup_addr(&server_ip_address).unwrap();
99
+    if server_address.is_some() {
100
+        if reverse == server_address.unwrap() {
101
+            println!("Reverse DNS lookup: '{}' MATCHES server address", reverse);
102
+        } else {
103
+            println!(
104
+                "Reverse DNS lookup: '{}' DOESN'T MATCH server address '{}'",
105
+                reverse,
106
+                server_address.unwrap()
107
+            );
108
+        }
109
+    }
110
+
111
+    // TODO: ICMP ping?
112
+
113
+    // port check TCP (UDP is hard to check)
114
+    doctor_tcp(server_ip_address, "21114", "API");
115
+    doctor_tcp(server_ip_address, "21115", "hbbs extra port for nat test");
116
+    doctor_tcp(server_ip_address, "21116", "hbbs");
117
+    doctor_tcp(server_ip_address, "21117", "hbbr tcp");
118
+    doctor_tcp(server_ip_address, "21118", "hbbs websocket");
119
+    doctor_tcp(server_ip_address, "21119", "hbbr websocket");
120
+
121
+    // TODO: key check
122
+}
123
+
124
+fn doctor(server_address_unclean: &str) {
125
+    let server_address3 = server_address_unclean.trim();
126
+    let server_address2 = server_address3.to_lowercase();
127
+    let server_address = server_address2.as_str();
128
+    println!("Checking server:  {}\n", server_address);
129
+    let server_ipaddr = server_address.parse::<IpAddr>();
130
+    if server_ipaddr.is_err() {
131
+        // the passed string is not an ip address
132
+        let ips: Vec<std::net::IpAddr> = lookup_host(server_address).unwrap();
133
+        println!("Found {} IP addresses: ", ips.iter().count());
134
+
135
+        ips.iter().for_each(|ip| println!(" - {ip}"));
136
+
137
+        ips.iter().for_each(|ip| doctor_ip(*ip, Some(server_address)));
138
+
139
+    } else {
140
+        // user requested an ip address
141
+        doctor_ip(server_ipaddr.unwrap(), None);
142
+    }
143
+}
144
+
71 145
 fn main() {
72 146
     let args: Vec<_> = env::args().collect();
73 147
     if args.len() <= 1 {
@@ -88,6 +162,12 @@ fn main() {
88 162
             }
89 163
             println!("Key pair is VALID");
90 164
         }
165
+        "doctor" => {
166
+            if args.len() <= 2 {
167
+                error_then_help("You must supply the rustdesk-server address");
168
+            }
169
+            doctor(args[2].as_str());
170
+        }
91 171
         _ => print_help(),
92 172
     }
93 173
 }

+ 1 - 0
src/version.rs

@@ -1 +1,2 @@
1 1
 pub const VERSION: &str = "1.1.6";
2
+pub const BUILD_DATE: &str = "2023-01-06 11:03";