From 88d42e54860d4b207c15bfd36caa2bf9a10836ce Mon Sep 17 00:00:00 2001 From: wx-chevalier <384924552@qq.com> Date: Fri, 11 Feb 2022 10:55:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=B8=B4=E6=97=B6=E6=B7=BB=E5=8A=A0rustde?= =?UTF-8?q?sk=E7=9A=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust-rdp/rust-desk/.cargo/config.toml | 4 + rust-rdp/rust-desk/.gitattributes | 1 + rust-rdp/rust-desk/.github/FUNDING.yml | 2 + .../.github/ISSUE_TEMPLATE/bug_report.md | 32 + .../.github/ISSUE_TEMPLATE/config.yml | 2 + .../.github/ISSUE_TEMPLATE/feature_request.md | 10 + .../.github/ISSUE_TEMPLATE/question.md | 10 + rust-rdp/rust-desk/.github/workflows/ci.yml | 181 + rust-rdp/rust-desk/.gitignore | 10 + rust-rdp/rust-desk/128x128.png | Bin 0 -> 10123 bytes rust-rdp/rust-desk/128x128@2x.png | Bin 0 -> 25356 bytes rust-rdp/rust-desk/32x32.png | Bin 0 -> 4193 bytes rust-rdp/rust-desk/CONTRIBUTING.md | 46 + rust-rdp/rust-desk/Cargo.lock | 4519 +++++++++++++++++ rust-rdp/rust-desk/Cargo.toml | 117 + rust-rdp/rust-desk/Dockerfile | 20 + rust-rdp/rust-desk/LICENSE | 674 +++ rust-rdp/rust-desk/README-DE.md | 162 + rust-rdp/rust-desk/README-ES.md | 160 + rust-rdp/rust-desk/README-FI.md | 161 + rust-rdp/rust-desk/README-FR.md | 160 + rust-rdp/rust-desk/README-JP.md | 164 + rust-rdp/rust-desk/README-ML.md | 161 + rust-rdp/rust-desk/README-NL.md | 163 + rust-rdp/rust-desk/README-PL.md | 161 + rust-rdp/rust-desk/README-ZH.md | 213 + rust-rdp/rust-desk/README.md | 161 + rust-rdp/rust-desk/SECURITY.md | 13 + rust-rdp/rust-desk/build.rs | 74 + rust-rdp/rust-desk/entrypoint | 34 + .../.github/ISSUE_TEMPLATE/bug_report.md | 25 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + .../enigo/.github/ISSUE_TEMPLATE/question.md | 19 + rust-rdp/rust-desk/libs/enigo/.gitignore | 14 + rust-rdp/rust-desk/libs/enigo/.travis.yml | 15 + rust-rdp/rust-desk/libs/enigo/Cargo.toml | 41 + rust-rdp/rust-desk/libs/enigo/LICENSE | 21 + rust-rdp/rust-desk/libs/enigo/README.md | 46 + rust-rdp/rust-desk/libs/enigo/appveyor.yml | 121 + rust-rdp/rust-desk/libs/enigo/build.rs | 61 + rust-rdp/rust-desk/libs/enigo/examples/dsl.rs | 11 + rust-rdp/rust-desk/libs/enigo/examples/key.rs | 12 + .../rust-desk/libs/enigo/examples/keyboard.rs | 16 + .../rust-desk/libs/enigo/examples/mouse.rs | 37 + .../rust-desk/libs/enigo/examples/timer.rs | 22 + rust-rdp/rust-desk/libs/enigo/rustfmt.toml | 1 + rust-rdp/rust-desk/libs/enigo/src/dsl.rs | 184 + rust-rdp/rust-desk/libs/enigo/src/lib.rs | 529 ++ rust-rdp/rust-desk/libs/enigo/src/linux.rs | 363 ++ .../libs/enigo/src/macos/keycodes.rs | 120 + .../libs/enigo/src/macos/macos_impl.rs | 667 +++ .../rust-desk/libs/enigo/src/macos/mod.rs | 4 + .../rust-desk/libs/enigo/src/win/keycodes.rs | 81 + rust-rdp/rust-desk/libs/enigo/src/win/mod.rs | 4 + .../rust-desk/libs/enigo/src/win/win_impl.rs | 378 ++ rust-rdp/rust-desk/libs/hbb_common/.gitignore | 4 + rust-rdp/rust-desk/libs/hbb_common/Cargo.toml | 48 + rust-rdp/rust-desk/libs/hbb_common/build.rs | 9 + .../libs/hbb_common/protos/message.proto | 409 ++ .../libs/hbb_common/protos/rendezvous.proto | 166 + .../libs/hbb_common/src/bytes_codec.rs | 274 + .../rust-desk/libs/hbb_common/src/compress.rs | 50 + .../rust-desk/libs/hbb_common/src/config.rs | 898 ++++ rust-rdp/rust-desk/libs/hbb_common/src/fs.rs | 554 ++ rust-rdp/rust-desk/libs/hbb_common/src/lib.rs | 197 + .../rust-desk/libs/hbb_common/src/quic.rs | 135 + .../libs/hbb_common/src/socket_client.rs | 91 + rust-rdp/rust-desk/libs/hbb_common/src/tcp.rs | 285 ++ rust-rdp/rust-desk/libs/hbb_common/src/udp.rs | 144 + rust-rdp/rust-desk/libs/scrap/.gitignore | 4 + rust-rdp/rust-desk/libs/scrap/Cargo.toml | 42 + rust-rdp/rust-desk/libs/scrap/README.md | 62 + rust-rdp/rust-desk/libs/scrap/build.rs | 114 + .../rust-desk/libs/scrap/examples/ffplay.rs | 51 + .../rust-desk/libs/scrap/examples/list.rs | 16 + .../libs/scrap/examples/record-screen.rs | 160 + .../libs/scrap/examples/screenshot.rs | 126 + .../rust-desk/libs/scrap/src/common/codec.rs | 536 ++ .../libs/scrap/src/common/convert.rs | 188 + .../rust-desk/libs/scrap/src/common/dxgi.rs | 113 + .../rust-desk/libs/scrap/src/common/linux.rs | 117 + .../rust-desk/libs/scrap/src/common/mod.rs | 32 + .../rust-desk/libs/scrap/src/common/quartz.rs | 125 + .../rust-desk/libs/scrap/src/common/vpx.rs | 25 + .../libs/scrap/src/common/wayland.rs | 81 + .../rust-desk/libs/scrap/src/common/x11.rs | 87 + rust-rdp/rust-desk/libs/scrap/src/dxgi/gdi.rs | 213 + rust-rdp/rust-desk/libs/scrap/src/dxgi/mod.rs | 568 +++ rust-rdp/rust-desk/libs/scrap/src/lib.rs | 23 + .../libs/scrap/src/quartz/capturer.rs | 111 + .../rust-desk/libs/scrap/src/quartz/config.rs | 75 + .../libs/scrap/src/quartz/display.rs | 63 + .../rust-desk/libs/scrap/src/quartz/ffi.rs | 240 + .../rust-desk/libs/scrap/src/quartz/frame.rs | 79 + .../rust-desk/libs/scrap/src/quartz/mod.rs | 11 + rust-rdp/rust-desk/libs/scrap/src/wayland.rs | 3 + .../libs/scrap/src/wayland/README.md | 11 + .../libs/scrap/src/wayland/capturable.rs | 58 + .../libs/scrap/src/wayland/pipewire.rs | 530 ++ .../libs/scrap/src/wayland/pipewire_dbus.rs | 144 + .../rust-desk/libs/scrap/src/x11/capturer.rs | 123 + .../rust-desk/libs/scrap/src/x11/display.rs | 55 + rust-rdp/rust-desk/libs/scrap/src/x11/ffi.rs | 205 + rust-rdp/rust-desk/libs/scrap/src/x11/iter.rs | 93 + rust-rdp/rust-desk/libs/scrap/src/x11/mod.rs | 10 + .../rust-desk/libs/scrap/src/x11/server.rs | 122 + rust-rdp/rust-desk/libs/scrap/vpx_ffi.h | 9 + rust-rdp/rust-desk/logo-header.svg | 151 + rust-rdp/rust-desk/logo.svg | 1 + rust-rdp/rust-desk/setup.nsi | 112 + rust-rdp/rust-desk/src/cli.rs | 94 + rust-rdp/rust-desk/src/client.rs | 1299 +++++ rust-rdp/rust-desk/src/common.rs | 462 ++ rust-rdp/rust-desk/src/ipc.rs | 620 +++ rust-rdp/rust-desk/src/lang.rs | 43 + rust-rdp/rust-desk/src/lang/cn.rs | 210 + rust-rdp/rust-desk/src/lang/en.rs | 21 + rust-rdp/rust-desk/src/lang/fr.rs | 203 + rust-rdp/rust-desk/src/lang/it.rs | 203 + rust-rdp/rust-desk/src/lib.rs | 30 + rust-rdp/rust-desk/src/main.rs | 150 + rust-rdp/rust-desk/src/platform/linux.rs | 654 +++ rust-rdp/rust-desk/src/platform/macos.rs | 432 ++ rust-rdp/rust-desk/src/platform/mod.rs | 57 + .../com.carriez.RustDesk_server.plist | 29 + .../com.carriez.RustDesk_service.plist | 19 + .../platform/privileges_scripts/install.scpt | 19 + .../src/platform/privileges_scripts/load.scpt | 16 + .../platform/privileges_scripts/unload.scpt | 6 + rust-rdp/rust-desk/src/platform/windows.rs | 1098 ++++ rust-rdp/rust-desk/src/port_forward.rs | 170 + rust-rdp/rust-desk/src/rendezvous_mediator.rs | 596 +++ rust-rdp/rust-desk/src/server.rs | 434 ++ .../rust-desk/src/server/audio_service.rs | 371 ++ .../rust-desk/src/server/clipboard_service.rs | 121 + rust-rdp/rust-desk/src/server/connection.rs | 1148 +++++ .../rust-desk/src/server/input_service.rs | 671 +++ rust-rdp/rust-desk/src/server/service.rs | 264 + .../rust-desk/src/server/video_service.rs | 532 ++ rust-rdp/rust-desk/src/tray-icon.ico | Bin 0 -> 4286 bytes rust-rdp/rust-desk/src/ui.rs | 825 +++ rust-rdp/rust-desk/src/ui/ab.tis | 350 ++ rust-rdp/rust-desk/src/ui/chatbox.html | 27 + rust-rdp/rust-desk/src/ui/cm.css | 233 + rust-rdp/rust-desk/src/ui/cm.html | 21 + rust-rdp/rust-desk/src/ui/cm.rs | 460 ++ rust-rdp/rust-desk/src/ui/cm.tis | 402 ++ rust-rdp/rust-desk/src/ui/common.css | 384 ++ rust-rdp/rust-desk/src/ui/common.tis | 359 ++ rust-rdp/rust-desk/src/ui/file_transfer.css | 265 + rust-rdp/rust-desk/src/ui/file_transfer.tis | 654 +++ rust-rdp/rust-desk/src/ui/grid.tis | 258 + rust-rdp/rust-desk/src/ui/header.css | 95 + rust-rdp/rust-desk/src/ui/header.tis | 419 ++ rust-rdp/rust-desk/src/ui/index.css | 380 ++ rust-rdp/rust-desk/src/ui/index.html | 19 + rust-rdp/rust-desk/src/ui/index.tis | 740 +++ rust-rdp/rust-desk/src/ui/install.html | 22 + rust-rdp/rust-desk/src/ui/install.tis | 45 + rust-rdp/rust-desk/src/ui/macos.rs | 145 + rust-rdp/rust-desk/src/ui/msgbox.tis | 234 + rust-rdp/rust-desk/src/ui/port_forward.tis | 77 + rust-rdp/rust-desk/src/ui/remote.css | 36 + rust-rdp/rust-desk/src/ui/remote.html | 37 + rust-rdp/rust-desk/src/ui/remote.rs | 1861 +++++++ rust-rdp/rust-desk/src/ui/remote.tis | 435 ++ rust-rdp/rust-desk/src/windows.cc | 392 ++ rust-rdp/rust-server/.gitignore | 10 + rust-rdp/rust-server/Cargo.toml | 10 + rust-rdp/rust-server/LICENSE | 661 +++ rust-rdp/rust-server/README.md | 24 + .../rust-server/libs/hbb_common/.gitignore | 4 + .../rust-server/libs/hbb_common/Cargo.toml | 46 + rust-rdp/rust-server/libs/hbb_common/build.rs | 9 + .../libs/hbb_common/protos/message.proto | 404 ++ .../libs/hbb_common/protos/rendezvous.proto | 134 + .../libs/hbb_common/src/bytes_codec.rs | 274 + .../libs/hbb_common/src/compress.rs | 50 + .../rust-server/libs/hbb_common/src/config.rs | 698 +++ .../rust-server/libs/hbb_common/src/fs.rs | 554 ++ .../rust-server/libs/hbb_common/src/lib.rs | 215 + .../rust-server/libs/hbb_common/src/quic.rs | 135 + .../rust-server/libs/hbb_common/src/tcp.rs | 146 + .../rust-server/libs/hbb_common/src/udp.rs | 75 + rust-rdp/rust-server/src/main.rs | 129 + 185 files changed, 41105 insertions(+) create mode 100644 rust-rdp/rust-desk/.cargo/config.toml create mode 100644 rust-rdp/rust-desk/.gitattributes create mode 100644 rust-rdp/rust-desk/.github/FUNDING.yml create mode 100644 rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/config.yml create mode 100644 rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/question.md create mode 100644 rust-rdp/rust-desk/.github/workflows/ci.yml create mode 100644 rust-rdp/rust-desk/.gitignore create mode 100644 rust-rdp/rust-desk/128x128.png create mode 100644 rust-rdp/rust-desk/128x128@2x.png create mode 100644 rust-rdp/rust-desk/32x32.png create mode 100644 rust-rdp/rust-desk/CONTRIBUTING.md create mode 100644 rust-rdp/rust-desk/Cargo.lock create mode 100644 rust-rdp/rust-desk/Cargo.toml create mode 100644 rust-rdp/rust-desk/Dockerfile create mode 100644 rust-rdp/rust-desk/LICENSE create mode 100644 rust-rdp/rust-desk/README-DE.md create mode 100644 rust-rdp/rust-desk/README-ES.md create mode 100644 rust-rdp/rust-desk/README-FI.md create mode 100644 rust-rdp/rust-desk/README-FR.md create mode 100644 rust-rdp/rust-desk/README-JP.md create mode 100644 rust-rdp/rust-desk/README-ML.md create mode 100644 rust-rdp/rust-desk/README-NL.md create mode 100644 rust-rdp/rust-desk/README-PL.md create mode 100644 rust-rdp/rust-desk/README-ZH.md create mode 100644 rust-rdp/rust-desk/README.md create mode 100644 rust-rdp/rust-desk/SECURITY.md create mode 100644 rust-rdp/rust-desk/build.rs create mode 100755 rust-rdp/rust-desk/entrypoint create mode 100644 rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/question.md create mode 100644 rust-rdp/rust-desk/libs/enigo/.gitignore create mode 100644 rust-rdp/rust-desk/libs/enigo/.travis.yml create mode 100644 rust-rdp/rust-desk/libs/enigo/Cargo.toml create mode 100644 rust-rdp/rust-desk/libs/enigo/LICENSE create mode 100644 rust-rdp/rust-desk/libs/enigo/README.md create mode 100644 rust-rdp/rust-desk/libs/enigo/appveyor.yml create mode 100644 rust-rdp/rust-desk/libs/enigo/build.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/examples/dsl.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/examples/key.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/examples/keyboard.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/examples/mouse.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/examples/timer.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/rustfmt.toml create mode 100644 rust-rdp/rust-desk/libs/enigo/src/dsl.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/lib.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/linux.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/macos/keycodes.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/macos/macos_impl.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/macos/mod.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/win/keycodes.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/win/mod.rs create mode 100644 rust-rdp/rust-desk/libs/enigo/src/win/win_impl.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/.gitignore create mode 100644 rust-rdp/rust-desk/libs/hbb_common/Cargo.toml create mode 100644 rust-rdp/rust-desk/libs/hbb_common/build.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/protos/message.proto create mode 100644 rust-rdp/rust-desk/libs/hbb_common/protos/rendezvous.proto create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/bytes_codec.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/compress.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/config.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/fs.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/lib.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/quic.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/socket_client.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/tcp.rs create mode 100644 rust-rdp/rust-desk/libs/hbb_common/src/udp.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/.gitignore create mode 100644 rust-rdp/rust-desk/libs/scrap/Cargo.toml create mode 100644 rust-rdp/rust-desk/libs/scrap/README.md create mode 100644 rust-rdp/rust-desk/libs/scrap/build.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/examples/ffplay.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/examples/list.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/examples/record-screen.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/examples/screenshot.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/codec.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/convert.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/dxgi.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/linux.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/mod.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/quartz.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/vpx.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/wayland.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/common/x11.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/dxgi/gdi.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/dxgi/mod.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/lib.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/quartz/capturer.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/quartz/config.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/quartz/display.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/quartz/ffi.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/quartz/frame.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/quartz/mod.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/wayland.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/wayland/README.md create mode 100644 rust-rdp/rust-desk/libs/scrap/src/wayland/capturable.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire_dbus.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/x11/capturer.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/x11/display.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/x11/ffi.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/x11/iter.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/x11/mod.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/src/x11/server.rs create mode 100644 rust-rdp/rust-desk/libs/scrap/vpx_ffi.h create mode 100644 rust-rdp/rust-desk/logo-header.svg create mode 100644 rust-rdp/rust-desk/logo.svg create mode 100644 rust-rdp/rust-desk/setup.nsi create mode 100644 rust-rdp/rust-desk/src/cli.rs create mode 100644 rust-rdp/rust-desk/src/client.rs create mode 100644 rust-rdp/rust-desk/src/common.rs create mode 100644 rust-rdp/rust-desk/src/ipc.rs create mode 100644 rust-rdp/rust-desk/src/lang.rs create mode 100644 rust-rdp/rust-desk/src/lang/cn.rs create mode 100644 rust-rdp/rust-desk/src/lang/en.rs create mode 100644 rust-rdp/rust-desk/src/lang/fr.rs create mode 100644 rust-rdp/rust-desk/src/lang/it.rs create mode 100644 rust-rdp/rust-desk/src/lib.rs create mode 100644 rust-rdp/rust-desk/src/main.rs create mode 100644 rust-rdp/rust-desk/src/platform/linux.rs create mode 100644 rust-rdp/rust-desk/src/platform/macos.rs create mode 100644 rust-rdp/rust-desk/src/platform/mod.rs create mode 100644 rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_server.plist create mode 100644 rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_service.plist create mode 100644 rust-rdp/rust-desk/src/platform/privileges_scripts/install.scpt create mode 100644 rust-rdp/rust-desk/src/platform/privileges_scripts/load.scpt create mode 100644 rust-rdp/rust-desk/src/platform/privileges_scripts/unload.scpt create mode 100644 rust-rdp/rust-desk/src/platform/windows.rs create mode 100644 rust-rdp/rust-desk/src/port_forward.rs create mode 100644 rust-rdp/rust-desk/src/rendezvous_mediator.rs create mode 100644 rust-rdp/rust-desk/src/server.rs create mode 100644 rust-rdp/rust-desk/src/server/audio_service.rs create mode 100644 rust-rdp/rust-desk/src/server/clipboard_service.rs create mode 100644 rust-rdp/rust-desk/src/server/connection.rs create mode 100644 rust-rdp/rust-desk/src/server/input_service.rs create mode 100644 rust-rdp/rust-desk/src/server/service.rs create mode 100644 rust-rdp/rust-desk/src/server/video_service.rs create mode 100644 rust-rdp/rust-desk/src/tray-icon.ico create mode 100644 rust-rdp/rust-desk/src/ui.rs create mode 100644 rust-rdp/rust-desk/src/ui/ab.tis create mode 100644 rust-rdp/rust-desk/src/ui/chatbox.html create mode 100644 rust-rdp/rust-desk/src/ui/cm.css create mode 100644 rust-rdp/rust-desk/src/ui/cm.html create mode 100644 rust-rdp/rust-desk/src/ui/cm.rs create mode 100644 rust-rdp/rust-desk/src/ui/cm.tis create mode 100644 rust-rdp/rust-desk/src/ui/common.css create mode 100644 rust-rdp/rust-desk/src/ui/common.tis create mode 100644 rust-rdp/rust-desk/src/ui/file_transfer.css create mode 100644 rust-rdp/rust-desk/src/ui/file_transfer.tis create mode 100644 rust-rdp/rust-desk/src/ui/grid.tis create mode 100644 rust-rdp/rust-desk/src/ui/header.css create mode 100644 rust-rdp/rust-desk/src/ui/header.tis create mode 100644 rust-rdp/rust-desk/src/ui/index.css create mode 100644 rust-rdp/rust-desk/src/ui/index.html create mode 100644 rust-rdp/rust-desk/src/ui/index.tis create mode 100644 rust-rdp/rust-desk/src/ui/install.html create mode 100644 rust-rdp/rust-desk/src/ui/install.tis create mode 100644 rust-rdp/rust-desk/src/ui/macos.rs create mode 100644 rust-rdp/rust-desk/src/ui/msgbox.tis create mode 100644 rust-rdp/rust-desk/src/ui/port_forward.tis create mode 100644 rust-rdp/rust-desk/src/ui/remote.css create mode 100644 rust-rdp/rust-desk/src/ui/remote.html create mode 100644 rust-rdp/rust-desk/src/ui/remote.rs create mode 100644 rust-rdp/rust-desk/src/ui/remote.tis create mode 100644 rust-rdp/rust-desk/src/windows.cc create mode 100644 rust-rdp/rust-server/.gitignore create mode 100644 rust-rdp/rust-server/Cargo.toml create mode 100644 rust-rdp/rust-server/LICENSE create mode 100644 rust-rdp/rust-server/README.md create mode 100644 rust-rdp/rust-server/libs/hbb_common/.gitignore create mode 100644 rust-rdp/rust-server/libs/hbb_common/Cargo.toml create mode 100644 rust-rdp/rust-server/libs/hbb_common/build.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/protos/message.proto create mode 100644 rust-rdp/rust-server/libs/hbb_common/protos/rendezvous.proto create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/bytes_codec.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/compress.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/config.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/fs.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/lib.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/quic.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/tcp.rs create mode 100644 rust-rdp/rust-server/libs/hbb_common/src/udp.rs create mode 100644 rust-rdp/rust-server/src/main.rs diff --git a/rust-rdp/rust-desk/.cargo/config.toml b/rust-rdp/rust-desk/.cargo/config.toml new file mode 100644 index 0000000..f19d0f0 --- /dev/null +++ b/rust-rdp/rust-desk/.cargo/config.toml @@ -0,0 +1,4 @@ +[target.'cfg(target_os="macos")'] +rustflags = [ + "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", +] \ No newline at end of file diff --git a/rust-rdp/rust-desk/.gitattributes b/rust-rdp/rust-desk/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/rust-rdp/rust-desk/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/rust-rdp/rust-desk/.github/FUNDING.yml b/rust-rdp/rust-desk/.github/FUNDING.yml new file mode 100644 index 0000000..1745f9b --- /dev/null +++ b/rust-rdp/rust-desk/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [rustdesk] +ko_fi: rustdesk diff --git a/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/bug_report.md b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5ba29c8 --- /dev/null +++ b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug Report +about: Report a bug (English only, Please). +title: "" +labels: bug +assignees: '' + +--- + + + +**Describe the bug you encountered:** + +... + +**What did you expect to happen instead?** + +... + + +**How did you install `RustDesk`?** + + + +--- + +**RustDesk version and environment** + + diff --git a/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/config.yml b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b92c70c --- /dev/null +++ b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +blank_issues_enabled: true + diff --git a/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/feature_request.md b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..9e02f55 --- /dev/null +++ b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature Request +about: Suggest an idea for this project ((English only, Please). +title: '' +labels: feature-request +assignees: '' + +--- + + diff --git a/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/question.md b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..435b58e --- /dev/null +++ b/rust-rdp/rust-desk/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,10 @@ +--- +name: Question +about: Ask a question about 'RustDesk' (English only, Please). +title: '' +labels: question +assignees: '' + +--- + + diff --git a/rust-rdp/rust-desk/.github/workflows/ci.yml b/rust-rdp/rust-desk/.github/workflows/ci.yml new file mode 100644 index 0000000..cd82821 --- /dev/null +++ b/rust-rdp/rust-desk/.github/workflows/ci.yml @@ -0,0 +1,181 @@ +name: CI + +# env: +# MIN_SUPPORTED_RUST_VERSION: "1.46.0" +# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + tags: + - '*' + +jobs: + # ensure_cargo_fmt: + # name: Ensure 'cargo fmt' has been run + # runs-on: ubuntu-20.04 + # steps: + # - uses: actions-rs/toolchain@v1 + # with: + # toolchain: stable + # default: true + # profile: minimal + # components: rustfmt + # - uses: actions/checkout@v2 + # - run: cargo fmt -- --check + + # min_version: + # name: Minimum supported rust version + # runs-on: ubuntu-20.04 + # steps: + # - name: Checkout source code + # uses: actions/checkout@v2 + + # - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }} + # default: true + # profile: minimal # minimal component installation (ie, no documentation) + # components: clippy + # - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix) + # uses: actions-rs/cargo@v1 + # with: + # command: clippy + # args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints + # - name: Run tests + # uses: actions-rs/cargo@v1 + # with: + # command: test + # args: --locked + + build: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + # - { target: x86_64-pc-windows-msvc , os: windows-2019 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install prerequisites + shell: bash + run: | + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake ;; + # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; + esac + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb' + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + shell: bash + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Show version information (Rust, cargo, GCC) + shell: bash + run: | + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - uses: Swatinem/rust-cache@v1 + + - name: Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --locked --release --target=${{ matrix.job.target }} + + # - name: Strip debug information from executable + # id: strip + # shell: bash + # run: | + # # Figure out suffix of binary + # EXE_suffix="" + # case ${{ matrix.job.target }} in + # *-pc-windows-*) EXE_suffix=".exe" ;; + # esac; + + # # Figure out what strip tool to use if any + # STRIP="strip" + # case ${{ matrix.job.target }} in + # arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;; + # aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;; + # *-pc-windows-msvc) STRIP="" ;; + # esac; + + # # Setup paths + # BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/" + # mkdir -p "${BIN_DIR}" + # BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}" + # BIN_PATH="${BIN_DIR}/${BIN_NAME}" + + # # Copy the release build binary to the result location + # cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}" + + # # Also strip if possible + # if [ -n "${STRIP}" ]; then + # "${STRIP}" "${BIN_PATH}" + # fi + + # # Let subsequent steps know where to find the (stripped) bin + # echo ::set-output name=BIN_PATH::${BIN_PATH} + # echo ::set-output name=BIN_NAME::${BIN_NAME} + + - name: Set testing options + id: test-options + shell: bash + run: | + # test only library unit tests and binary for arm-type targets + unset CARGO_TEST_OPTIONS + unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--lib --bin ${PROJECT_NAME}" ;; esac; + echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} + + - name: Build tests + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --locked --tests --target=${{ matrix.job.target }} + + # - name: Run tests + # uses: actions-rs/cargo@v1 + # with: + # use-cross: ${{ matrix.job.use-cross }} + # command: test + # args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} diff --git a/rust-rdp/rust-desk/.gitignore b/rust-rdp/rust-desk/.gitignore new file mode 100644 index 0000000..cce3b90 --- /dev/null +++ b/rust-rdp/rust-desk/.gitignore @@ -0,0 +1,10 @@ +/target +.vscode +.idea +.DS_Store +src/ui/inline.rs +extractor +__pycache__ +src/version.rs +*dmg +sciter.dll diff --git a/rust-rdp/rust-desk/128x128.png b/rust-rdp/rust-desk/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..045d8f89476309b36680e0c373000be5f89ab981 GIT binary patch literal 10123 zcmZ{}Wl$VU&@H?yuq^KGwgh)~*9Ui(;O-8KL+}K54H7K4vq-Su9)i0|fZ%ZReqY@m z_g3AS>h7x3H9h@n`pld-byazE6cQ8w0D!KjAfx#&LjT9WpnrZ)%;F~iK#U)tt?#92 z;Y;P_;c8>=WKHGe?`BP9?PqTT0Qjx$=Nfp>@%u(?en(-4yJ;Xxz}^VUyu74}*Y^E# z*2l;=fa&syr7>D%3&R#b)%1SYm;A{7mY&iRSGBO_(fSh4%D4GgzItfZC7z#nfB1U; z7bo=nw%b3n|3-U@QzSIwEgjTjs$MTB*yQML;5TB##o43vtAIkm{qzl6 zFWbW38)cW47lDQC!jZ>-UGnkMuHJQ)9@8Dyn_b-RSmo*M8do7mglDhBh1ijYT{roU z#}T{1Y-!IbJR)W4rnJ;l;{RQq&#qF!-)g07hH&@Ey}!1Zg-Jx_L+mPQ24JlT|c|P?r-8`%bmS*wxO{*X6-6uH~ zA3UGNx)frC>$}TIx7zCUp3X7vq^myndkq?Ao44(D3@KdHucGizhuzat6cTuRtyx9{ zW8bTy4I^@yH=&MM+s5Dj5ZuvMRJYv|lMx?rgH za8i$Iq+AS-=$MCPUeoleiVLJZF_z~kRYO`9JV!Yz@;t|Y9i~!o^y$V*RpsfX(uur_ z#>%xViyJ|x=H?C^>AFUaJ%RHb8i$7Y_r3+hA(7bfB>rg{@}$9b#zUf@6IPcj6Q4jhbBJj&iB%_`EEOk422#i_7#1$=jn@>W}Blqf!33oD#yY5 zqK+btO8#AuxOsH@2?>u!qkbBo4|er=aFLt=)~bR{lk<3E+@ob{S^H_6nnZ0k$L;rP zujw7)Fy`ixRz|GTzI!r&M3sUmp-*O-R*Z-u23T=Cke0m#gJI zx9IqgnfD{C+2x0&TC!vEBA9Y7PYE-oQsq()bKi}(9a=H%e!Kc>YJY$Dm-UJB$aC>X zr_!ty<+s00o}!MMKNrZSO>4Vkv&iey_`bN?FF(5*$=99cHC&G5DtpQI)-PtsP23yI z`9qfX@5wC%FMT=h`I@4+xqf6Q`U=*b|9OrJ%8Pk)q(N&eoG?pl!yeo{A0ZaO#q#Nc zNU++YOxl6gbUgiX`^oz(`_V!u-UHLbRAJpVSMv|q@A+YSW9*(=9l}wVxE%N8?1xu} ziX3Lv+g<-}zv&!R|Jj@;#zP7IN$=@OpwkB8(hcLaZ z2Om(OBe{g@-$%*Y4l*n%Gb)sDXS8pRQp=|AAolcJ3u)=m!pXXZ>#FGKD`6`>TV|I& zT9VQ=CwC?HN31XUcr+&WALjsyeZHnu!S7`_^2CgtgrFKcc+7ltclnTn-DNM^ebE{8 zA_MVw@Feg@YHZfQxUJs>-~HMQ9oV4ffb@pLc{T2<$V;0%iZZ8>%%*Sh!(A|685B1T zuGUfP!#aJL<>zAmK%PFa>ycz^ujwCkUqB>nM5wP(BE%*&&&@rmXXphE=ngJ$Tq>wh zd9=4}^2lE#I<}Fi7Xfh@-3LzIT)Z_HpI>fe#k6U$oW6VB5*fvLHXn3MeRRN`i>-4V z=x_#`UPro}wFV8`*1aTKNk?hubQC9un)PqN@3Q|TNqfK5=7z9%KMik?Bu7#b6`NNX z3;R*zOgk`ElN77MU7QnH9VY@++v;YyZsYINH$0rzpsD!>6V#gb>>cBn$^QEptdv>d_l8jB~}MNo@j;;E9@7|&Xl^g8&i(UYf2kJoVoLxnnXli7D==^2TMb`)AfLj@kl+ zl~RtqbG+1eO3KyRI}q_6zE6EfP+nIPrzQj}?a}T>{JqEzs3MKHh7G}ZhU?2_>44Bd zo`oq|A(!(#IH|Qx;=w7Wb z`yy%eCbZmO*tph1PYsp2Vz;eig1`T&-7}9p9v*`_>5@~LF|dmNKQ|IGF514k# zrk?nDjCn?*(txQ|lRVg3YYkSIy^TdBGDEq32afwMdU7Ys!vE;mR?6HL>UTaL@~t8ju8>W(B+b@2s-*$FvK6l4sF)%VDU+rrPyBxHjNA){z+(^x7xR;>l5$l>TruR^GgMFhQ2MULTuEpW5x!Pjy?g}Aq2|KdN2fkvFx4?4 z+B*3zvvLgMDN7XQ;bCcKcux0Y7z#7?}!xjh{U}M+OMN0aHh|hpH;lUgpBC z+;PqzCaE@61%Uh+gut9|^jC_8Vko~57ty5cl9-A^@VZrc>&ffr#e_De*1yHH>qGU4 z(QS?zzK&G9B#JxXXV+P7s$r$QO_G6l1Q~tXAbp@vu_pt*ffI}MTv`4e2A3s#91&DY z+hH36DES89QxeE+FPQRl#vTawf!%xEI@bALmVW{jT7-_qs?%6k|3Kx?X7ZosEPJ5} zLPIGy3ft|9S4g3>lL^2$`q@qo>U(rRq4-j^Qv(`n7(YnVIMt@rx&V>$TX(eP?9#J7 z8v8N4sZ5ueyH1NA;Hekwj_LVR4Yr*=$A>Z*Fn6urd1J$u_`nE@pd~|SmPQ$yDND^! z7We%9+~h5Rq5PSD`tU|wiH??YNBzf6QN!Bq@$XvTLT745L#BkXEuvVfATJy9MGWzx z5&@r)_v8de5=SqwkX%qQ0kfRdmX%qcXUTbr$1OX< z+_~VGn}$d%R@&r#Z6Z31WdBv=Yv{O?>(9R7Aqm|^O9#UPg$W@XK%6D)f$U0X8Sx!} z9oB!M#m`J0W&?{oU~=AlB=r#a;!xDQ#x#JG!>tBRBB5T<>YEJpT_r|!oF zMFU6{H9MR*1bOhKjp;{+puy<_aS|pKS`xq<#r_JltBz&{l_+NU6zv!tKzeCfGIxWR zi4ye;d_T&GzZZ>7NIHNUo2yx8B5SxkbbWeFZBLBJ?YL{$>|iZpt4OS#Mc z)*WccNcMI=zA&%*tCw%tu-fRYhJQl9JZ?%PWHMn!>cEpe%2Un7dkBe~q?A^lVJ*;>3tJCAHB+daYkx`8M- z-+mzrkqo3Z|K|_1<*_WRSd!L$*?t=l=E@iCyrQ~^dv)sWiH@$2uwJ!cp}=z!))v3) zmn0CvPK0B|sYFsFI5O_jcXn4HCAR*rSPs!b-Xr5H8H34g!~x?^Mgm?`Q&^&soprn0 zYy*RsL9g96zpF+YAcofm1$YZ7E_hG5m|L3k`g8PpN7x;+#O!bOD{6R<5zYoi&ZXrc zkL=22i$`qDm(J-Ty;OZTCS?(>|I)?;GH+00{9%$CD7VGMbEt#61uC!P~N?ImdnI6kq zYhH1CtWCpDJ$tMvkKT(q#EfQwGL1va+Km~%TRKpM;idK@BKViVlpwmS;)N^bT4^Fx zlW4WHK4cgYz7z1XEz7+ZJI!z~8JR@gmBBx1e8P=#Tk^!fLQO7p{?E+tSG zO`>?o7puSKSz!1uk|EKA#UqCtZzUU6p6*qyKQNY$R$Ng;&l5C%l7P`7jlXCcQ66E# z`ZlIHzU(m)%L#eq#1y4>wW19W13>#Y&ApLN>!!5G{dRnp5f%2dso@e~MUzx6?IKK&u_+ z5&)6wdIwt4Ib0%Mh2;_e8!s#>Wvgg!`?6Gol`OC%1Gt37WI61gmNb((5O4L&(nQf? zuIGS2KCF3kk*qpeFjY{7()4IonJfm|lFhV0drSX^BY5v->=WI=M00`VMU)6#_tHjN zpt`mM3oLRE$$1da0+;IN-qKvOV1XMnj#KvFM@QzM?Pw68yRZbz<{Lz1Gsy%(9b(Id zWQZ~Lzn0Q+7EWBE!VfoCMmK99iXC9LSbdf!Lixkrflg5P$5{#OtQHd1n;BLG3Q3y|q%nvO5yO`cEXS2yIL?BPuDoqrf0r$~wE2BxIaP z4bi{yD|$F)r-WxIK;O;`$CCHkEz1Z{#&vfcr^oa8pp(ZZvK@+k2f2qMU<bkMc6~;X?ui;#7-}pzX}#v2l(L zt&!e0IP!BM`BEq5zz1CU;ZX%{#Cmvi;4dHKO+x>O7fg+j^BsG!h{gY&>zRBj2wo;Q7)g>n>bpO475;mkilO)+LM$wGZ-0OG zj-2>c>h@}AD8a5fUUS@1U3XiHow5h8R-f`?>r&$ugveZ#^)#Dj)=wQI@ykNTImL%{ zwLep5go*mP+Jx8-V~0_vO9*dZ%HywFf-0*_H*X+4?I*wMt>4?s|aegM;9Lx=~!66Bz*)Toes6jQ_jbyF=-WDqEZM>*4^;`Ui;g# zyq+~L*piQ0>~$_oZW32%FWFI>c;-k&mzWk)u_(%7x9s?B@B1kc=EXQ(@WRwIp2eU` z^SdVnDri&nnGumVlM(njen$!wfOvv=@LGqCwhEO!J-ane3!ntbp0Ge5r~?M@4J~Q)<)Hp&=$SZ+(rYLc@FT&hc`|qUeH~`tb-AP*;Tf)CNU)$7Wt|hC^bE1(qgl z1fja^-_CRIEBEHi#v0uJA`$Si+o5rZqlSUQ_q43u3Wzx+oh7DOgLr;;Z5?s|6KkTv ze2y{soJ7mvRGnt}f+jhh+|8SA$m}VmeC9-^ZPwd^#xOZAj&z1^nX{Z}y1c@2#KY!`faa+-e4EQudeo6?H}SD4$|XAuA+QD!YvpLEMseVZ zrM;k}EkaecWHMWQ3!jS4!e9KkoDkmyE(hULeY7HTZ-nos2}P`v&0U#^I!uZ`E$*qB z1z;U@>qaU5&28M-m8*e}8^OQ7)G-&#a*VSWre- z-xt7*0q9K?I8oRNt4G!cX97X;e&bK+iF+_?z`$9N*PLK=8XpN#sZLCig#2hEp?9;? zw{Nl0$LtSkdWU&`j4KVnWb3R|63sWCyV3-r-~{JN|609(`sSDM`Df@_*ki`Dj4nbG^6kob!=yqlG{bXyW=3B%!DsTu?Kv0OWQz7nM8HLqSRN!KyD=@js z7pl(6HR(W##Iw$a`@J$tss2-#o>vj2$(5CgO$H}}qI)x$+5Oe^9os;YVd1mqZ6$F$ zoLH(~2JzSbx+O(=>`C+P0*Zay9= z@@jJH6=r8hoH0rViusn>1dYDx`cBBpYK(A`Y6TxJU$=OxU&A!tw5H_@-l$;|5-&n z6<_`#$ZiS-o&W#}-v1cfoukp-KM=`F@jqFlJrF)Jf)E0K^gjg6R#8S$+i&$e-``DJ zmSRNQ6+##-jO?5891R%92WJ{Lj*oA%)@{+8w%}NBriFhp%#b1?u)t4ST-VW7vOM;v ztW;><>$QYp$g zp+rd)|DU0<)h=VHpoSj-1P>pEP{gWE7^`k##%K-m-}=x%sem-+Zr|}2N1|~9JqsP} zhQye93UN!>oQ>_gXbq^abPAjXa;L4cSWsWlu$^}Y{ey6!=@P=$49a7m;oUim(SQH0 zJ(vv`l@({WSSVq$?ah}F>;S7hSM_1cPhimVDKd|Eq8Z`JcH#wo7z66juA0i$RTG?N z+I57I@TC(om;#n{a<0dh5?p480?Byw80H5^ zlpKH1)IiEFgY^KtK-FTIXjbYV7SK^$fUyt`kP*sP-B+H615yDv#HfiyI^jSbtUN9c zGbJ^xh&G9LNTqZ!CRRPrJ5D)T;S!F~T`=OIRE3Skg`xS$*(~ zkWxxWdB_dr25#Xv+DW})kw$b1j{c2<(I=(1e)K*O%6s=fa zORpOgrnNW&RGKw8?9&E=t<{|tPdcb^h9Ltp7gu*K3I?bnO2NG*Wy2N~!>acGjz;|= zue1ID;4NWFhRczQZ>g9<=MY0~oeDn;T%Cs(IODpVAMW3*kP52tlc2!Y=m1!jyf~T~ zJ7)!p2i-8VKZ581S{+r8k0RFXFCWCs^m~Ca+?IkdAZgsKn+gjl?tkg5HIk+OVx}f* z^Z%p;PB?n*K3!exUU|4WVdE(pQsoc9fGEUvF@W#erfq z;ZWTbpB8SIu+!@?y+4zoe)UjAe-i>_B&PhvRbYT38IqJT2+h58vJ>dN4gH=n;CBx8 zqbtKm(?ifi-YS4J*l{f+arL>7KU}EqzaR9XRZJNz^pl7Pf^6>8f$dEEaxiAxV$zmmYSmsX@S?>{jjOlZG=Gy&^KEJaS9nGTMj+r>29Mi-G~$HA`|k8 z4NoiPfXwFcW$41v=&#dS2oKN;Y zu6Ty`qM8g1tdl%nztS{2+D{}@_W%NTphRJg^lTYYCj@(twG|J7V(ED-Y}kV?7P0A_ z$1g#*GK6FQz!?3GBZZ?mDg=a??y8lY>9^gL@C45kBf2e-fvqJ7H5IfK$aH_?wihXi zka8LAi3pUNDKb#uC4_=xma78<+mMzDhWPdfPHfxO1%$EdqkNv^hv!GpY6p+}dU%!%GOOcbD*%=V2bJih=@U zVjF*vLvqZM18yDI0GO7ff~pKy_LOh}h$fHwfBr$_?Q|(BRrWK(a+@?&^jSit`Zz#} zB7LgVxnt;T;yEpp6p_#igFoG;y{LK4$PT0dvZcEHjF{^cHL92P?{ zp&tY;pp^hE;l^A|vJwo?Z>B^+AQa76iji##J4hXQ$fw02q0z5y^*~ zfL**!TqSi6{*HvhwVPT6n@J?4mAQQyD{Y0_x$A-YQ!Gc8CuVa!_S(7iB)YOom_`WM z$*2(WrEN?RAcG=WMI1*ocPVC?5?&sw?UNZXAe3Ui5S$sBcr99@jtj83S`vEy3;WF* z)ssw|DPRvr@u(crf9pQ{8d~zP07{RO=Yg>bJk2#|p#9k=)Y%HXo8O)SI1~Jc>d*XQ zK3eAzo66@HH6R zNmu0}+4o^?8!D|PEsVSVkZrkAh<;3b^@rk8%VAl3^?7)Jwx?Ll_K3)}&dT2rUF0cI zcB%PP=leZx)V^9k-?7z!qa1&or%?~NO)wlo{dH&d9L5_Lu#<=XYpzYx4fxi7et=Z< z%tvF-wLY9IE7OK0A?sv=XE$IuPTJxIjY$9Z{Re#xtc|>XVyA)C3+^MwfoMlvTdEwu zj>8rysPk86q1`OtYx31LqsY~k+o{J5yJ=YV<-oCKnKYB=Mn35DUSBV?RG(>*k;tER zNXF>n3yy+6rtSd^y(Y4RFN{(T77f3&KzU4S;s}*B7%AVs8bHziY`x9^tWF02ltegg zP4xSgUpj+>1-PcdlSK(k!ij%U^TJo9axZE|q+SkIPj2zK_ii({d**i2_*P?4m;&RQ zQr;xWnmbxMF??$yAMM@x%e1(AA&Q48BI!-%9B8?i_7&d|H5rTo2dJ!3MVeP0NMd#N za@?2>H5pl+*l*21F-m7Q^zKGMB2V+ib~i#VhotXycI?-j=s)jvFG4XJ_OV2ou?n7+ zLJCvber%tb3E%be&Tx|xs6dC3hR+RkwmlU0FZZ;7z^kJxD+(49_$qO~-*$tjSOaHE zTcU*KSHf<3o>?N`C!c7cW&a)}NsE2CRU`Afsg45H4HV2XVNjtnznE&d;5q-vB$fm* z7Iw3)u*JTjeg}{wdJq+!stT#u{^*%T$t1>$vLklidbs%pQ*5qleBG^Xr5)>O(bYn) zc=^bKxQ`_T_yM@{(eI?&!_N!;3jg*~RF|-1MaB#Zf#nHtxPUik>Fo9b?J3Jg)y$d& z@&3}1Oi?@))ArBy)^r0SshH5gIjjGA_sq5C>^f{xA~f41H`-lY`ey2;-nT3|@Td0C zpSq1lb8a?PM)fA>s}GoLz0+c}y8YOLeEiHd2pSxg2l`r?^56Aw7%p^Ju1No`lLs#j zLx~fPWk!|MB2x;d*RnW+Pymi-Lg$qYeV`SK5UMN;F01av6sr2iK4GXZu!t$JF%3Oj zj(~*yQ7bGIs9%W>=Lw}WztaJn!vYXInGIaVyD#rciJn=HkDUJX{H;^&|DyH(KgrjyenW~K XFgC7uZ$0_<<^U+ls>;+!nTP)`s}Kb4 literal 0 HcmV?d00001 diff --git a/rust-rdp/rust-desk/128x128@2x.png b/rust-rdp/rust-desk/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..39e2b23cf62d86a3ec4a4cd29a632dc1d1fd1a18 GIT binary patch literal 25356 zcmeFYbyQr>wl3NYjk`NEk_4x53GVJ8c(BH$aS4IO-6gntun^oGLeQWgAwY1qV8JfQ zZ=bz?``z=-eP@h!|2r@ky;jvXzd7rxIcu#NMMtQs%3-0CqXPf{ECqRKO#lG+cnJic zB0qi@x|CT00F>iC+PZFm(4 z>oBP{ov{_HuFe?c$frvz^mZaIn=>t1Jvg!~`Nf;h9`0{iAAYEv3$`TNv2+klM6ZEy39SkzT`M+%XOl!8GQHVt{`@z6!~aNO!W9J8uIX~(=KvF&A&3>z=L&at{^C?YcblIJ(YQk`D<_Rg<}!nQFr# zQI&K&G7qU4SsuMSf8*YnLF1Q@b?`RVw`x*~fn`2~(1Ua}^vw9BE+*#y0ZR=hJAUzH z#_YlxovVe$tJOi?Nt7Q_}^V`UVlfAd3MxY%nAE&oOqYXaoKfuDt7I1 z$sWRZbwR)~Nc5rwQ!0JB%TPwL_iml73*hg6$5Zv7Wfp_ENwPTz3d?=flqPtqVQ_J3 zZ}`^xZZ~pe_x>9k_hf(6BrpN^DsV$Ru!eIEH{6_FA}C=R$2l<|-t`+M*V7O9Mlo!= zUwlhKJsn2hOLt)LC4}1#Ft_aR$?GVl|0*xnkfJwG72Ha+b7a31j!e;ZVJapL_*SN) zHP$erZ*btwV^_YRqwi34&gZj_jJ#>=b5M0DD(e=6f_2E->~}mKJ#CrcovLS*`6gE8 zy0ZS1`CWD6FY@P(U%UI++YTFVsGl_>YG%2eGL`0d{33g+-{vtr*7VKi>w)L3#X-`n z|Mf4NI$}}1C=RXG>U)bAPot{kCe*;B%B+OM6EX~UYde_*QJG<|G2@Pz`1{~(EjLOn{ z3U99N$Uc@=4>1*IPg;i>yP$BIDMKw3t2#s*rVf5euT_p$1_gGE#MsDMW@&z%eenTz zkLKl8pR%Nlv&&XD<#fjTXqQgW^v`;}qe8kmQd>!~EY%r?3-5A2+@2KU$5OmxQ;x-) z;L}0gs}}2jIKcv z7@|jX+uEmhORCmMtR(_X)Hq@t73q>~!uDjadvl(q`U48-;k}cs3ZBy3?2F!vcK>*A z!1l7Uikr)*`Jw-lg{l*`(Hb>(RzefPnwBi&Ellw6+wX7qMK#S_kt|}R-9qUT zBfXB~iARtTfjRfyuHAf&Si+d@Jxa8@19t9Z2?!y7@l&{gZ2sg38>cek^YemV`HN2q zR|eMjzdQ+HaNQzk;Vf(yoRw^Sne}|8#j}|EALm80a`p=Y(0dXF3Dvk2U0!b<>a41tOBa zZDVT{-MF3;B-F~55MmfTrNw>oyh^!5k%@_{qRJtCc>?;@PD+q}yuXYJ&!LP6?9$l5 zZVpY2?EhlrD3Sb2)fC0NrvWQfMdJHna@(o=$MNRu;S~H&VXVauuFfU<_k}oRqQkv| z?EQY$>EXC`OD=Y?_Nur;)1n7+S^*j2GSPUN66DYjtMVBS2HnCJvdid5P{Agz-qqr` z?4?iv^|Ou=$n%gDmV7$@*9C*2%pY)s80TfGPe*OCyct!*g8@i>KYAzMAqrrNIv`q z{tuF*-7dLuju%8^OschL8#pYSZ1H!PjEw6I;Gq`oSuYBsdU-wvzH|#AJ!Nm-pDR($J3AZ@?GaXf&SsGe zE~m-pguDQIc>W3*p8Ukb7c;Wh460)9n%azEK{s`^n@SUncP%j3r^C?MEM*&swmJ5k zh1 zKT@ZYpccSt$r~NS{Egt%eg&8dKg$tl$>q3n3FD zyveJpDOB%81lNrFd{ruN32& zNuHzBOA;BDZII1;sidyGU8~_=>_ylIk=RxBUyFr%Ci77@Yr}Jw7C$V>M)z)%yZgwP zhXnF(E~_-cfGdXM>tZzBbH7xPx>D3NxW5mW3sD)I=kNYVLog@ZB)r;>ua?zwey`hB z=;i@kcYl=;5mv&w>mwxp;!GTGFLkM3OFi6eTX(JUIf6PSDVCIo`1Pwa9l9S0c%bKv z-WfzZU_indh_4KjlA&lop8Q+CZesp}^{}s61VC6ONX|mwr4o9w(uTr3g3z8Lp(&RN z64G0vuy5Cd8K51^q3P-mmN1-;{Z;8}E6B;)DH#-6QwVeHFAUx)=8)!-mz;5bQyZI{ zx&zk*%t)o9B}5{7JnKg*41Er%nijDF|rVQ-XY8 z*8nIS1_YoVyviV%+d|KJb{-&Z50^7!mqE{0e)_FOM@%RHiR?u#?qC3}IWcOJLK|ZL zT6KY}0_UDoG{Ia+ASF(L6KZ!AKv{H8!8`a_Wu0nXRKx+0Q^`>qTt~=)2|JhX?tU$l z!rb|N!(KSq;}eD8gNu>fs|~RY`Dd|6o-L*5Z#AVH%@Pg=+HrS~dvlaWK7huhXh#@{ zXEtU3Yz_gIesaLW{5c_=&8c7(VqNJH zRpyVS@(DyJbf8787tW|H+=E9{ei>c$wG)g+HbnRS*PFaq_0H`H(9c4e&*kkwhZEg| z$SEF)*wUt6a-W=`W!9T9$9)K^eBuyawzsQax)l|3RB+lLJN5wak$uijfq_KjGz?oR-j_g{RRBSEW2Z9+(9RhAQ_B z&XNJL=;aFqmV+VRl(Q0*;_4*&`N4;h(H2bVHKo)2d8@=Io;H~=R+3-^o67;*qx>4& z%4xu-wJQN)ATV{KYpZ*DBVRR&Q!5XwtjbL9Q>^qxY8ubv z%u5=k&N?;;XCkAUx^;FI%lh9e##O#fKU76Sk4&W&|F|X(B=Hu#{hlHN>V)*+=?%Cs zx>%z{gfrCC>T&+W4?( z#Zk8Pck@i(zE(kUIl!11nwZml_mg~Q46VNiKChvp+o!zDS-250`k4olh)OIbS{51_K_ha48#l36)@)jaVM)%D_oQyUPxL3xcmb! zoWhKInP%g#E-cMAxj2p#fli$(Su>w}hgX+HQ-Vp{OuaGHU>F&KWPmFR2R(h>Swui- z44!=rwlfM=#3ZM#1Iigur*=(~*oC-L{CL)Xx&5$P3MA4`Lk_)4zO= z?4B&OSy#W4JocR7jun^B+d#)nyf9Uz(<~{2TERKOrcN2eeAsN+7 zsy}S2UJv#YD$qkLLjK&h{8U5qURt8d*WKwzL8v;jXnk7c7_GvX3r2VH)-Qs$ds>Us z+OdoaR0Lc#NY^&;_4Sxi{sI>@^NLvUSpa#*nMjb>WJ~i>y(EtPLYkkX5`hbiYNG8x!gf0UHk7ES<6dIi_O|sjjyX4q3 zk<>>YdM)W_%;jpTI#-k=^zLns1X$%t?$iy zi7EPoe;=s?>k#kWH=pJ$F#Ykiywp=wQi8)|^Ea617MYtTXX~|foKclE zsWi_%(-X#rcL4Qa&6IfKTKG7a-+qb!C3gVV(x~f%*NPSerj+Q8NemWc!jf}!Ptmn8 zM%!{08;d^#r+E9~7I<%Gp-f#RFAg}EP@TB|-l^Gzp8zn#^#apci$zqoG~FU2K8KsH)P zBbo8ZxiVZf+{n0MQP8o=8O}4aazHPxLo*`kKzr+FC;o7;>!@N|)XG=E4u3~$21(5z zkhceOR)$RioNh{%>h(qOxY{JJfid>_KZM6PKgMn+=pxFvCWi#rwX!099#9ovOmQ-u zsweJ~1B4>f+Wbh7RK+NXiIb|~EK238Ai`ZLGg&~miM7q9W*@}yH1i}5VA4;~+>sat zITt4N_DAvTf1T|OqxM0{>f%TXobN9=*(QAp2s-6|pKfJ4V{{O$S+BCUzUBXpQ$`m) zF$^=SuJ&Vn7oOJO=)VdvQKyA(Ro^(r_2V0^0qZfp1??lO_Yj^~Vq|ddE5=Mwe4~`p zvKS+T;rdX8$*6p1)X1r=pDaAVV&hOSrKLGxPr%Q=lDf{gJ2p9AHvM>~I@7~rGGP#D z4PEF+2NFbw<&YB%n0Xh_-_q`F*5{zCu^!}T)GUO1x3BIsm_)$xNxPSKNaEr3XJ@5^ zbDZCw@*DSf>Shb-ubMWz`P{x<7m$UZt>g_?IWGwv+1tgLMqlSjRJjS%b9!R3cH_Nv z)zVoQ>t0!PH|{*A!RRDifMSWug5~f;dI>u+wdpf8Mp4i-n3v>xVH#ZUAXr>$)Yiu~ zhA%NG80}e%joDa+v7Te842l#;{LNz0t;8_fU!xY(^|0h%8|E*g_afCY+9re;sSlVWm0@6OO;mi4#o! zeMCx%4$m%4%{Vc$oRU-KhQ$l%_SU%cE+qVF4U3H7u-ZLDemQUZJxJVE!CLz?Hc^cc zW^)OFo5kp`SBLFW)P*X!%DlzWoeIVLQG)+$G)yRRagLlQeoYP^i;K*dj?a)Gm5Q(Q z+G3c>dL@Nq{cNso9PD#SuTRin7|arof-idWT7{WNg7p_rB4bDr)COwqmb#762^>aoai_TU&_NOoP&uRHHpMq9{E(dbVvRHtwQHjUNag)#NwmmzjasMy}@`drIqSCW!Cc{9854mul)t@=Mn z)7&o8pQO@zjk8dwGS%;Eb^f#Hl~aeu$#6Wk(TFmW85L#E0nw@f3W@BO{<1dJH~uMS zz4Q!d%}g76c;QTOd2a|QWz~93Mn;Tl?0CIwFTLr73Oov^GLSR7n>EXe+DJ4wc5Bqj ztOP*$vDMU>a8z{51Mc-GIA;Pyoi~}zO!=C4E2ot9sX)fmN;msGAdFbx){E(Ai zD||l#OEjkTx7YVy4f4{bM)U&ON#mO-=ZS~5s`v6-4m~YR@^^|Oqpw{pGee?QT?%wf zu$YZp{aK{Ar;(0vE0+ zZ-8C)H#%ZSiV{4{1G5||aS;0N&0nBnYS@cU*S9*@3oZe+_?f71$;`^bqY%ZWQ0m%m z-{P^VVk9q!;zBV$TSg-_)0$bc;wQr$nd@v`BBl}FvQCOvkHGP-?qexteu%S1$LkV9 zgGEt18|**Ox0Z4cZp|5=zEIHg#RO9f-}HEP7u_MmiPjXS%neB+$Qd?{V|V8-T1$K| zTsYOa_TH=&&#cx$BQ%UBoh^?HKer)7O-HT~_6`U?-gk*xd=Qp<9bsTVp)WU(Q~we1 znKeAST6fCFTFXLZhO1gtuxBgavte~k1PkPUj?8b#a4WQ($h;K6TD5X!JtSjc z5~tsp{fcetX09alLSXe(|JLmLXzWcKLu*b{EN85@szrgRmOVlYS%^V!yls@r)|nkV zDz)#a7eHKl&2Dx5dnt)pwG~O9yRjoOKAe6Iaqg!uEG>`^fTE=GKXc?S-E$)gdpw|@?jDmmj3Y|&K}dAyq|NA6;-J^Vv173+uYkmyPceoxs%*R~ zFoDYVd|r%i)bfz?FjHtjRVH!d1uXbdqaV$!9F~$ehk_1%iPUeP$)2S0p|zlB+PLsK z@b4N>4RBHJ66p|vRDQj{6_KvhQVIamOBlry#;l|N zLb9bR!hR%78i z;NO*TjrjG-j|yxH;qk#36c-1*aEKxo=t_&z3>G2_PqvdZw>o%lx}iaaJmsK&(hTeO zFxUkfNd6EMj}cTXpO6IdVau9fRP^sP(yPaidW4j0$3F;PP{CxG0!IXf{d` zSqsk};%k86i?0DJUU@OC*)z2oiY&CpBxD&een>+TQ>d`awI)uf@hQZ5RWv{!K;H48 zE9+x?Ye$r7mvD;O_cz}zQz_}F1mdSwoqh|A!*hok4=If`tcLbyl2VaaV+8=$`r^ixX&{qrmBAJ& zW+-^Tje0pP7FN&PwgW*eL{bIFAq)5p>#~ z{1?I#xi6Oz-t6;$p%i^zC354L*c`+(X+5debMI-VitpXYWv#{|J#=vQPppYVyk^dd zWvs&hFN@G?Y@Jgm%+@PBEZz|hUWBQ*oknrK&1nPGr7kTGR*OgIQ;v62-O8cb1in!( z!RCQ?No)Zb;;dwlySKGBr%q{m4)p2gGl+xJ`Mw~98qL@OCW#mEygd$NEvmkT4e6yc zMbdURbP2SNNY*o&DAm*7S3A1Da#QFW5^KrOR6_&dWiez!eC+@&AX`OZg4pkJUo8Oz zQ|ICF9VRmIqxC#{kh47L_d-3Qp0<)iiDhGv^ z2kqog3L*5S8ktDx%Xw1eA-`(Aq8u(%{mM+Fv#xsixOZu87#Ho>n|}7qp=jVGp#pOg zlW&~;O^j7%7<77xF3i1}TM5T^lc?Co*tICU$y-U)+7VJiSp^*w$a_lt;TAg^FVd;w zwoNE?a`~i$9I9@=dTqUM!^GPBRE*sM-j%mo?I)X0S9q3gri=wj^Z2e8@NEOLFSSkh zr#)SVLF~Y%aLu2?NW1;Cw*}Zv4#`63$Q$se=gictI(yPQ5bx+9n{FhbAHQhz*=m<_ zMLoJ~kI5!6gtB`M&M2kRP4m1s(QJka2mk|8o7Qm_HY8M4^mh84lCeRx`a`sITm){K z(^Kdp7L>Osl-g?qAC)*mE1Q?9Ibmanp{>E%QM_8+SAm3-!0yd9ml? zDSHgZCtROsquIQp+of&+2kiFUdN*F9!39*yMcMN6Vq5zQ-^4nO=WB#$C7=+;N`6s z_pJly2kU_rDDD+fG5UMUlk%fqIEqdO)NtodRln>E!=^Z&S9EaDIX3U2`Q|;;e~Yhw z2s@jc9NCgCY4mRhvUKX9Fb(vyBPx@LHaiQwkPj(WCz`SNCN1ah+`p9#lo^;75b|E= zovI+Mq=XIV$8R!E^NHY*zj&>k@>GB*HulILX^K5V)wv=YwD7Iu%k!SHC8=_$-8}hE zoQ}JU#CnCe^}3)=-a8!P3%+SgVMb??Ab&i%H2hfV49e8 zRf1EMyY0)vk{WZM(1Vl2s-kd~eY`LDG%^WVi2vdkw%xjI42CULh5f9*QFQC#Gis!X3 z_sKv%dq2?>L5vF7`WbF892_Ssg`o!SBET#vRVfKGlYthX#>OHv#fE^b^fWisZC^sA zQl`Yq2s-C`Y}yNoe>Rew23@d9U?fN}99=GcbCs=L?)h=vprWRXI}%_^E-*SZA2Pmn zsBeBCL`wfPDlr>qH9pE7J9vk~wc5s&f2LCWc+44WBPFG-ASLx5C!3GwnOScVMCAL# zDFU}CHn69HqwbxvCFM(O;=(>1iBMUZskm`Y%kksK7k_wIb98%h=~X}l~(Wx z{X9|JvH*!8v6p4Pa_MF`eZN_LND8r~j23+kLu^Hc>b2ycdCD2FRh71B%$h6LKg>kf;lzRI^Ia^istrkZbU8m zwZf{v{NyU)ar$(lcB-n(F>-7|D7&J@UrPw>H9YP} zsZ=fvc|K2b005jko5z!8T@_^^b0-IOQwt|ED7%*f?C~TT01y`Qf|;7zLEXS+P%9fp z5xS$cE;_J{g$UhCUKNN6ObTjkBk$t^)$&o*HuteJ7qp-g6Ga#H5_%+XfV!E2y&UWv zU4^_v=>G5uJzoEA=AZ-rfw^6@)=A5Rc=KO*dY?gdH zylgy{rsiyBf&yG@Jf?g+f)?CdT>NJIf1^-yv3aZnQ~SS1^_$A#k;>Ep0_7Fp7GUEv z72tj(Ff)COh7-bO4&|{h=i=ow<2Qx=q53@!LK5ltmMxCgp^%bMpM@u`m~s zeWWyf%r+YbQ!6M3%+cyk$M1O&dK}H8vZlX_C`{~ME*wUejg|2Lk0LI24j z;o|1$iAcexwWaI74&hr{YS0+N4w2`lE++} zJmx%)xyr_A0p(%if$&4vOwFL2Y~1{h8OhK0m@R?=|IY5}Wa;K<>H?Lpddz{xd48;| zKl2P`{44#K{vGaV4gH;45H3zO&c`K?SDTAVh>J&vmzxD5AOwNX{WaxaVUFLc?0*bd z`1d-bq9XKnQG|c5Q$h;Avr)?(2D7(;y8OFX|B*cZFSx(i|07ZVPv(Dz{beoX1oM8Z zWNSAyPsjh#{eJ@di$U4O9O~%m^k0SkcgSC|{Ow`%81r9kj}N)WXC}u#ADaJ2mEYC$ zfAQxZDfoZU!z1N^;VGhu^<>9i>T+Eod-#Jy!q#gW&fc5Rj2Y`q+r#rl2B& zvVlp4P0HIIS#%BnfB_29653vi2ie|UMzh|B{;zYdDYFZ`Q(2W7H@tJ?ED;98^ZL;k zY?S0PztT1-OxJg)pe+7uRG5CP^?6d#M@k8S43$9~-jDk<=tMYRLn9Sxms84(^Lklq zF4X~nA(hp@j;89;dpxN>w(`xK@p9YuBZK8U&W~Pe(R!CU+)An*btl>!7;SXP*h8o> z45+gB>lT<qnP-%^tN!3N zW=|ux-VE`tYxq+6=9)3nBg^c*hu9K{JNR_T%RSo!|FoCPr2zS`Ie;FzGD<3te8OS@ zM6)myvZ+xWtVzNYjJW2dQzca>3V-5;962J`>LC=9NqjQ5X|rfbLD1cX^$<0YPI1|! zZiB70<_Qr4Mvcp+&*9UH*#b!X2nkDYx$}TR)@A2*`)hlw$*{urz;{_{9Lic+DeYiB-f^KoiC0)@LF5+@Ovi z;mu3uA`kWh9OIgEm7g`Ps_SAGyYEWQGVCBXH`mRM$2V$6`(daP}Q7-*fpPg?yCxEWZ3@bWlDT#qln-WmVE3+XP{9G|qRTJ-;oA zBYWG#NLuEKh_Fa}rrW(B3Tw&eb|7_V%x(}L1$O^S>(#Bf=Y3O4c|$fw{iUn99HmP3 z9Xk4H-t*W#{%#dO(_K@#?}camL6?2sS|rf$u43nBmWhV&IN~!Q-FZB@c^($J^1M7S z5)VX$psHWWQise5)b7jIbUcrO>|cekBK9#*Ix_5{zP5xuAoej{q#YAFZEsKfp#YsZ zDA%o)aBrnjBv5$PK6T#fOM7AZhQ`Jj3**9*rz?JYJp3em86F>gW>c66%0OpP8r zee_RZF7BPgFuG^4Ah!-*Wp0Ydw)ik(~k*P46Nh^)2| z<$Iz?NY4ue6<~Rp4Qknty&SJzx7*X@S0>*JFGEV+{fgTBejnY)SkU{Z)~_AApk?8K10AWX%qjfcTBGQ@PcCDCm^fMwKKxOA$m|V{ z=;&X9!UJ9#aLi-JKpuL<#N+_}Cy6cfC27yKr&W){=0ET z1Hcp$xVH4IEO!?wvj8qL1UzyQqR6j_&GxZ&ybd0bM~>6SM+zwqf+2fup+5O#lBI6w ze)~c_O;bKiAoR!j?i52neS{QS#3Px+VmF=={v^Eu0-09BT_cfpyX>kaT6aHZB3Mq% z3YH!TSPy(Es*fBF4C;oM!22+`1H&oD1#m?l=n1fzkS*Y}d!B-Trt?pU;n9h}y)XEA zWLvpL5~tk#(vRkXD(9_5oIYV_>A}z$X9$LgEYde)V1o?qNJu-N*$7vy`wO+#8N=;1 zI8AWnvRfMJwwZ=YjeL82-Jt#o38`gek~Se2hWQzdlsurY<9LZ3@|sl=hA08^rrl7F zlrf_%FJD=<=mTi}kRAi9_onXD77-#`tLuQ=2hB%&#naVb56r2wz8{B_xTQG;4|YOv3sU%+QICF zok_5HFHU1OFGfBCB(}!tI^{iK)^^AiP9L{ZUyjbWzaSBdItW5^>w|lm!$=pd1G-G< z8`W>N<(nZ68bT7q@#4^kJ8%k?GY&z)(et~~O$cxy`Bn?K|MfIY&tpd^epsA`{Ri?` zM{NgQ#&|D=!{j%I7BSIJD&hke$r$li9yI|&@GEOj7p7bhd$7b%lyeZ9_`lE9HQQ>TvRX06*K^F7()U|+fUqGB?4>2z|IN@BM%@3 zgIZtV7UzeHhUf^b<4Lq} zwID_U?0;-#mblyQz)3`%dF_qS&!0$+*uFevn5HE}kV7GnCg4CoUP0itc$3S5`W7+L zbtSGk;Q8C0cUZ6=P*T#`93=v~va848X&lLEgTQ*f{RLV|sjWE>$-5Cr@pN`2po z=;GQfIJayUexyN|LAfKMm{?5P{D!QuDrBB~wF>#_!Wj}KK*Or7oQG;sw=!9Iv(L!z z<2WA)Hbkduh>7oo7;4QQdb@2#kEU+rQ0X?;7>E&e1IZl{9ck( z$0w;DT3X1{Bc?in_D=9;>!(Q;I#0nbV$mUtNbu#D{klS+tc)SHkZ0qmJhLbh^bU~JDfSSUW-{ic+e#sNhhIQ9Rh$f`gGccBV)=#+o6#fj&-Oi=@5%Vm zJK-s>zwo1$5UxcLh5&;vvmFIg=q=YkP!4qw&o_>fmX6PZOhQAHR!+qgd7EE=U>_;i zkfumyQhRb)!=9Y0zrq@jXdx;s3HT%!YsJ%36{V%)p-=@x^3xT>Ku4^#AQyg(nqwP0 z0}%-7HPU*lEA}+xA}v@SX7lSOhFzr0k+mwzJVpdhI?SH{sPICXFxpkq_^}i*^4oE3 zU01ulhjxP2-i4@x@*u5@pGtaYj8Wz}0^pxSC#p?Tmg4&2Ri<+UuR?&$rg+Kg0n->b z2cq-h3Xf}!%&&3X%NugmcW|d~Oota89(Hdl^w3R&M^Ijqtg_}HUjvUpt1LAVbhIAL z=(zXj4P~hS7tCR40LSft&(+~GlM^=z@`o~n$)U@j!7_x}_YK;_-A4{^n(v4-IY4O{ z=hW(I^b`ayvE~36sbd=2Wo4ksS$?&fZFNY%-6t3ox%PTbiFf=~z#^{i8!=L5_B_;; zw`z*h!4~*)WANAf-R?@#%#~F!3xp0W(ax;$i^1Bdm=R-_y{#>{$C08D5A_%?bc;|| z5d7ysao8;qSGn`O?il&QP7g;ZoF)uC3t=L5xZL23QR+3=4qXsL3?&w;ullLAT0r@? zfN~1NN!bQR$`Rq?6*(EASA3*ma8q%M`o3De#%$cN@Z7P+wfEJ8)b|3F8K!poOQ6vSa4q>Rs5 zBhCb80^g|6-Z^FAP+Tr*-1Z1kF83B2D-hW1L{M2tBC=D5m(EjvsRx3jHthjEPl_bp$#Q_wPDGfp6ZdF@{csNSeWHljmQO^figHlefha=a#3jCeMOa zrls8%KGR)|jKQ|dG{)Xmg%_>5akLW=Nrenqe4Jx2p+`6womseg(sDfE898^r&?C=` zFwEQ~MjV>5ag4IJnz~V7My8zLgt1u_KN+pc5mM-(W(aHs;9!8D2u;JVu6)uNHoSs zpGrAnF_~ompY{7YFjgvLARPjqVwQusMQYbjh0po6*E*J9;$gsLQ9v7czPXnzsYExD zl2!K`yL6Y9_da~&qEA->)r^roRD8HWD&d0FV|1Rg-VRWRe=kOAkRv*NmK$W{(Vc!@ z&RJT+h2v-pFxU*5HW>jSQB`(4+}mGs`GJX0gpaSXYpbbGeZO3O{(+1sy<+kF_7>1y zPn9Ep9ApD)V>cSG1!$mNioe3C50SBPLq0jOvO_kC#Yxo`SHqz^#M3L(Pkn|CjahNF z!KDi2l{x;11v`*r9R8R?LZli2e#M1uh0<^Ikf3w5wbo4rrtvinSLwEKuq}u@JZJan zz!JcEfM6&pPRu3j&!sGuc-L zr%y|hx$Cj7BZEvWqPTR$NX9R^GlM3V5HmPlbD;2u=01tYr9YkuIo`2b`4z&4ZK~@; zpAb4HakVOXb#ynm;4RTFOOUJZ(Sn!#CxM9#udfI1Hwev0P~Ohm+LtU${hO7YgX1q& z0r8rE(>@IT@gSM$gF9Y2CIL5MzgiTb*q##sp3>z96%};Xhs1TUIqZjPOyNs(Le;Qk zeT=Azfi#!rg$1~&V}S-+-D(E4YD(MX(ts^l*P#KXdNbnW1@&K<;#*)!r=Y@8lL@A) z8XajngShilhVuq)j?lU)YI%HzDElEK;Fq|IG&bWwG|xh~3 zMu?^kkht0n>)$!5o3i`_5Rp0_xA7_j`qj~N))8Ry>B-X6-fQhDFu*A&9r1X@VCapF zk`|;~8mL1u>YVFM%MBUJSfV)6g_F~0QZ|!ytZLnjudM1@hA72+eW_T}fua;CmxG$H zMiLLo5IK(?3M)JrCQ(?5!CKWmel}-S(iD;p87Y%j1u#blU5RD24`ZeAiTW!hOvMjV z)bE&an-o>7@*4~=4L_E^^WFr(pW=orX8>`0b(D7+J`IkL9{;4bp5MSqmNK8al3YGooPwB|?xlm6UdV()!1MxISK*iZ}6(-`ZM?K;)m^(Siz zc-L#{oHtk(jS>qg9`hElvCN9%<^*n@;*eL`P$Bo5{ibW^%3<)qPpxzlw_~v)Yf)Yzv$r{IYh< zf?+Z%tUHU`_Pp{cS`(1unui-=VA*H%&O{^T)^j{ts`idi_!qM9Rcib|h8ifDORM-+ z8!^tHp7rZjRJF?rG&vpHLU)x?0uaZ{fg|KM~r4|Ba_f}5OW0gtu!$}rm!TpeWMD$4*VKpnAtmSAyPL$a8$^>wLrPOV!Qh60XBuk zGOay`7rjRh4?O6dhSn4(8;YewQfiPJWA^O2(#oVr&!xS{f zBcp%R)n!KJk7)>mUXkp5Nq>5Ey7DY+Wy+y;kAdMyhdqwh>gE8XRQlGbZ_pLVeieb# zrW+%Lx7I8pOF>Me?>I~>V4gxc3&H@{RS;+ZmW! zvpL?a4idy-*(>iSXoU;FKUx(EosM@brq@CG*42rmi)XK5bZ(URe8olFU-qYslZfT7OxvB!HkSyYhoNjy2h62KqZQ(b`-KUrh}+JblmpSQ?!-4zn-mS;(O?&DBd>mk$X$WData30l!xeToQf=p6Z$y=sJ~;iz9PGae#!1TzV-7e zRT!7cGQy_G7#A4iWI<@Q_OGo3oj%$7nKa(r)dgJkHMqJB)%vmrmB8)%fO0_@+LPwX zKrtH7OK1O;U)>P_Vk(hOx@8ESLU{JMwv_B*KXlw!TBNHNnzZP(I#qbwk$o!i$~d+R zPuCFMij-!(hN7#7RDA7no_j<}=sEk^XJDshZ$zDAiw}0an$7Ozh(l?gz#`UNC}qRW z%JSy2^92Zk2QitZWK6ZL-7?XZzN7h$bbzff;l#N0P)D~0;IXz3qB(@$*BVZ|&7^lM zPew?yZZ?5t8=iKxAm)6FDMR6}3UBD$5J6F%Y=>!v)i{O?vWJ&!cz3GDHka3#tH873 z?1^+)nS*{%dVeN*O@I9ORoa@5`hxDu{>37aldGjSd4+OxNGkzPHsbdz6H6K_D1b#K zofv!*z_-$UWM6$&uk=jh$sUTj_0L|C%gAjMNr!I2dMn6Lg_`S6MzwMPJYeh(BFC#C z1u^xk1?$61Ui+vkFB%2XZ_J&ZXW0gQr;TWTLGn$2!cwRoX!#aVQV0R2dn(oi*nA0p ziE!tX2CJa($cBGlTX`|>mlVaYZyz|FeJY%|j5t3YNY3Agj;SCANN0#gk(ph2+kZVl zdOR(4tVn??@baT{eZGyw&6B<@6lrfMSa%R+@0T-G^%+tg+8`Om%E!;>f#x;4vfE{JGLZ{W`GE& z1W9%=;`WBHnaQ}|iQ~_aNWKsicmiomfbcZ3m?zC)!6fX+jYZI$6;usXNDk4i^Pq&l z=<8ygP07wzu|pAqc^w!AP!AP0F~A~s?HXZPB57tx1T}E|(VkAsI z;^%puxU|-GfJtTcbpMFxFauNgV3Jl0Kq1*%{FuAMATTK?4z$y?N?MQ8o$t4CqZCKT zbKV;dBGbplQgkuNS8)Lr(ZPMXhKo*>@&K9&;UNL0k1vo?K_%1@i(Gk(D7Jx$z-Y4R zrQU~2B^V;ELO3|jsM@uJM|qnnxQO9sWUUUH)97(4)g=x$qgij>&`rCb6%a&iPI>bs z06^DgB2U-i{!BnP{GIrtcBnv550lh6&7>e^SSX(N7W(Hl0HSHQ!siSFPxy2ds>vuT zH743cmOZCghMCj>S&uB@Oevp!Ass4X(ANVP{Oe;jj54Atd3}kT3vv7(3R7*Sb*mnd-B=$S)N#PG7wpGT49X3aQ+Et|0&Q08k@^*PX^X z@g(9~Y$>fx9s=A$3s54=_SLXe6VUi3EgDGnPNM2nCI7owWj&v8UqxCr()Te)Ld(+h~Z4s$A7&RnHnKn4rNQWMtoc%+}aHE02P z&;gcc5t_NcDetQjiGYT}_)PJkh=%Z&cyNam(9)Bk{XtAzr5<@%u4Nq`1bL+&{5B>& zKpJaiY_OPe{+zI?M&$D^@)Twqab+cW=6&jhu|AGi$KC;OkeQ~s%)aX?-&v_Wdpfx2*csy(xub#xmXNDK+K_mA1bW~lX(o* zDxL~VYjr^wkRcRrQRfgA3}9Icz+BKHV3oA!i8GMd#Z;9#Ui;!n$Agn(2+ipm7yE6u z!Cmz%Z4jh{M39td{uvi|83!s@W&zIZrinK%5)PMQO_z>Arf|gsv$nPl1Kwg}iT10& z>o~kkHynaps*G>(yNAV}<`Z_O;U$CvvK)`M0JN{y-PGLi8JXhED|o^)3zano0E4&q z=6F3hT;P?E&xX1gaf--wz}-mN?W8rm9}4092k)KMM-an@2@sEW>2RC5CL$3T@#BeG zu+#J~X%!6NhY+5aJmjG0k8z0O{rUWuXbK2Uj{NIe9XDh|41u)at<{9SiS;YvX_Ew@ zh(TpNVDo`afuAy9W`*PaqcEF|vTC$CI>Z-3_F1$afoSFM z*Jz;ddsNJjs*VDt$0??qe{`8tAU`z(KWbfSyNbm096A(rP z?Y~cU!GmKoo=beq6Fx}3W!F^)S|)%Qa}}|2ZXL|f8=8wS=O*GyL)vV5?}@Lb04q`( z!00dlw!7pqTo_FP&|=(dCs@f8xDc)hOp4LQXADZ8RnK`AhcL-&Z39G>@ce4dwy}%8 z5Ap$XffUlwnRuFQ@C=QR`e&+Eis-R(Gte4RP9ML(MH(U1Dz}f}M36d1P5WuoRzUv00&o_O>1LG5^GLBI>^=)!32bryFQ*Ft5P;o% zu=lJ~0sxl-mesN=2TAEHE$2bVUH;UrejPL+=pmIU`CdWm>H1rEu3g zq#H+_?Z$7Uq0$MTS`15TB})E5BClUHHlDJ1i!Hqyl&KW z;Fa1E8W4lELHOcL;Frt=3rFB+i#^v?PO;*I(<%6QgB(pl-{qnJ<@s>`buhyO;?^W# zh0C+v8ZZpyg|Ko3OfL}An`OdK1Wvaj-GpNM5`a~B(>wt0w84mEDj+`qA6)_0%oU7~ zjw>~A*<8Y;I1oOw7=Cl59BscZ#7F{KdfQ?c9%Y!6*bRJy22xb(^_jr9n(9{VG$aia%6$H?58is}?^h<$_bK&O2K#|o3T4CZB zITt>>3>MVN`H1cPaKeiI+wlz0Wj1~^c&ZDIbwPhjzQ^-8;Hv+hG1~wD6n9BPK~x3s zwN*j|C`_>N1D{(2E9#)oFW=*(GVpdQJaiiH+jE)azp)&Ej&2y1H2ZFP8LVFjx6MP8 z3!5q&a4B$GoqkBdP1E2r*FbHVoCMVufu1&?4K%kRfXjB&zS;z*I^}#k2Qa?|e*an_ zS4o1Fv8O*c>1(M$GAiEr?=6DAxdrOVteXJYwxtHIG{75Y0S~riefbH%2hTLZ;bswW zmC?@%d13Wzc;rTyQ6!R9JgR%pn2|||ll3$6;O>pEvR=L`zft+$7l9w{ftNbLZR6oF z(i4E@1cu<)89360w2_v4Kisns?pqB71tNn{iGVR81;0@bx2=H|4!lE@Q(IbjKXyl z@bulVZmv8Ne%=q^Bphyo7n@)pgYCP+*y8aG4RAr@AUv@{bY77e)rGKmIehCjpdLV@JhX&wErtpOOF{bzGxw@2l&2@Ve&HsJ6Zq8v1* zPTa#wsRV$UV%WSA9&y8SyWz)YM1J70esrkMh2Omz zZeA`X``Q5=eb8_QIy*qi&5mktr>&k=6PxGfN8#$(P*r02krKnD!yQYZDhQ!m_~tIe zy|I3-=4nbm2MlYdWEDeX#8`m<}<3G^W`*lKd;bIKp2xVn8wc?Q*zlEmRjl&?c?bOcPoM zpy3!CIEhI3QUox$8|!?KuiY$@E^y(^uyQgf*h}Xp5LuOZHf3SnhBNrVBcOi z9)^=?h}n6AX)wt3xQ5E$iWq<(4utL9?l% z=?cT^C*h}!7kocO05Un&a4-rFzYI&ugv7MlUeh&jJ0af*pIil-=Rs2!Y(EJt?eOg` zpj(g~KENyLBg|1Eyuzoujwp5_21J2eSY8GjD}_6D?Hniyi2$E&!yb7T;!Ha{yAMd& z-cquY02~e&Hep*6Y&{4*k7(6pQ{_y4Kzxexp|%tv5xA`vy8Gd5H@wgS`_6zF7A}xG z07=J{Ap(~HsSz;Z!syq}fZM8}xL5@BstO^HBSxrY1Dbnb$1!;7n58yR1R$mIL=z|D z=eI$1J`@BYWb+pmbVJaZdpDz66rXet!h$wvZHM-5*xn912M}q6uFG@MYJiy$Ct%72 z*G+?^r7*1wDl1`8CFBJmHwRohn!0I*mBr33u+2L*R!+S zIh7GNOh4NT`Yuh~pfBG$W?9Am;R#LgqtiAvKtoR~U z5>3Z2=t-D2-F|n@7c0v(O?-l0af@*4d??glTNjuSF<z8yLl7auc5k9mWDhuSTx|~RK55q4G!56lQ)NF^tiZNw5vZ_Ko0wQ7?gO5EA z`%Z~Ig4qrYsIP!)m%%-wgGtYSirq1wZJuAPF z6BvPSy$lV_Q|Dz?mx}B=JmZY+rlDMiyVr`l{HXvFaZo(77mhbs-KUsZRhroe7d+Vj zFCTytZByfA=6hj&DXb5Ge~h)pnkD^<-EiX^$n#AN<3H5{Ti<|3_5%4+=dc)sGh4ry z2AFPmcsB%`;J*f@6=wenb-5rf51eC6S~t^x7jmn?@6P^p%9vu-#A93GspI0iJGAVn z@|*GmU;;ibv<$&__JYd;A6h92a2#3On@@8y;=N1bqILIQMi_Z5;s)l zX7^oxTR;44ANw}zUxq*54)`IhW;IL=0=Q5fn=yFk4alTn^I9m+&93jLTmGzLtQ|)U@Hs>w zRkjg;VFmntvJD0Zbv72;&M`c*WiZ-z>z0%`_;VGDNXM)Ogxd#;yq9{iGt`^+EdxJa7OSBQP)wt7pUXVlmLe zj>fNzlK@6+8X6yt!--aStBEEU5t}Ih6X+X(U5)Vlo$&YpL?@pd@%PI36#-ln(xW~217cTw>1S}T z13rB<+%QiVh+Sd><8=XI8BrT=2b(cX@jVA3aQ{|#aIZM^wVbI_%Bu+AvRRNgLAITM z?p}C(34H!qF_PR4Ct}7NhY?r=5J_MQ3yP#**9rLgOK_|ck=5f-#;*uqq83Jnt~7LY z!Rx!g9fpkyVPPE<=SUpWFhm!m%vb{fIz&x~NUi|FDQNA6y~p6`1MvJQYZqHrpNaq! z0Zf=npt(UeATtE_zX*4A!|i>rxDLvTw*>@MsWFCL%V(_c_)oDmlM*6k0mO&h-{vo_Pq002JPP0_mUyg>gK33?4lJYpdZO zZ-y0hkRwSK9m9a(gq86&78gK+UIU_%Tmzb>__Pj++WkL$6-Xd*oUK@lGJZt>S9Vig zU@Vw$um%3|G1$BWu3HXEs)SOQR2Lu(o&8p&+Zba2iHsnC$-V|m8PM7f-`Nc(55wVZ zgrsqh^@BPT0oVc7CX8iZM>n+Wf|f2=Py>r+h+&!K`J%e+$}<4;^o>sekb%M2Rj@9BPDrY65zN;p^V1*RWF&fb4L9CkU-!cxV^U0QW6`rPEo((%)LY!PW8bP`{D6};(5>QJSXP@d?~f7F0b)x7*3tEGvFo?P7q8h z0+{$XfoXk`q9aXngHQ&gb#UEmm|Y4%pJ?JS5sN<(hBF7NOD2s{05BJm{tw2B8@9ulxD zp)m@L{XhrM4(+F)wit>EMN&~+A$Yu^GBM-_?-&@NFAD9wu{^M0%9>38G(jQIM@hVkBe4kF5e|eXcs(mNCb#t37A_6p%CQz&L7y(v`89?#UY)9 zcmjIDVm$lEF!T+=(H{6tn?-s_AOPMRh7PPDP4HAcQ# zmMcpN4pZFQldufL4ftVx5ax#4w= zeyY!{;0X-6ai~R+&hX6({=4x@ACoJqp^XptT_CGNjZCR8W{Y3ZOGVVns;n z)dBK7#QbVm6cnZef`H%6u*C(iU>e7(ee|oPQBcT!RQc&zG>wLH1hBr2os~Y0D>pzv zA^YWjxrgIxXR-4f0o=8oH?$1z;8N?OppeyYLMBbay*IG^909BdX}u!`jrl4WSV1AH zQRsr93{5jMZRi{U0C);$nI589Es=sk_M#@lsZb#;7kC5$>+3jK7ve3oL<$O73Jq#= zckzXPJQv>k2Iku^kT{*hy$Dp8) zO$g{DHx==c&(Dzy|J)(~%>-D!oc5Yh9*mo$(N%V*f-$wzQR{{6Giusj~aqlcYdsL4g z1qGYK3AG+}f8##xe&UJu4vcz_iQj*XmIe-O__}$ZZh$3C!?=_$prBw+)OZQ!<+E$c z9PRW6cn2TwgOxnscJd4XwTKD|wuB#?I?v6hI1x^l7$o$uJo}FbAue&GMC@+d-?ldkj8Ox$XM}id9n1%=Jow)?$u3h zJDnhh6kw`hQ3?u|$V;yO%AWTBxtWF7YyV zdKK5SM7XAHm_?l#Ac>eNtDvCpUW0c*z6)xCoT&@)dUHQdYjAXK4Q-&M?$;*%S6){9 z)r;n=-KUwqua$}_2Q%}1Ea^(Hpe06ecyt7|0c21K#tKsh4(p?dS$$Oj`pUf=PbGMJ zz~EF~A#Iyxar7T=)DFI%yKNf+cvrN*G#!8a64$+TicN>lFt0mKpvZx%&`k&j1!03w z%)k>ja3(W2GbXxesV`IMJk`UR?l2XDF>;3!1fxj;=?pGYMG_S3spw6o zgOuM*B;;Wv&&NCbIsr>1F>dSCYc0ndrnDFhAxMgRZ+07*qoM6N<$ Ef?R_wi2wiq literal 0 HcmV?d00001 diff --git a/rust-rdp/rust-desk/32x32.png b/rust-rdp/rust-desk/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..bba85feb6bc83e16410e2bfaa94551c224d27aa8 GIT binary patch literal 4193 zcmV-n5T5UeP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3&scHBA+h5vIEy#&s~aya3f-a#+FKTuM|PVD44 zepafak~o0C#l@v!_W%AX=0E&JTdIkv)ZB8m{KXcV?_5-S{k8M$Y&_rR&)<*u{qDYb zJ@DKLoWu3A^t*lMe)`ejnZu3u_f5T@_<9d?KX?WVIBDY%J znmTS%=iTs_a)0VGS-Kzb+q}II3tn1@iGue#xM0tByetUUr}N2t``~EjLOFMm;p9TT zEx{r0miL^kee0liPJXV;&)$Ds{p5V#@B85=U*_9KTzvSzrH`F|T>NI@@UqCie=v&O zzbxneh^#8IuhsRK@o2sgb)4;Kf}{6pD8Bg_G)_?uC%%LhR;a{p`fWnswtv~;&b;l+H(h1s z04`$ggfT7<0h?V&E<0PiFV5k|N`N|+`w;yg z0xl)LFw(~mvNy$7uXEFN~$Kc z)N{x&r<`-iN^-q~5=$z%lu}D8y@ncVs=1b0YpcEa7U00tax1O2)_V7*Gf`)(PW#TC z;YS#8q>)D%b+pkZ@R@O@nP-`Gw%M0oVL=lsud?cDt8ZsEOtIrmJMXgVZo3OulOR!& zWGPamNqff3lrkL8d?TF@MwP zqdw+uI(^i~e0NDtAM@QMJ$=k~m-O^8-(Awv$9#85PapHUPKrOxI}d7iPjhTZTgq;E z^|hgfZHrry`p#lX2G1qge&9Yy`}s|n4tKsK5Hi=cx8$aoL2yA>P% zKIzOK8oUhB^G;G*ke$|LMGR}t7WTqIaP*jzrj+_u%hFYu-!H4L<@27fbeMc=>Qk@# zqGok<@t%8a_%Mi3ic8(K2#>5G6U!~y8%{5y>Gon3_7}rpMx&?3CRA|^%IJ_z8f#J^ z#xlN-0Ud=dZ5GIy?n+0hcEa#KO(0<`m-b3&x|Id>;Dng#@Fol8skfGqa#(zIn02|B z=+fGz%;+_B@RxIGe7Y<>n}YA0FSxKD7KbOBR~s4_JUtRkfbg6Cci(efvDk(EW~|et zQ2&;yUmfi3zoOgrojr6-#ceOHa4{%pqpecY^kP=_LKJ59tMro6qQHTc8FvT^UsB#G zZtW$GM$2nldge({m*B7|3@(gg<|KCO;^q(I%55A4lCa_2d`{cyyl&6sb>WMt88uT{ zjc;jFzJ-zwG$jXH8W!6VYU*PUL9jC&=AYV|ha-^mUm+D_gya&L-GoIH0W88rsQv`C z+)z*G%5%l$;Ua>E5U|@v;lz?;S<07>Mu35*WL4&`f)#1nM{Eg0 z)IwS2&InqOKWBsuP(fA-%`dbN8w=7hpNL?BVvBVlAEfQAf_FnsJsF-NCPFW>J!@hY zX;q=@)W<}dtju161fkf%Y*1Rlthi%e7=RFBamgCe&s`?X32bEdHJ38sE>oeQh^Zh3 zAMd)b2U7x$-lG`sxFvwDGOl)3XVJ_y0E|u{c`Xq!R#3S2_3#A z!w}F|wt@Qq!DQ%0*?Zv51In^~;LDkHG zDRA+vx4FzjC}$S6;)B*Id2VnvZNIR_&SPwr?pk6eQ4UVG<`5Kpfa>z4{2^g>EdPK7 zf9HPl6&C!-{hCyopoW4qY8WT?C@RaCwZu4p_Y&r;lAR8|CSy zR-uo^uoC8H3W-kaCP}T#Mbry+evZ9KlSd=X*~T^(FND{+9f7bt92&giq*X2hkq#}z zyYx9gE)|~(8)yx&(eSqI-YbH?5CrBUMS!;blO$-2PBB!2t6Clm8*7AsR!4FYCu*pv z0~U_Y;0%!YbSqxX6ZT}33k7^qe`%ayPKpwMd>|=ygxEt-a#e<-)E~h>ph~r$VZX4` zrsyOy&#oMdj@Bs>YbdKhLsvxLSbT|x){CfN?mo4~Q@yAonv&SI3^{s;1|yGI?T}Cb zC2!m^d;zGa4L-zJ&!Bb2rqlVYYcyyS$=pGqrkDH?PcF^ZDqg+yEXmDKgn^3oK)3=e z3giWayMMCKJMA}02na1pKu6Q8A@t15lBcjmR=!mhi7b5x`jqhNdjg9jK$bOo7#DW+ zBO;-KN;DU|CJK4d6__vp5vBBQRY?Sv%Itw_Fn4jTAwgqjSAI+F9AbiwvNBoH{+e3w zM;!TJGOd{itis(>?@iHYIT8SMa!D7o1xP%COvOtG?EjgeYwaU?{oag`TMj2tSX85I zBRDaR;~`3W?~Y0O#5XgQlwBu1(opZXwup&uxHh$PI&`mF3xb|jG|;5vhiQ5UAM=~? z)9)G1{6Xjak>6en8J5S%_>6MU1;MMYMW`Q2(lQo4DcuQ4;&2A>MyygsaAMSVSzc9B zh*)VZ)aid0Xc8)oWssV^O1SOT8@`{G%qa6R3LV{5o*)3vuFIO@kQVAgG&l+UM%iB% zn&q*x!ib!S`V%$~3*JhKObI7B-EtxQ^nDNiRB%j4;{vhf?L*!19Tk6~#>}s~)4$uC znqPOPpKneTPKyFCZPwC0P_?-XJ=PCZOW;y#=r)2p^j!kr-A(%C5S3I~NH#uix z%2T_J_DQ;DAfq6T+nGJ?D>mkxy8MQWFkjnDeVAq+SoDpv`R`1C- z57QU$LsXnMq*hS%?2_2Ab{$+vZQL)cm@P*!&_sH@%hGqGj*sX|Q$^`{nW|G=FT^yh zC;>ZMF@D?He69KCtiyL*qjcYshW zGRt?1$8Vf-E(<&}WF`~y#35oa*ThN_v%IMh zPZ5VzO{aVz<*~|ni?dcNv-&;x3xgSbWtr+gyS1{D6JAm<3bemC&c`qi*ad18$N4^XoZ1QCe+I7f zhQCw=W#$(7O#>TsJgj54hX`2A&MrlwHYBQ^;n4_cQvYG|+bobgy~6)%S7w z03@la_ziGy2#n+?d%eZGJDYp^_e{ONA3k_;lIbFEEdT%j24YJ`L;(K){{a7>y{D4^ z000SaNLh0L016ZU016ZV^=n(?00007bV*G`2jm772?Y`;nIF3V00Q+%L_t(o!|j(% zNK{c2$A2?wW<`EtN*`9Jh$w`bh6$CTAc$6`wkWJk(ib>p6uM{;N(C~4qF^KllBnn+ zj222nWMgSkW@QvbX<&+Cs59e7O^c_z_hx)E&rzv`2j1eGckey_d+z_7doHZYKNgLR zMP~^HN`Yj+YnA*=0PR3Add(+g06I$u&;|Ic(T2x?IP{v&iw3RLfRZ(I&cfOyQiC;s zWL3*p7QRLKvn&uP_5j#D^Bt~GkpO)c;c2b_;?92fdJP_B!`Yoyz?K%74_CE2z8MAQ z)4|tMFd1iod58{$-GRcF5)0?vLqQ`LrqzW*t7cF`8Wf!poiCj}pw+^G2zYP>GW}Nx zK=mQWNd&FLb-U{)1F#r=&@;QE9P?ZNU9xDWJ)DO<2f=1%Jvk%frA*o&nL(wY; zyaAQ1Fl7?Q`FlfImSaxX1(2T#8#jo0TPGycLi>dL49pg|{0uHsfzd45<0Ij?zmouh z+#ohw)Q3hOv(-N2`XA8HAz`=+oQQD}!09mXa2NH52AH)tE)V6MFf%9GBK9~5AUHs- zcYjgG>V6}P{StLwFW|Qm0JgZxb@QD1>upXl*4@oM5fuSU&dBvZwfCY0Hc8UYG-IDP ziod>(%JqX`>ilPH1D~y;K0M|mfaU=?nT|w5l!xQpUrGiS$*Mgcog`tu8D72z zEt$?@!`E9eP5!dhyKwIS#MUTepk6HQ4lkqYJy6jCvsP*7=Pl&- zc3;~$UHx#Z)^7SQuOHAE;PoIBW*TfYg`<-d@>oyW&b4xtk400000NkvXXu0mjfN"] +edition = "2018" +build= "build.rs" +description = "A remote control software." + +[features] +inline = [] +cli = [] +use_samplerate = ["samplerate"] +use_rubato = ["rubato"] +use_dasp = ["dasp"] +default = ["use_dasp"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +whoami = "1.2" +scrap = { path = "libs/scrap" } +hbb_common = { path = "libs/hbb_common" } +enigo = { path = "libs/enigo" } +sys-locale = "0.1" +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +cfg-if = "1.0" +lazy_static = "1.4" +sha2 = "0.10" +repng = "0.2" +libc = "0.2" +parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } +flexi_logger = "0.22" +runas = "0.2" +magnum-opus = { git = "https://github.com/open-trade/magnum-opus" } +dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } +rubato = { version = "0.10", optional = true } +samplerate = { version = "0.2", optional = true } +async-trait = "0.1" +crc32fast = "1.3" +uuid = { version = "0.8", features = ["v4"] } +clap = "3.0" +rpassword = "5.0" +base64 = "0.13" +sysinfo = "0.23" +num_cpus = "1.13" + +[target.'cfg(not(any(target_os = "android")))'.dependencies] +cpal = { git = "https://github.com/open-trade/cpal" } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +machine-uid = "0.2" +mac_address = "1.1" +sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" } +ctrlc = "3.2" +arboard = "2.0" +clipboard-master = "3.1" +#rdev = { path = "../rdev" } +rdev = { git = "https://github.com/open-trade/rdev" } + +[target.'cfg(target_os = "windows")'.dependencies] +systray = { git = "https://github.com/liyue201/systray-rs" } +winapi = { version = "0.3", features = ["winuser"] } +winreg = "0.10" +windows-service = "0.4" + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +cocoa = "0.24" +dispatch = "0.2" +core-foundation = "0.9" +core-graphics = "0.22" +notify = "4.0.17" +include_dir = "0.7.2" +[target.'cfg(target_os = "linux")'.dependencies] +libpulse-simple-binding = "2.24" +libpulse-binding = "2.25" +rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.10" + +[workspace] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo"] + +[package.metadata.winres] +LegalCopyright = "Copyright © 2020" +# this FileDescription overrides package.description +FileDescription = "RustDesk" + +[target.'cfg(target_os="windows")'.build-dependencies] +winres = "0.1" +winapi = { version = "0.3", features = [ "winnt" ] } + +[build-dependencies] +cc = "1.0" +hbb_common = { path = "libs/hbb_common" } + +[dev-dependencies] +hound = "3.4" + +[package.metadata.bundle] +name = "RustDesk" +identifier = "com.carriez.rustdesk" +icon = ["32x32.png", "128x128.png", "128x128@2x.png"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio"] +osx_minimum_system_version = "10.14" + +#https://github.com/johnthagen/min-sized-rust +#!!! rembember call "strip target/release/rustdesk" +# which reduce binary size a lot +[profile.release] +#lto = true +#codegen-units = 1 +#panic = 'abort' +#opt-level = 'z' # only have smaller size after strip diff --git a/rust-rdp/rust-desk/Dockerfile b/rust-rdp/rust-desk/Dockerfile new file mode 100644 index 0000000..94d713f --- /dev/null +++ b/rust-rdp/rust-desk/Dockerfile @@ -0,0 +1,20 @@ +FROM debian + +WORKDIR / +RUN apt update -y && apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo + +RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 +RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics +RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus + +RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user +WORKDIR /home/user +RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +USER user +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh +RUN chmod +x rustup.sh +RUN ./rustup.sh -y + +USER root +COPY ./entrypoint / +ENTRYPOINT ["/entrypoint"] diff --git a/rust-rdp/rust-desk/LICENSE b/rust-rdp/rust-desk/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/rust-rdp/rust-desk/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/rust-rdp/rust-desk/README-DE.md b/rust-rdp/rust-desk/README-DE.md new file mode 100644 index 0000000..2ac9a6b --- /dev/null +++ b/rust-rdp/rust-desk/README-DE.md @@ -0,0 +1,162 @@ +

+ RustDesk - Your remote desktop
+
Server • + Kompilieren • + Docker • + Dateistruktur • + Screenshots
+ [English] | [中文] | [Español] | [Français] | [Nederlands] | [Polski] | [日本語] | [Русский] | [Português]
+ Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren +

+ +Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Das hier ist ein Programm was man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren rendezvous/relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/blog/id-relay-set/) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. + +[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Kostenlose öffentliche Server + +Hier sind die Server die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. + +| Standort | Serverart | Spezifikationen | Kommentare | +| --------- | ------------- | ------------------ | ---------- | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | +| Singapore | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## Abhängigkeiten + +Die Desktop Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Die groben Schritte zum Kompilieren + +- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor + +- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu + + - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` + - Linux/MacOS: `vcpkg install libvpx libyuv opus` + +- Nutze `cargo run` + +## Kompilieren auf Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### vcpkg installieren + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### libvpx reparieren (Für Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Kompilieren + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +cargo run +``` + +### Ändere Wayland zu X11 (Xorg) + +RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen. + +## Auf Docker Kompilieren + +Beginne damit das Repository zu klonen und den Docker Container zu bauen: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: + +```sh +target/debug/rustdesk +``` + +Oder, wenn du eine Releaseversion benutzt: + +```sh +target/release/rustdesk +``` + +Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z.B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. + +## Dateistruktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code + +## Screenshots + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-ES.md b/rust-rdp/rust-desk/README-ES.md new file mode 100644 index 0000000..d0e1409 --- /dev/null +++ b/rust-rdp/rust-desk/README-ES.md @@ -0,0 +1,160 @@ +

+ RustDesk - Your remote desktop
+ Servidores • + Compilar • + Docker • + Estructura • + Captura de pantalla
+ [English] | [中文] | [Deutsch] | [Français] | [Nederlands] | [Polski] | [日本語] | [Русский] | [Português]
+ Necesitamos tu ayuda para traducir este README a tu idioma +

+ +Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/blog/id-relay-set/), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk agradece la contribución de todo el mundo. Ve [`CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda inicial. + +[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) + +## Servidores gratis de uso público + +A continuación se muestran los servidores que está utilizando de forma gratuita, puede cambiar en algún momento. Si no estás cerca de uno de ellos, tu red puede ser lenta. + +- Seoul, AWS lightsail, 1 VCPU/0.5G RAM +- Singapore, Vultr, 1 VCPU/1G RAM +- Dallas, Vultr, 1 VCPU/1G RAM + +## Dependencies + +La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo.. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Pasos para compilar desde el inicio + +- Prepara el entono de desarrollode Rust y el entorno de compilación de C++ y Rust. + +- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/Osx: vcpkg install libvpx libyuv opus + +- run `cargo run` + +## Como compilar en linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Install vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Soluciona libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Compila + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +cargo run +``` + +### Cambia Wayland a X11 (Xorg) + +RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. + +## Como compilar con Docker + +Empieza clonando el repositorio y compilando el contenedor de docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos a la orden de compilación, puede hacerlo al final de la linea de comandos en el apartado``. Por ejemplo, si desea compilar una versión optimizada para publicación, deberá ejecutar el comando anterior seguido de `---release`. El ejecutable resultante estará disponible en la carpeta de destino en su sistema, y puede ser ejecutado con: + +```sh +target/debug/rustdesk +``` + +O si estas ejecutando una versión para su publicación: + +```sh +target/release/rustdesk +``` + +Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También hay que tener en cuenta que otros subcomandos de carga como `install` o `run` no estan actualmente soportados via este metodo y podrían requerir ser instalados dentro del contenedor y no en el host. + +## Estructura de archivos + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, configuración, tcp/udp wrapper, protobuf, fs funciones para transferencia de ficheros, y alguna función de utilidad. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control específico por cada plataforma para el teclado/ratón +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/entrada/servicios de video, y conexiones de red +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma + +## Captura de pantalla + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-FI.md b/rust-rdp/rust-desk/README-FI.md new file mode 100644 index 0000000..86e87be --- /dev/null +++ b/rust-rdp/rust-desk/README-FI.md @@ -0,0 +1,161 @@ +

+ RustDesk - Etätyöpöytäsi
+ Palvelimet • + Rakenna • + Docker • + Rakenne • + Tilannevedos
+ [中文] | [Español] | [Français] | [Deutsch] | [Nederlands] | [Polski] | [日本語] | [Русский] | [Português] | [Suomi]
+ Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi +

+ +Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetuksia. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/blog/id-relay-set/), tai [kirjoita oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi. + +[**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases) + +## Vapaita julkisia palvelimia + +Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas. +| Sijainti | Myyjä | Määrittely | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapore | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## Riippuvuudet + +Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Rakentamisaskeleet harppoen + +- Valmistele Rust-kehitysympäristö ja C++-rakentamisympäristö + +- Asenna [vcpkg](https://github.com/microsoft/vcpkg), ja aseta `VCPKG_ROOT`-ympäristömuuttuja oikein + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- aja `cargo run` + +## Kuinka rakentaa Linuxissa + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Asenna vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Korjaa libvpx (Fedora-linux-versiota varten) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Rakenna + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### Vaihda Wayland-ympäristö X11 (Xorg)-ympäristöön + +RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamaan Xorg oletus GNOME-istuntona. + +## Kuinka rakennetaan Dockerin kanssa + +Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Sitten, joka kerta kun sinun on rakennettava sovellus, aja seuraava komento: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Huomaa, että ensimmäinen rakentaminen saattaa kestää pitempään ennen kuin riippuvuudet on siirretty välimuistiin, seuraavat rakentamiset ovat nopeampia. Lisäksi, jos sinun on määritettävä eri argumentteja rakentamiskomennolle, saatat tehdä sen niin, että komennon lopussa `-kohdassa. Esimerkiksi, jos haluat rakentaa optimoidun julkaisuversion, sinun on ajettava komento yllä siten, että sitä seuraa argumentti `---release`. Suoritettava tiedosto on saatavilla järjestelmäsi kohdehakemistossa, ja se voidaan suorittaa seuraavan kera: + +```sh +target/debug/rustdesk +``` + +Tai, jos olet suorittamassa jakeluversion suoritettavaa tiedostoa: + +```sh +target/release/rustdesk +``` + +Varmista, että suoritat näitä komentoja RustDesktop-tietovaraston juurihakemistossa, muutoin sovellus ei ehkä löydä vaadittuja resursseja. Huomaa myös, että muita cargo-alikomentoja kuten `install` tai `run` ei nykyisin tueta tässä menetelmässä, koska ne asentavat tai suorittavat ohjelman säiliön sisällä eikä isäntäohjelman sisällä. + +## Tiedostorakenne + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs-funktiot tiedostosiirtoon, ja jotkut muut apuohjelmafunktiot +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: näyttökaappaukset +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Graafinen käyttöliittymä +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code + +## Tilannekuvat + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-FR.md b/rust-rdp/rust-desk/README-FR.md new file mode 100644 index 0000000..2cf4e81 --- /dev/null +++ b/rust-rdp/rust-desk/README-FR.md @@ -0,0 +1,160 @@ +

+ RustDesk - Your remote desktop
+ Serveurs - + Build - + Docker - + Structure - + Images
+ [English] | [中文] | [Deutsch] | [Española] | [Nederlands] | [Polski] | [日本語] | [Русский] | [Português]
+ Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle. +

+ +Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/blog/id-relay-set/), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk accueille les contributions de tout le monde. Voir [`CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations. + +[**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases) + +## Serveurs publics libres + +Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent. + +- Séoul, AWS lightsail, 1 VCPU/0.5G RAM +- Singapour, Vultr, 1 VCPU/1G RAM +- Dallas, Vultr, 1 VCPU/1G RAM + +## Dépendances + +Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Étapes brutes de la compilation/build + +- Préparez votre environnement de développement Rust et votre environnement de compilation C++. + +- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`. + + - Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/Osx : vcpkg install libvpx libyuv opus + +- Exécuter `cargo run` + +## Comment compiler/build sous Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Installer vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Corriger libvpx (Pour Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Construire + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p cible/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +Exécution du cargo +``` + +### Changer Wayland en X11 (Xorg) + +RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) pour configurer Xorg comme la session GNOME par défaut. + +## Comment construire avec Docker + +Commencez par cloner le dépôt et construire le conteneur Docker : + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Ensuite, chaque fois que vous devez build le logiciel, exécutez la commande suivante : + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Notez que le premier build peut prendre plus de temps avant que les dépendances ne soient mises en cache, les constructions suivantes seront plus rapides. De plus, si vous devez spécifier différents arguments à la commande de compilation, vous pouvez le faire à la fin de la commande dans la position ``. Par exemple, si vous voulez construire une version optimisée de la version release, vous devez exécuter la commande ci-dessus suivie de `---release`. L'exécutable résultant sera disponible dans le dossier cible sur votre système, et peut être lancé avec : + +```sh +target/debug/rustdesk +``` + +Ou, si vous exécutez un exécutable provenant d'une release : + +```sh +target/release/rustdesk +``` + +Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du référentiel RustDesk, sinon l'application ne pourra pas trouver les ressources requises. Notez également que les autres sous-commandes de cargo telles que `install` ou `run` ne sont pas actuellement supportées par cette méthode car elles installeraient ou exécuteraient le programme à l'intérieur du conteneur au lieu de l'hôte. + +## Structure du projet + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)** : codec vidéo, config, wrapper tcp/udp, protobuf, fonctions fs pour le transfert de fichiers, et quelques autres fonctions utilitaires. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)** : capture d'écran +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)** : contrôle clavier/souris spécifique à la plate-forme +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)** : INTERFACE GRAPHIQUE +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)** : services audio/clipboard/input/vidéo, et connexions réseau +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)** : démarrer une connexion entre pairs +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée. +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme + +## Images + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-JP.md b/rust-rdp/rust-desk/README-JP.md new file mode 100644 index 0000000..e4d0ed7 --- /dev/null +++ b/rust-rdp/rust-desk/README-JP.md @@ -0,0 +1,164 @@ +

+ RustDesk - Your remote desktop
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [中文] | [Español] | [Français] | [Deutsch] | [Nederlands] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Русский] | [Português]
+ このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。 +

+ +Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/blog/id-relay-set/) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo). + +RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。 + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## 無料のパブリックサーバー + +下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。 +| Location | Vendor | Specification | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapore | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## 依存関係 + +デスクトップ版ではGUIに [sciter](https://sciter.com/) が使われています。 sciter dynamic library をダウンロードしてください。 + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## ビルド手順 + +- Rust開発環境とC ++ビルド環境を準備します + +- [vcpkg](https://github.com/microsoft/vcpkg), をインストールし、 `VCPKG_ROOT` 環境変数を正しく設定します。 + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- run `cargo run` + +## Linuxでのビルド手順 + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Install vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Fix libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### Wayland の場合、X11(Xorg)に変更します + +RustDeskはWaylandをサポートしていません。 + [こちら](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) を確認して、XorgをデフォルトのGNOMEセッションとして構成します。 + +## Dockerでビルドする方法 + +リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。 + + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +その後、アプリケーションをビルドする必要があるたびに、以下のコマンドを実行します。 + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +なお、最初のビルドでは、依存関係がキャッシュされるまで時間がかかることがありますが、その後のビルドではより速くなります。さらに、ビルドコマンドに別の引数を指定する必要がある場合は、コマンドの最後にある `` の位置で指定することができます。例えば、最適化されたリリースバージョンをビルドしたい場合は、上記のコマンドの後に +`---release` を実行します。できあがった実行ファイルは、システムのターゲット・フォルダに格納され、次のコマンドで実行できます。 + +```sh +target/debug/rustdesk +``` + +あるいは、リリース用の実行ファイルを実行している場合: + +```sh +target/release/rustdesk +``` + +これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。 + +## File Structure + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有のキーボード/マウスコントロール +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: オーディオ/クリップボード/入力/ビデオサービス、ネットワーク接続 +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続の開始 +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。 +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード + +## Snapshot + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-ML.md b/rust-rdp/rust-desk/README-ML.md new file mode 100644 index 0000000..4348bd1 --- /dev/null +++ b/rust-rdp/rust-desk/README-ML.md @@ -0,0 +1,161 @@ +

+ RustDesk - Your remote desktop
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [中文] | [Español] | [Français] | [Deutsch] | [Nederlands] | [Polski] | [Suomi] | [日本語] | [Русский] | [Português]
+ ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ് +

+ +ഞങ്ങളുമായി ചാറ്റ് ചെയ്യുക: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ. ബോക്‌സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/blog/id-relay-set/), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo). + +എല്ലാവരുടെയും സംഭാവനയെ RustDesk സ്വാഗതം ചെയ്യുന്നു. ആരംഭിക്കുന്നതിനുള്ള സഹായത്തിന് [`CONTRIBUTING.md`](CONTRIBUTING.md) കാണുക. + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## സൗജന്യ പൊതു സെർവറുകൾ + +നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്‌വർക്ക് സ്ലോ ആയേക്കാം. +| സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapore | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## ഡിപെൻഡൻസികൾ + +ഡെസ്‌ക്‌ടോപ്പ് പതിപ്പുകൾ GUI-യ്‌ക്കായി [sciter](https://sciter.com/) ഉപയോഗിക്കുന്നു, ദയവായി സ്‌സൈറ്റർ ഡൈനാമിക് ലൈബ്രറി സ്വയം ഡൗൺലോഡ് ചെയ്യുക. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## നിർമ്മിക്കാനുള്ള അസംസ്കൃത പടികൾ + +- നിങ്ങളുടെ Rust development envയും and C++ build envയും തയ്യാറാക്കുക + +- [vcpkg](https://github.com/microsoft/vcpkg) ഇൻസ്റ്റാൾ ചെയ്ത് `VCPKG_ROOT` env വേരിയബിൾ ശരിയായി സജ്ജമാക്കുക + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- run `cargo run` + +## ലിനക്സിൽ എങ്ങനെ നിർമ്മിക്കാം + +### ഉബുണ്ടു 18 (ഡെബിയൻ 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### ഫെഡോറ 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### ആർച് (മഞ്ചാരോ) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### vcpkg ഇൻസ്റ്റാൾ ചെയ്യുക + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### libvpx പരിഹരിക്കുക (ഫെഡോറയ്ക്ക്) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### നിർമാണം + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### വേലാൻഡ് X11 (Xorg) ആയി മാറ്റുക + +RustDesk Wayland-നെ പിന്തുണയ്ക്കുന്നില്ല. സ്ഥിരസ്ഥിതി ഗ്നോം സെഷനായി Xorg കോൺഫിഗർ ചെയ്യുന്നതിന് [ഇത്](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) പരിശോധിക്കുക. + +## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം + + റെപ്പോസിറ്റോറി ക്ലോണുചെയ്‌ത് ഡോക്കർ കണ്ടെയ്‌നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +തുടർന്ന്, ഓരോ തവണയും നിങ്ങൾ ആപ്ലിക്കേഷൻ നിർമ്മിക്കേണ്ടതുണ്ട്, ഇനിപ്പറയുന്ന കമാൻഡ് പ്രവർത്തിപ്പിക്കുക: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +ഡിപൻഡൻസികൾ കാഷെ ചെയ്യുന്നതിനുമുമ്പ് ആദ്യ ബിൽഡ് കൂടുതൽ സമയമെടുത്തേക്കാം, തുടർന്നുള്ള ബിൽഡുകൾ വേഗത്തിലാകും. കൂടാതെ, നിങ്ങൾക്ക് ബിൽഡ് കമാൻഡിലേക്ക് വ്യത്യസ്ത ആർഗ്യുമെന്റുകൾ വ്യക്തമാക്കണമെങ്കിൽ, കമാൻഡിന്റെ അവസാനം `` സ്ഥാനത്ത് നിങ്ങൾക്ക് അങ്ങനെ ചെയ്യാം. ഉദാഹരണത്തിന്, നിങ്ങൾ ഒരു ഒപ്റ്റിമൈസ് ചെയ്ത റിലീസ് പതിപ്പ് നിർമ്മിക്കാൻ ആഗ്രഹിക്കുന്നുവെങ്കിൽ, മുകളിലുള്ള കമാൻഡ് തുടർന്ന് `---release` നിങ്ങൾ പ്രവർത്തിപ്പിക്കും. തത്ഫലമായുണ്ടാകുന്ന എക്സിക്യൂട്ടബിൾ നിങ്ങളുടെ സിസ്റ്റത്തിലെ ടാർഗെറ്റ് ഫോൾഡറിൽ ലഭ്യമാകും, കൂടാതെ ഇത് ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കാം: + +```sh +target/debug/rustdesk +``` + +അല്ലെങ്കിൽ, നിങ്ങൾ ഒരു റിലീസ് എക്സിക്യൂട്ടബിൾ പ്രവർത്തിപ്പിക്കുകയാണെങ്കിൽ: + +```sh +target/release/rustdesk +``` + +RustDesk റിപ്പോസിറ്ററിയുടെ റൂട്ടിൽ നിന്നാണ് നിങ്ങൾ ഈ കമാൻഡുകൾ പ്രവർത്തിപ്പിക്കുന്നതെന്ന് ദയവായി ഉറപ്പാക്കുക, അല്ലാത്തപക്ഷം ആപ്ലിക്കേഷന് ആവശ്യമായ ഉറവിടങ്ങൾ കണ്ടെത്താൻ കഴിഞ്ഞേക്കില്ല. ഹോസ്റ്റിന് പകരം കണ്ടെയ്‌നറിനുള്ളിൽ പ്രോഗ്രാം ഇൻസ്റ്റാൾ ചെയ്യുകയോ പ്രവർത്തിപ്പിക്കുകയോ ചെയ്യുന്നതിനാൽ, `install` അല്ലെങ്കിൽ `run` പോലുള്ള മറ്റ് കാർഗോ സബ്‌കമാൻഡുകൾ നിലവിൽ ഈ രീതിയെ പിന്തുണയ്ക്കുന്നില്ല എന്നതും ശ്രദ്ധിക്കുക. + +## ഫയൽ ഘടന + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code + +## സ്നാപ്പ്ഷോട്ടുകൾ + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-NL.md b/rust-rdp/rust-desk/README-NL.md new file mode 100644 index 0000000..6d8b51e --- /dev/null +++ b/rust-rdp/rust-desk/README-NL.md @@ -0,0 +1,163 @@ +

+ RustDesk - Jouw verbinding op afstand
+ Servers • + Bouwen • + Docker • + Structuur • + Snapshot
+ [中文] | [Español] | [Français] | [Deutsch] | [Nederlands] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Русский] | [Português]
+ We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal +

+ +Praat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Nog weer een applicatie voor toegang op afstand, geschreven in Rust. Werkt meteen, geen configuratie nodig. Je hebt volledig beheer over je data, zonder na te hoeven denken over veiligheid. Je kunt onze rendez-vous/relay-server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk verwelkomt bijdragen van iedereen. Zie [`CONTRIBUTING.md`](CONTRIBUTING.md) om te lezen hoe je van start kunt gaan. + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Gratis openbare servers + +Onderstaande servers zijn de servers die je gratis kunt gebruiken, ze kunnen op termijn veranderen. Als je niet fysiek dichtbij een van deze servers bent, kan je verbinding traag werken. +| Locatie | Aanbieder | Specificaties | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapore | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## Afhankelijkheden + +Desktopversies gebruiken [sciter](https://sciter.com/) voor de grafische schil. Gelieve zelf de sciter-library te downloaden. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Handmatige bouwinstructies + +- Bereid je Rust-ontwikkelomgeving en C++-bouwomgeving voor. + +- Installeer [vcpkg](https://github.com/microsoft/vcpkg) en configureer de `VCPKG_ROOT` omgevingsvariabele op de juiste manier: + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- Voer uit: `cargo run` + +## Bouwen op Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Installatie van vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Fix voor libvpx (voor Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Bouwen + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### Wissel van Wayland naar X11 (Xorg) + +RustDesk ondersteunt Wayland niet. Lees [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) hoe je Xorg als standaardsessie kunt instellen voor GNOME. + +## Bouwen met Docker + +Kloon eerst deze repository en bouw de Docker-container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Voer vervolgens de volgende commando's uit iedere keer dat je de applicatie opnieuw moet bouwen: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Let op dat de eerste build langer kan duren omdat de dependencies nog niet zijn gecached; latere builds zullen sneller zijn. Als je extra command line arguments wilt toevoegen aan het build-commando, dan kun je dat doen aan het einde van de opdrachtregel in plaats van ``. Bijvoorbeeld: als je een geoptimaliseerde releaseversie wilt bouwen, draai dan het bovenstaande commando gevolgd door `---release`. + +Het uitvoerbare bestand, in debug-modus, zal verschijnen in de target-map, en kan als volgt worden uitgevoerd: + +```sh +target/debug/rustdesk +``` + +Als je een release-versie hebt gebouwd, is het commando als volgt: + +```sh +target/release/rustdesk +``` + +Zorg ervoor dat je deze commando's van de root van de RustDesk-repository uitvoert, anders kan het programma de nodige afhankelijkheden mogelijk niet vinden. Let ook op dat andere cargo-subcommando's zoals `install` en `run` zijn momenteel niet ondersteund, aangezien deze zouden worden uitgevoerd in een container in plaats van op de host. + +## Bestandsstructuur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: videocodec, configuratie, TCP/UDP-wrapper, protobuf, bestandssysteemfuncties voor bestandsoverdracht en nog wat andere nuttige functies +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: schermopname +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platformspecifieke muis- en toetsenbordbeheer +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: geluids-, klembord-, invoer- en video-services, netwerkverbindingen +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: voor het opzetten van peer-verbindingen +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicatie met [rustdesk-server](https://github.com/rustdesk/rustdesk-server), afwachten van redirect op afstand (TCP hole punching) of een relayed verbinding +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platformspecifieke code + +## Snapshot + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-PL.md b/rust-rdp/rust-desk/README-PL.md new file mode 100644 index 0000000..ed937da --- /dev/null +++ b/rust-rdp/rust-desk/README-PL.md @@ -0,0 +1,161 @@ +

+ RustDesk - Your remote desktop
+ Serwery • + Kompilacja • + Docker • + Struktura • + Snapshot
+ [English] | [中文] | [Deutsch] | [Española] | [Français] | [Nederlands] | [日本語] | [Русский] | [Português]
+ Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język +

+ +Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/blog/id-relay-set/), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk zaprasza do współpracy każdego. Zobacz [`CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu. + +[**POBIERZ KOMPILACJE**](https://github.com/rustdesk/rustdesk/releases) + +## Darmowe Serwery Publiczne + +Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska. +| Lokalizacja | Dostawca | Specyfikacja | +| --------- | ------------- | ------------------ | +| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapur | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## Zależności + +Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać bibliotekę dynamiczną sciter samodzielnie. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Podstawowe kroki do kompilacji. + +- Przygotuj środowisko programistyczne Rust i środowisko programowania C++ + +- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw `VCPKG_ROOT` env zmienną prawidłowo + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- uruchom `cargo run` + +## Jak Kompilować na Linuxie + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Zainstaluj vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Fix libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Kompilacja + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +cargo run +``` + +### Zmień Wayland na X11 (Xorg) + +RustDesk nie obsługuje Waylanda. Sprawdź [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) by skonfigurować Xorg jako domyślną sesję GNOME. + +## Jak kompilować za pomocą Dockera + +Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Następnie, za każdym razem, gdy potrzebujesz skompilować aplikację, uruchom następujące polecenie: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Zauważ, że pierwsza kompilacja może potrwać dłużej zanim zależności zostaną zbuforowane, kolejne będą szybsze. Dodatkowo, jeśli potrzebujesz określić inne argumenty dla polecenia budowania, możesz to zrobić na końcu komendy w miejscu ``. Na przykład, jeśli chciałbyś zbudować zoptymalizowaną wersję wydania, uruchomiłbyś powyższą komendę a następnie `---release`. Powstały plik wykonywalny będzie dostępny w folderze docelowym w twoim systemie, i może być uruchomiony z: + +```sh +target/debug/rustdesk +``` + +Lub, jeśli uruchamiasz plik wykonywalny wersji: + +```sh +target/release/rustdesk +``` + +Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium RustDesk, w przeciwnym razie aplikacja może nie być w stanie znaleźć wymaganych zasobów. Należy również pamiętać, że inne podpolecenia ładowania, takie jak `install` lub `run` nie są obecnie obsługiwane za pomocą tej metody, ponieważ instalowałyby lub uruchamiały program wewnątrz kontenera zamiast na hoście. + +## Struktura plików + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, config, wrapper tcp/udp, protobuf, funkcje fs do transferu plików i kilka innych funkcji użytkowych +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: przechwytywanie ekranu +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: specyficzne dla danej platformy sterowanie klawiaturą/myszą +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/schowek/wejście(input)/wideo oraz połączenia sieciowe +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: uruchamia połączenie peer +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: specyficzny dla danej platformy kod + +## Migawki(Snapshoty) + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README-ZH.md b/rust-rdp/rust-desk/README-ZH.md new file mode 100644 index 0000000..8b51c63 --- /dev/null +++ b/rust-rdp/rust-desk/README-ZH.md @@ -0,0 +1,213 @@ +

+ RustDesk - Your remote desktop
+ 服务器 • + 编译 • + Docker • + 结构 • + 截图
+ [English] | [Español] | [Français] | [Deutsch] | [Nederlands] | [Polski] | [日本語] | [Русский] | [Português]
+

+ +Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +远程桌面软件,开箱即用,无需任何配置。您完全掌控数据,不用担心安全问题。您可以使用我们的注册/中继服务器, +或者[自己设置](https://rustdesk.com/blog/id-relay-set/), +亦或者[开发您的版本](https://github.com/rustdesk/rustdesk-server-demo)。 + +欢迎大家贡献代码, 请看 [`CONTRIBUTING.md`](CONTRIBUTING.md). + +[**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases) + +## 免费公共服务器 + +以下是您免费使用的服务器,它可能会随着时间的推移而变化。如果您不靠近其中之一,您的网络可能会很慢。 + +- 首尔, AWS lightsail, 1 VCPU/0.5G RAM +- 新加坡, Vultr, 1 VCPU/1G RAM +- 达拉斯, Vultr, 1 VCPU/1G RAM + +## 依赖 + +桌面版本界面使用[sciter](https://sciter.com/), 请自行下载。 + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## 基本构建步骤 + +- 请准备好 Rust 开发环境和 C++编译环境 + +- 安装[vcpkg](https://github.com/microsoft/vcpkg), 正确设置`VCPKG_ROOT`环境变量 + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/Osx: vcpkg install libvpx libyuv opus + +- 运行 `cargo run` + +## 在 Linux 上编译 + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### 安装 vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### 修复 libvpx (仅仅针对 Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### 构建 + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +cargo run +``` + +### 把 Wayland 修改成 X11 (Xorg) + +RustDesk 暂时不支持 Wayland,不过正在积极开发中. +请查看[this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)配置 X11. + +## 使用 Docker 编译 + +首先克隆存储库并构建 docker 容器: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +针对国内网络访问问题,可以做以下几点优化: + +1. Dockerfile 中修改系统的源到国内镜像 + + ``` + 在Dockerfile的RUN apt update之前插入两行: + + RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list + RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list + ``` + +2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: + + ``` + RUN echo '[source.crates-io]' > ~/.cargo/config \ + && echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \ + && echo '# 替换成你偏好的镜像源' >> ~/.cargo/config \ + && echo "replace-with = 'sjtu'" >> ~/.cargo/config \ + && echo '# 上海交通大学' >> ~/.cargo/config \ + && echo '[source.sjtu]' >> ~/.cargo/config \ + && echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \ + && echo '' >> ~/.cargo/config + ``` + +3. Dockerfile 中加入代理的 env + + ``` + 在User root后插入两行 + + ENV http_proxy=http://host:port + ENV https_proxy=http://host:port + ``` + +4. docker build 命令后面加上 proxy 参数 + ``` + docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port + ``` + +然后,每次需要构建应用程序时,运行以下命令: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +运行若遇到无权限问题,出现以下提示: + +``` +usermod: user user is currently used by process 1 +groupmod: Permission denied. +groupmod: cannot lock /etc/group; try again later. +``` + +可以尝试把`-e PUID="$(id -u)" -e PGID="$(id -g)"`参数去掉。(出现这一问题的原因是容器中的 entrypoint 脚本中判定 uid 和 gid 与给定的环境变量不一致时会修改 user 的 uid 和 gid 重新运行,但是重新运行时取不到环境变量中的 uid 和 gid 了,会再次进入 uid 与 gid 与给定值不一致的逻辑分支) + +请注意,第一次构建可能需要比较长的时间,因为需要缓存依赖项(国内网络经常出现拉取失败,可多尝试几次),后续构建会更快。此外,如果您需要为构建命令指定不同的参数, +您可以在命令末尾的 `` 位置执行此操作。例如,如果你想构建一个优化的发布版本,你可以在命令后跟 `---release`。 +将在 target 下产生可执行程序,请通过以下方式运行调试版本: + +```sh +target/debug/rustdesk +``` + +或者运行发布版本: + +```sh +target/release/rustdesk +``` + +请确保您从 RustDesk 存储库的根目录运行这些命令,否则应用程序可能无法找到所需的资源。另请注意,此方法当前不支持其他`Cargo`子命令, +例如 `install` 或 `run`,因为运行在容器里,而不是宿主机上。 + +## 文件结构 + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏 +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video 服务, 已经连接实现 +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继) +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 + +## 截图 + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/README.md b/rust-rdp/rust-desk/README.md new file mode 100644 index 0000000..7d6713d --- /dev/null +++ b/rust-rdp/rust-desk/README.md @@ -0,0 +1,161 @@ +

+ RustDesk - Your remote desktop
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [中文] | [Español] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Русский] | [Português]
+ We need your help to translate this README and RustDesk UI to your native language +

+ +Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/blog/id-relay-set/), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for help getting started. + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Free Public Servers + +Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow. +| Location | Vendor | Specification | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapore | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | + +## Dependencies + +Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Raw steps to build + +- Prepare your Rust development env and C++ build env + +- Install [vcpkg](https://github.com/microsoft/vcpkg), and set `VCPKG_ROOT` env variable correctly + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- run `cargo run` + +## How to build on Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Install vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Fix libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### Change Wayland to X11 (Xorg) + +RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session. + +## How to build with Docker + +Begin by cloning the repository and building the docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Then, each time you need to build the application, run the following command: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Note that the first build may take longer before dependencies are cached, subsequent builds will be faster. Additionally, if you need to specify different arguments to the build command, you may do so at the end of the command in the `` position. For instance, if you wanted to build an optimized release version, you would run the command above followed by `---release`. The resulting executable will be available in the target folder on your system, and can be run with: + +```sh +target/debug/rustdesk +``` + +Or, if you're running a release executable: + +```sh +target/release/rustdesk +``` + +Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application may be unable to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. + +## File Structure + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code + +## Snapshot + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/rust-rdp/rust-desk/SECURITY.md b/rust-rdp/rust-desk/SECURITY.md new file mode 100644 index 0000000..f1114f9 --- /dev/null +++ b/rust-rdp/rust-desk/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| --------- | ------------------ | +| 1.1.x | :white_check_mark: | +| 1.x | :white_check_mark: | +| Below 1.0 | :x: | + +## Reporting a Vulnerability + +Here we should write what to do in case of a security vulnerability diff --git a/rust-rdp/rust-desk/build.rs b/rust-rdp/rust-desk/build.rs new file mode 100644 index 0000000..d3cb36a --- /dev/null +++ b/rust-rdp/rust-desk/build.rs @@ -0,0 +1,74 @@ +#[cfg(windows)] +fn build_windows() { + cc::Build::new().file("src/windows.cc").compile("windows"); + println!("cargo:rustc-link-lib=WtsApi32"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=windows.cc"); +} + +#[cfg(all(windows, feature = "inline"))] +fn build_manifest() { + use std::io::Write; + if std::env::var("PROFILE").unwrap() == "release" { + let mut res = winres::WindowsResource::new(); + res.set_icon("icon.ico") + .set_language(winapi::um::winnt::MAKELANGID( + winapi::um::winnt::LANG_ENGLISH, + winapi::um::winnt::SUBLANG_ENGLISH_US, + )) + .set_manifest_file("manifest.xml"); + match res.compile() { + Err(e) => { + write!(std::io::stderr(), "{}", e).unwrap(); + std::process::exit(1); + } + Ok(_) => {} + } + } +} + +fn install_oboe() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os != "android" { + return; + } + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let target = format!("{}-android-static", target_arch); + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: std::path::PathBuf = vcpkg_root.into(); + path.push("installed"); + path.push(target); + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + println!("cargo:rustc-link-lib=oboe"); + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=OpenSLES"); + // I always got some strange link error with oboe, so as workaround, put oboe.cc into oboe src: src/common/AudioStreamBuilder.cpp + // also to avoid libc++_shared not found issue, cp ndk's libc++_shared.so to jniLibs, e.g. + // ./flutter_hbb/android/app/src/main/jniLibs/arm64-v8a/libc++_shared.so + // let include = path.join("include"); + //cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper"); +} + +fn main() { + #[cfg(all(windows, feature = "inline"))] + build_manifest(); + #[cfg(windows)] + build_windows(); + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-lib=framework=ApplicationServices"); + hbb_common::gen_version(); + install_oboe(); +} diff --git a/rust-rdp/rust-desk/entrypoint b/rust-rdp/rust-desk/entrypoint new file mode 100755 index 0000000..514de9b --- /dev/null +++ b/rust-rdp/rust-desk/entrypoint @@ -0,0 +1,34 @@ +#!/bin/sh + +cd $HOME/rustdesk +. $HOME/.cargo/env + +argv=$@ + +while test $# -gt 0; do + case "$1" in + --release) + mkdir -p target/release + test -f target/release/libsciter-gtk.so || cp $HOME/libsciter-gtk.so target/release/ + release=1 + shift + ;; + --target) + shift + if test $# -gt 0; then + rustup target add $1 + shift + fi + ;; + *) + shift + ;; + esac +done + +if [ -z $release ]; then + mkdir -p target/debug + test -f target/debug/libsciter-gtk.so || cp $HOME/libsciter-gtk.so target/debug/ +fi + +VCPKG_ROOT=/vcpkg cargo build $argv diff --git a/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md b/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a5a2f77 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug, needs investigation +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps or a minimal code example to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - OS: [e.g. Linux, Windows, macOS ..] + - Rust [e.g. rustc --version] + - Library Version [e.g. enigo 0.0.13 or commit hash fa448be ] + +**Additional context** +Add any other context about the problem here. diff --git a/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md b/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4cd9c3f --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement, needs investigation +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/question.md b/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..ef240a0 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,19 @@ +--- +name: Question +about: Ask your Question here +title: '' +labels: question +assignees: '' + +--- + +**Describe your Question** +A clear and concise description of what you want to know. + +**Describe your Goal** +A clear and concise description of what you want to achieve. Consider the [XYProblem](http://xyproblem.info/) + +**Environment (please complete the following information):** + - OS: [e.g. Linux, Windows, macOS ..] + - Rust [e.g. rustc --version] + - Library Version [e.g. enigo 0.0.13 or commit hash fa448be ] diff --git a/rust-rdp/rust-desk/libs/enigo/.gitignore b/rust-rdp/rust-desk/libs/enigo/.gitignore new file mode 100644 index 0000000..0e497c2 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock + +# RustFmt files +**/*.rs.bk + +# intellij +.idea \ No newline at end of file diff --git a/rust-rdp/rust-desk/libs/enigo/.travis.yml b/rust-rdp/rust-desk/libs/enigo/.travis.yml new file mode 100644 index 0000000..0152a83 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/.travis.yml @@ -0,0 +1,15 @@ +language: rust +rust: + - stable + - beta + - nightly +matrix: + allow_failures: + - rust: nightly +before_install: + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get -qq update; fi + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get install -y libxdo-dev; fi +os: + - linux + - osx + diff --git a/rust-rdp/rust-desk/libs/enigo/Cargo.toml b/rust-rdp/rust-desk/libs/enigo/Cargo.toml new file mode 100644 index 0000000..6842dab --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "enigo" +version = "0.0.14" +authors = ["Dustin Bensing "] +edition = "2018" +build = "build.rs" + +description = "Enigo lets you control your mouse and keyboard in an abstract way on different operating systems (currently only Linux, macOS, Win – Redox and *BSD planned)" +documentation = "https://docs.rs/enigo/" +homepage = "https://github.com/enigo-rs/enigo" +repository = "https://github.com/enigo-rs/enigo" +readme = "README.md" +keywords = ["input", "mouse", "testing", "keyboard", "automation"] +categories = ["development-tools::testing", "api-bindings", "hardware-support"] +license = "MIT" + +[badges] +travis-ci = { repository = "enigo-rs/enigo" } +appveyor = { repository = "pythoneer/enigo-85xiy" } + +[dependencies] +serde = { version = "1.0", optional = true } +serde_derive = { version = "1.0", optional = true } +log = "0.4" + +[features] +with_serde = ["serde", "serde_derive"] + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser", "winbase"] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-graphics = "0.22" +objc = "0.2" +unicode-segmentation = "1.6" + +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" + +[build-dependencies] +pkg-config = "0.3" diff --git a/rust-rdp/rust-desk/libs/enigo/LICENSE b/rust-rdp/rust-desk/libs/enigo/LICENSE new file mode 100644 index 0000000..d4b9c09 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 pythoneer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rust-rdp/rust-desk/libs/enigo/README.md b/rust-rdp/rust-desk/libs/enigo/README.md new file mode 100644 index 0000000..df6ca79 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/README.md @@ -0,0 +1,46 @@ +[![Build Status](https://travis-ci.org/enigo-rs/enigo.svg?branch=master)](https://travis-ci.org/enigo-rs/enigo) +[![Build status](https://ci.appveyor.com/api/projects/status/6cd00pajx4tvvl3e?svg=true)](https://ci.appveyor.com/project/pythoneer/enigo-85xiy) +[![Dependency Status](https://dependencyci.com/github/pythoneer/enigo/badge)](https://dependencyci.com/github/pythoneer/enigo) +[![Docs](https://docs.rs/enigo/badge.svg)](https://docs.rs/enigo) +[![Crates.io](https://img.shields.io/crates/v/enigo.svg)](https://crates.io/crates/enigo) +[![Discord chat](https://img.shields.io/discord/315925376486342657.svg)](https://discord.gg/Eb8CsnN) +[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/enigo-rs/Lobby) + + +# enigo +Cross platform input simulation in Rust! + +- [x] Linux (X11) mouse +- [x] Linux (X11) text +- [ ] Linux (Wayland) mouse +- [ ] Linux (Wayland) text +- [x] MacOS mouse +- [x] MacOS text +- [x] Win mouse +- [x] Win text +- [x] Custom Parser + + +```Rust +let mut enigo = Enigo::new(); + +enigo.mouse_move_to(500, 200); +enigo.mouse_click(MouseButton::Left); +enigo.key_sequence_parse("{+CTRL}a{-CTRL}{+SHIFT}Hello World{-SHIFT}"); +``` + +for more look at examples + +Runtime dependencies +-------------------- + +Linux users may have to install libxdo-dev. For example, on Ubuntu: + +```Bash +apt install libxdo-dev +``` +On Arch: + +```Bash +pacman -S xdotool +``` diff --git a/rust-rdp/rust-desk/libs/enigo/appveyor.yml b/rust-rdp/rust-desk/libs/enigo/appveyor.yml new file mode 100644 index 0000000..af3142a --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/appveyor.yml @@ -0,0 +1,121 @@ +# Appveyor configuration template for Rust using rustup for Rust installation +# https://github.com/starkat99/appveyor-rust + +## Operating System (VM environment) ## + +# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. +os: Visual Studio 2015 + +## Build Matrix ## + +# This configuration will setup a build for each channel & target combination (12 windows +# combinations in all). +# +# There are 3 channels: stable, beta, and nightly. +# +# Alternatively, the full version may be specified for the channel to build using that specific +# version (e.g. channel: 1.5.0) +# +# The values for target are the set of windows Rust build targets. Each value is of the form +# +# ARCH-pc-windows-TOOLCHAIN +# +# Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker +# toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for +# a description of the toolchain differences. +# See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of +# toolchains and host triples. +# +# Comment out channel/target combos you do not wish to build in CI. +# +# You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands +# and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly +# channels to enable unstable features when building for nightly. Or you could add additional +# matrix entries to test different combinations of features. +environment: + matrix: + +### MSVC Toolchains ### + + # Stable 64-bit MSVC + - channel: stable + target: x86_64-pc-windows-msvc + # Stable 32-bit MSVC + - channel: stable + target: i686-pc-windows-msvc + # Beta 64-bit MSVC + - channel: beta + target: x86_64-pc-windows-msvc + # Beta 32-bit MSVC + - channel: beta + target: i686-pc-windows-msvc + # Nightly 64-bit MSVC + - channel: nightly + target: x86_64-pc-windows-msvc + #cargoflags: --features "unstable" + # Nightly 32-bit MSVC + - channel: nightly + target: i686-pc-windows-msvc + #cargoflags: --features "unstable" + +### GNU Toolchains ### + + # Stable 64-bit GNU + - channel: stable + target: x86_64-pc-windows-gnu + # Stable 32-bit GNU + - channel: stable + target: i686-pc-windows-gnu + # Beta 64-bit GNU + - channel: beta + target: x86_64-pc-windows-gnu + # Beta 32-bit GNU + - channel: beta + target: i686-pc-windows-gnu + # Nightly 64-bit GNU + - channel: nightly + target: x86_64-pc-windows-gnu + #cargoflags: --features "unstable" + # Nightly 32-bit GNU + - channel: nightly + target: i686-pc-windows-gnu + #cargoflags: --features "unstable" + +### Allowed failures ### + +# See Appveyor documentation for specific details. In short, place any channel or targets you wish +# to allow build failures on (usually nightly at least is a wise choice). This will prevent a build +# or test failure in the matching channels/targets from failing the entire build. +matrix: + allow_failures: + - channel: nightly + +# If you only care about stable channel build failures, uncomment the following line: + #- channel: beta + +## Install Script ## + +# This is the most important part of the Appveyor configuration. This installs the version of Rust +# specified by the 'channel' and 'target' environment variables from the build matrix. This uses +# rustup to install Rust. +# +# For simple configurations, instead of using the build matrix, you can simply set the +# default-toolchain and default-host manually here. +install: + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain %channel% --default-host %target% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin + - rustc -vV + - cargo -vV + +## Build Script ## + +# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents +# the "directory does not contain a project or solution file" error. +build: false + +# Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs +#directly or perform other testing commands. Rust will automatically be placed in the PATH +# environment variable. +test_script: + - cargo test --verbose %cargoflags% diff --git a/rust-rdp/rust-desk/libs/enigo/build.rs b/rust-rdp/rust-desk/libs/enigo/build.rs new file mode 100644 index 0000000..6672b22 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/build.rs @@ -0,0 +1,61 @@ +#[cfg(target_os = "windows")] +fn main() {} + +#[cfg(target_os = "macos")] +fn main() {} + +#[cfg(target_os = "linux")] +use pkg_config; +#[cfg(target_os = "linux")] +use std::env; +#[cfg(target_os = "linux")] +use std::fs::File; +#[cfg(target_os = "linux")] +use std::io::Write; +#[cfg(target_os = "linux")] +use std::path::Path; + +#[cfg(target_os = "linux")] +fn main() { + let libraries = [ + "xext", + "gl", + "xcursor", + "xxf86vm", + "xft", + "xinerama", + "xi", + "x11", + "xlib_xcb", + "xmu", + "xrandr", + "xtst", + "xrender", + "xscrnsaver", + "xt", + ]; + + let mut config = String::new(); + for lib in libraries.iter() { + let libdir = match pkg_config::get_variable(lib, "libdir") { + Ok(libdir) => format!("Some(\"{}\")", libdir), + Err(_) => "None".to_string(), + }; + config.push_str(&format!( + "pub const {}: Option<&'static str> = {};\n", + lib, libdir + )); + } + let config = format!("pub mod config {{ pub mod libdir {{\n{}}}\n}}", config); + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("config.rs"); + let mut f = File::create(&dest_path).unwrap(); + f.write_all(&config.into_bytes()).unwrap(); + + let target = env::var("TARGET").unwrap(); + if target.contains("linux") { + println!("cargo:rustc-link-lib=dl"); + } else if target.contains("freebsd") || target.contains("dragonfly") { + println!("cargo:rustc-link-lib=c"); + } +} diff --git a/rust-rdp/rust-desk/libs/enigo/examples/dsl.rs b/rust-rdp/rust-desk/libs/enigo/examples/dsl.rs new file mode 100644 index 0000000..e58ec44 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/examples/dsl.rs @@ -0,0 +1,11 @@ +use enigo::{Enigo, KeyboardControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + // write text and select all + enigo.key_sequence_parse("{+UNICODE}{{Hello World!}} ❤️{-UNICODE}{+CTRL}a{-CTRL}"); +} diff --git a/rust-rdp/rust-desk/libs/enigo/examples/key.rs b/rust-rdp/rust-desk/libs/enigo/examples/key.rs new file mode 100644 index 0000000..472f377 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/examples/key.rs @@ -0,0 +1,12 @@ +use enigo::{Enigo, Key, KeyboardControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + enigo.key_down(Key::Layout('a')).ok(); + thread::sleep(Duration::from_secs(1)); + enigo.key_up(Key::Layout('a')); +} diff --git a/rust-rdp/rust-desk/libs/enigo/examples/keyboard.rs b/rust-rdp/rust-desk/libs/enigo/examples/keyboard.rs new file mode 100644 index 0000000..c9a12cd --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/examples/keyboard.rs @@ -0,0 +1,16 @@ +use enigo::{Enigo, Key, KeyboardControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + // write text + enigo.key_sequence("Hello World! here is a lot of text ❤️"); + + // select all + enigo.key_down(Key::Control).ok(); + enigo.key_click(Key::Layout('a')); + enigo.key_up(Key::Control); +} diff --git a/rust-rdp/rust-desk/libs/enigo/examples/mouse.rs b/rust-rdp/rust-desk/libs/enigo/examples/mouse.rs new file mode 100644 index 0000000..50a3506 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/examples/mouse.rs @@ -0,0 +1,37 @@ +use enigo::{Enigo, MouseButton, MouseControllable}; +use std::thread; +use std::time::Duration; + +fn main() { + let wait_time = Duration::from_secs(2); + let mut enigo = Enigo::new(); + + thread::sleep(wait_time); + + enigo.mouse_move_to(500, 200); + thread::sleep(wait_time); + + enigo.mouse_down(MouseButton::Left).ok(); + thread::sleep(wait_time); + + enigo.mouse_move_relative(100, 100); + thread::sleep(wait_time); + + enigo.mouse_up(MouseButton::Left); + thread::sleep(wait_time); + + enigo.mouse_click(MouseButton::Left); + thread::sleep(wait_time); + + enigo.mouse_scroll_x(2); + thread::sleep(wait_time); + + enigo.mouse_scroll_x(-2); + thread::sleep(wait_time); + + enigo.mouse_scroll_y(2); + thread::sleep(wait_time); + + enigo.mouse_scroll_y(-2); + thread::sleep(wait_time); +} diff --git a/rust-rdp/rust-desk/libs/enigo/examples/timer.rs b/rust-rdp/rust-desk/libs/enigo/examples/timer.rs new file mode 100644 index 0000000..92ded3d --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/examples/timer.rs @@ -0,0 +1,22 @@ +use enigo::{Enigo, Key, KeyboardControllable}; +use std::thread; +use std::time::Duration; +use std::time::Instant; + +fn main() { + thread::sleep(Duration::from_secs(2)); + let mut enigo = Enigo::new(); + + let now = Instant::now(); + + // write text + enigo.key_sequence("Hello World! ❤️"); + + let time = now.elapsed(); + println!("{:?}", time); + + // select all + enigo.key_down(Key::Control).ok(); + enigo.key_click(Key::Layout('a')); + enigo.key_up(Key::Control); +} diff --git a/rust-rdp/rust-desk/libs/enigo/rustfmt.toml b/rust-rdp/rust-desk/libs/enigo/rustfmt.toml new file mode 100644 index 0000000..b2715b2 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/rustfmt.toml @@ -0,0 +1 @@ +wrap_comments = true diff --git a/rust-rdp/rust-desk/libs/enigo/src/dsl.rs b/rust-rdp/rust-desk/libs/enigo/src/dsl.rs new file mode 100644 index 0000000..dfb8adb --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/dsl.rs @@ -0,0 +1,184 @@ +use crate::{Key, KeyboardControllable}; +use std::error::Error; +use std::fmt; + +/// An error that can occur when parsing DSL +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + /// When a tag doesn't exist. + /// Example: {+TEST}{-TEST} + /// ^^^^ ^^^^ + UnknownTag(String), + + /// When a { is encountered inside a {TAG}. + /// Example: {+HELLO{WORLD} + /// ^ + UnexpectedOpen, + + /// When a { is never matched with a }. + /// Example: {+SHIFT}Hello{-SHIFT + /// ^ + UnmatchedOpen, + + /// Opposite of UnmatchedOpen. + /// Example: +SHIFT}Hello{-SHIFT} + /// ^ + UnmatchedClose, +} +impl Error for ParseError { + fn description(&self) -> &str { + match *self { + ParseError::UnknownTag(_) => "Unknown tag", + ParseError::UnexpectedOpen => "Unescaped open bracket ({) found inside tag name", + ParseError::UnmatchedOpen => "Unmatched open bracket ({). No matching close (})", + ParseError::UnmatchedClose => "Unmatched close bracket (}). No previous open ({)", + } + } +} +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_string()) + } +} + +/// Evaluate the DSL. This tokenizes the input and presses the keys. +pub fn eval(enigo: &mut K, input: &str) -> Result<(), ParseError> +where + K: KeyboardControllable, +{ + for token in tokenize(input)? { + match token { + Token::Sequence(buffer) => { + for key in buffer.chars() { + enigo.key_click(Key::Layout(key)); + } + } + Token::Unicode(buffer) => enigo.key_sequence(&buffer), + Token::KeyUp(key) => enigo.key_up(key), + Token::KeyDown(key) => enigo.key_down(key).unwrap_or(()), + } + } + Ok(()) +} + +#[derive(Debug, PartialEq, Eq)] +enum Token { + Sequence(String), + Unicode(String), + KeyUp(Key), + KeyDown(Key), +} + +fn tokenize(input: &str) -> Result, ParseError> { + let mut unicode = false; + + let mut tokens = Vec::new(); + let mut buffer = String::new(); + let mut iter = input.chars().peekable(); + + fn flush(tokens: &mut Vec, buffer: String, unicode: bool) { + if !buffer.is_empty() { + if unicode { + tokens.push(Token::Unicode(buffer)); + } else { + tokens.push(Token::Sequence(buffer)); + } + } + } + + while let Some(c) = iter.next() { + if c == '{' { + match iter.next() { + Some('{') => buffer.push('{'), + Some(mut c) => { + flush(&mut tokens, buffer, unicode); + buffer = String::new(); + + let mut tag = String::new(); + loop { + tag.push(c); + match iter.next() { + Some('{') => match iter.peek() { + Some(&'{') => { + iter.next(); + c = '{' + } + _ => return Err(ParseError::UnexpectedOpen), + }, + Some('}') => match iter.peek() { + Some(&'}') => { + iter.next(); + c = '}' + } + _ => break, + }, + Some(new) => c = new, + None => return Err(ParseError::UnmatchedOpen), + } + } + match &*tag { + "+UNICODE" => unicode = true, + "-UNICODE" => unicode = false, + "+SHIFT" => tokens.push(Token::KeyDown(Key::Shift)), + "-SHIFT" => tokens.push(Token::KeyUp(Key::Shift)), + "+CTRL" => tokens.push(Token::KeyDown(Key::Control)), + "-CTRL" => tokens.push(Token::KeyUp(Key::Control)), + "+META" => tokens.push(Token::KeyDown(Key::Meta)), + "-META" => tokens.push(Token::KeyUp(Key::Meta)), + "+ALT" => tokens.push(Token::KeyDown(Key::Alt)), + "-ALT" => tokens.push(Token::KeyUp(Key::Alt)), + _ => return Err(ParseError::UnknownTag(tag)), + } + } + None => return Err(ParseError::UnmatchedOpen), + } + } else if c == '}' { + match iter.next() { + Some('}') => buffer.push('}'), + _ => return Err(ParseError::UnmatchedClose), + } + } else { + buffer.push(c); + } + } + + flush(&mut tokens, buffer, unicode); + + Ok(tokens) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!( + tokenize("{{Hello World!}} {+CTRL}hi{-CTRL}"), + Ok(vec![ + Token::Sequence("{Hello World!} ".into()), + Token::KeyDown(Key::Control), + Token::Sequence("hi".into()), + Token::KeyUp(Key::Control) + ]) + ); + } + #[test] + fn unexpected_open() { + assert_eq!(tokenize("{hello{}world}"), Err(ParseError::UnexpectedOpen)); + } + #[test] + fn unmatched_open() { + assert_eq!( + tokenize("{this is going to fail"), + Err(ParseError::UnmatchedOpen) + ); + } + #[test] + fn unmatched_close() { + assert_eq!( + tokenize("{+CTRL}{{this}} is going to fail}"), + Err(ParseError::UnmatchedClose) + ); + } +} diff --git a/rust-rdp/rust-desk/libs/enigo/src/lib.rs b/rust-rdp/rust-desk/libs/enigo/src/lib.rs new file mode 100644 index 0000000..893f591 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/lib.rs @@ -0,0 +1,529 @@ +//! Enigo lets you simulate mouse and keyboard input-events as if they were +//! made by the actual hardware. The goal is to make it available on different +//! operating systems like Linux, macOS and Windows – possibly many more but +//! [Redox](https://redox-os.org/) and *BSD are planned. Please see the +//! [Repo](https://github.com/enigo-rs/enigo) for the current status. +//! +//! I consider this library in an early alpha status, the API will change in +//! in the future. The keyboard handling is far from being very usable. I plan +//! to build a simple +//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) +//! that will resemble something like: +//! +//! `"hello {+SHIFT}world{-SHIFT} and break line{ENTER}"` +//! +//! The current status is that you can just print +//! [unicode](http://unicode.org/) +//! characters like [emoji](http://getemoji.com/) without the `{+SHIFT}` +//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) +//! or any other "special" key on the Linux, macOS and Windows operating system. +//! +//! Possible use cases could be for testing user interfaces on different +//! plattforms, +//! building remote control applications or just automating tasks for user +//! interfaces unaccessible by a public API or scripting laguage. +//! +//! For the keyboard there are currently two modes you can use. The first mode +//! is represented by the [key_sequence]() function +//! its purpose is to simply write unicode characters. This is independent of +//! the keyboardlayout. Please note that +//! you're not be able to use modifier keys like Control +//! to influence the outcome. If you want to use modifier keys to e.g. +//! copy/paste +//! use the Layout variant. Please note that this is indeed layout dependent. + +//! # Examples +//! ```no_run +//! use enigo::*; +//! let mut enigo = Enigo::new(); +//! //paste +//! enigo.key_down(Key::Control); +//! enigo.key_click(Key::Layout('v')); +//! enigo.key_up(Key::Control); +//! ``` +//! +//! ```no_run +//! use enigo::*; +//! let mut enigo = Enigo::new(); +//! enigo.mouse_move_to(500, 200); +//! enigo.mouse_down(MouseButton::Left); +//! enigo.mouse_move_relative(100, 100); +//! enigo.mouse_up(MouseButton::Left); +//! enigo.key_sequence("hello world"); +//! ``` +#![deny(missing_docs)] + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; + +// TODO(dustin) use interior mutability not &mut self + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +pub use win::Enigo; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::Enigo; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use crate::linux::Enigo; + +/// DSL parser module +pub mod dsl; + +#[cfg(feature = "with_serde")] +#[macro_use] +extern crate serde_derive; + +#[cfg(feature = "with_serde")] +extern crate serde; + +/// +pub type ResultType = std::result::Result<(), Box>; + +#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq)] +/// MouseButton represents a mouse button, +/// and is used in for example +/// [mouse_click](trait.MouseControllable.html#tymethod.mouse_click). +/// WARNING: Types with the prefix Scroll +/// IS NOT intended to be used, and may not work on +/// all operating systems. +pub enum MouseButton { + /// Left mouse button + Left, + /// Middle mouse button + Middle, + /// Right mouse button + Right, + + /// Scroll up button + ScrollUp, + /// Left right button + ScrollDown, + /// Left right button + ScrollLeft, + /// Left right button + ScrollRight, +} + +/// Representing an interface and a set of mouse functions every +/// operating system implementation _should_ implement. +pub trait MouseControllable { + /// Lets the mouse cursor move to the specified x and y coordinates. + /// + /// The topleft corner of your monitor screen is x=0 y=0. Move + /// the cursor down the screen by increasing the y and to the right + /// by increasing x coordinate. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_move_to(500, 200); + /// ``` + fn mouse_move_to(&mut self, x: i32, y: i32); + + /// Lets the mouse cursor move the specified amount in the x and y + /// direction. + /// + /// The amount specified in the x and y parameters are added to the + /// current location of the mouse cursor. A positive x values lets + /// the mouse cursor move an amount of `x` pixels to the right. A negative + /// value for `x` lets the mouse cursor go to the left. A positive value + /// of y + /// lets the mouse cursor go down, a negative one lets the mouse cursor go + /// up. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_move_relative(100, 100); + /// ``` + fn mouse_move_relative(&mut self, x: i32, y: i32); + + /// Push down one of the mouse buttons + /// + /// Push down the mouse button specified by the parameter `button` of + /// type [MouseButton](enum.MouseButton.html) + /// and holds it until it is released by + /// [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). + /// Calls to [mouse_move_to](trait.MouseControllable.html#tymethod. + /// mouse_move_to) or + /// [mouse_move_relative](trait.MouseControllable.html#tymethod. + /// mouse_move_relative) + /// will work like expected and will e.g. drag widgets or highlight text. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_down(MouseButton::Left); + /// ``` + fn mouse_down(&mut self, button: MouseButton) -> ResultType; + + /// Lift up a pushed down mouse button + /// + /// Lift up a previously pushed down button (by invoking + /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down)). + /// If the button was not pushed down or consecutive calls without + /// invoking [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) + /// will emit lift up events. It depends on the + /// operating system whats actually happening – my guess is it will just + /// get ignored. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_up(MouseButton::Right); + /// ``` + fn mouse_up(&mut self, button: MouseButton); + + /// Click a mouse button + /// + /// it's esentially just a consecutive invokation of + /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed + /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just + /// for + /// convenience. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_click(MouseButton::Right); + /// ``` + fn mouse_click(&mut self, button: MouseButton); + + /// Scroll the mouse (wheel) left or right + /// + /// Positive numbers for length lets the mouse wheel scroll to the right + /// and negative ones to the left. The value that is specified translates + /// to `lines` defined by the operating system and is essentially one 15° + /// (click)rotation on the mouse wheel. How many lines it moves depends + /// on the current setting in the operating system. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_scroll_x(2); + /// ``` + fn mouse_scroll_x(&mut self, length: i32); + + /// Scroll the mouse (wheel) up or down + /// + /// Positive numbers for length lets the mouse wheel scroll down + /// and negative ones up. The value that is specified translates + /// to `lines` defined by the operating system and is essentially one 15° + /// (click)rotation on the mouse wheel. How many lines it moves depends + /// on the current setting in the operating system. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_scroll_y(2); + /// ``` + fn mouse_scroll_y(&mut self, length: i32); +} + +/// A key on the keyboard. +/// For alphabetical keys, use Key::Layout for a system independent key. +/// If a key is missing, you can use the raw keycode with Key::Raw. +#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Key { + /// alt key on Linux and Windows (option key on macOS) + Alt, + /// backspace key + Backspace, + /// caps lock key + CapsLock, + #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// command key on macOS (super key on Linux, windows key on Windows) + Command, + /// control key + Control, + /// delete key + Delete, + /// down arrow key + DownArrow, + /// end key + End, + /// escape key (esc) + Escape, + /// F1 key + F1, + /// F10 key + F10, + /// F11 key + F11, + /// F12 key + F12, + /// F2 key + F2, + /// F3 key + F3, + /// F4 key + F4, + /// F5 key + F5, + /// F6 key + F6, + /// F7 key + F7, + /// F8 key + F8, + /// F9 key + F9, + /// home key + Home, + /// left arrow key + LeftArrow, + /// meta key (also known as "windows", "super", and "command") + Meta, + /// option key on macOS (alt key on Linux and Windows) + Option, // deprecated, use Alt instead + /// page down key + PageDown, + /// page up key + PageUp, + /// return key + Return, + /// right arrow key + RightArrow, + /// shift key + Shift, + /// space key + Space, + #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// super key on linux (command key on macOS, windows key on Windows) + Super, + /// tab key (tabulator) + Tab, + /// up arrow key + UpArrow, + #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// windows key on Windows (super key on Linux, command key on macOS) + Windows, + /// + Numpad0, + /// + Numpad1, + /// + Numpad2, + /// + Numpad3, + /// + Numpad4, + /// + Numpad5, + /// + Numpad6, + /// + Numpad7, + /// + Numpad8, + /// + Numpad9, + /// + Cancel, + /// + Clear, + /// + Pause, + /// + Kana, + /// + Hangul, + /// + Junja, + /// + Final, + /// + Hanja, + /// + Kanji, + /// + Convert, + /// + Select, + /// + Print, + /// + Execute, + /// + Snapshot, + /// + Insert, + /// + Help, + /// + Sleep, + /// + Separator, + /// + VolumeUp, + /// + VolumeDown, + /// + Mute, + /// + Scroll, + /// scroll lock + NumLock, + /// + RWin, + /// + Apps, + /// + Multiply, + /// + Add, + /// + Subtract, + /// + Decimal, + /// + Divide, + /// + Equals, + /// + NumpadEnter, + /// + RightShift, + /// + RightControl, + /// + RightAlt, + /// + /// Function, /// mac + /// keyboard layout dependent key + Layout(char), + /// raw keycode eg 0x38 + Raw(u16), +} + +/// Representing an interface and a set of keyboard functions every +/// operating system implementation _should_ implement. +pub trait KeyboardControllable { + /// Types the string parsed with DSL. + /// + /// Typing {+SHIFT}hello{-SHIFT} becomes HELLO. + /// TODO: Full documentation + fn key_sequence_parse(&mut self, sequence: &str) + where + Self: Sized, + { + self.key_sequence_parse_try(sequence) + .expect("Could not parse sequence"); + } + /// Same as key_sequence_parse except returns any errors + fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError> + where + Self: Sized, + { + dsl::eval(self, sequence) + } + + /// Types the string + /// + /// Emits keystrokes such that the given string is inputted. + /// + /// You can use many unicode here like: ❤️. This works + /// regadless of the current keyboardlayout. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.key_sequence("hello world ❤️"); + /// ``` + fn key_sequence(&mut self, sequence: &str); + + /// presses a given key down + fn key_down(&mut self, key: Key) -> ResultType; + + /// release a given key formally pressed down by + /// [key_down](trait.KeyboardControllable.html#tymethod.key_down) + fn key_up(&mut self, key: Key); + + /// Much like the + /// [key_down](trait.KeyboardControllable.html#tymethod.key_down) and + /// [key_up](trait.KeyboardControllable.html#tymethod.key_up) + /// function they're just invoked consecutively + fn key_click(&mut self, key: Key); + + /// + fn get_key_state(&mut self, key: Key) -> bool; +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +struct Enigo; + +impl Enigo { + /// Constructs a new `Enigo` instance. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// ``` + pub fn new() -> Self { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Enigo {}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Self::default() + } +} + +use std::fmt; + +impl fmt::Debug for Enigo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Enigo") + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_key_state() { + let mut enigo = Enigo::new(); + let keys = [Key::CapsLock, Key::NumLock]; + for k in keys.iter() { + enigo.key_click(k.clone()); + let a = enigo.get_key_state(k.clone()); + enigo.key_click(k.clone()); + let b = enigo.get_key_state(k.clone()); + assert!(a != b); + } + let keys = [Key::Control, Key::Alt, Key::Shift]; + for k in keys.iter() { + enigo.key_down(k.clone()).ok(); + let a = enigo.get_key_state(k.clone()); + enigo.key_up(k.clone()); + let b = enigo.get_key_state(k.clone()); + assert!(a != b); + } + } +} diff --git a/rust-rdp/rust-desk/libs/enigo/src/linux.rs b/rust-rdp/rust-desk/libs/enigo/src/linux.rs new file mode 100644 index 0000000..24628c1 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/linux.rs @@ -0,0 +1,363 @@ +use libc; + +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; + +use self::libc::{c_char, c_int, c_void, useconds_t}; +use std::{borrow::Cow, ffi::CString, ptr}; + +const CURRENT_WINDOW: c_int = 0; +const DEFAULT_DELAY: u64 = 12000; +type Window = c_int; +type Xdo = *const c_void; + +#[link(name = "xdo")] +extern "C" { + fn xdo_free(xdo: Xdo); + fn xdo_new(display: *const c_char) -> Xdo; + + fn xdo_click_window(xdo: Xdo, window: Window, button: c_int) -> c_int; + fn xdo_mouse_down(xdo: Xdo, window: Window, button: c_int) -> c_int; + fn xdo_mouse_up(xdo: Xdo, window: Window, button: c_int) -> c_int; + fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int; + fn xdo_move_mouse_relative(xdo: Xdo, x: c_int, y: c_int) -> c_int; + + fn xdo_enter_text_window( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_send_keysequence_window( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_send_keysequence_window_down( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_send_keysequence_window_up( + xdo: Xdo, + window: Window, + string: *const c_char, + delay: useconds_t, + ) -> c_int; + fn xdo_get_input_state(xdo: Xdo) -> u32; +} + +fn mousebutton(button: MouseButton) -> c_int { + match button { + MouseButton::Left => 1, + MouseButton::Middle => 2, + MouseButton::Right => 3, + MouseButton::ScrollUp => 4, + MouseButton::ScrollDown => 5, + MouseButton::ScrollLeft => 6, + MouseButton::ScrollRight => 7, + } +} + +/// The main struct for handling the event emitting +pub struct Enigo { + xdo: Xdo, + delay: u64, +} +// This is safe, we have a unique pointer. +// TODO: use Unique once stable. +unsafe impl Send for Enigo {} + +impl Default for Enigo { + /// Create a new Enigo instance + fn default() -> Self { + Self { + xdo: unsafe { xdo_new(ptr::null()) }, + delay: DEFAULT_DELAY, + } + } +} +impl Enigo { + /// Get the delay per keypress. + /// Default value is 12000. + /// This is Linux-specific. + pub fn delay(&self) -> u64 { + self.delay + } + /// Set the delay per keypress. + /// This is Linux-specific. + pub fn set_delay(&mut self, delay: u64) { + self.delay = delay; + } +} +impl Drop for Enigo { + fn drop(&mut self) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_free(self.xdo); + } + } +} +impl MouseControllable for Enigo { + fn mouse_move_to(&mut self, x: i32, y: i32) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_move_mouse(self.xdo, x as c_int, y as c_int, 0); + } + } + fn mouse_move_relative(&mut self, x: i32, y: i32) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_move_mouse_relative(self.xdo, x as c_int, y as c_int); + } + } + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + if self.xdo.is_null() { + return Ok(()); + } + unsafe { + xdo_mouse_down(self.xdo, CURRENT_WINDOW, mousebutton(button)); + } + Ok(()) + } + fn mouse_up(&mut self, button: MouseButton) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_mouse_up(self.xdo, CURRENT_WINDOW, mousebutton(button)); + } + } + fn mouse_click(&mut self, button: MouseButton) { + if self.xdo.is_null() { + return; + } + unsafe { + xdo_click_window(self.xdo, CURRENT_WINDOW, mousebutton(button)); + } + } + fn mouse_scroll_x(&mut self, length: i32) { + let button; + let mut length = length; + + if length < 0 { + button = MouseButton::ScrollLeft; + } else { + button = MouseButton::ScrollRight; + } + + if length < 0 { + length = -length; + } + + for _ in 0..length { + self.mouse_click(button); + } + } + fn mouse_scroll_y(&mut self, length: i32) { + let button; + let mut length = length; + + if length < 0 { + button = MouseButton::ScrollUp; + } else { + button = MouseButton::ScrollDown; + } + + if length < 0 { + length = -length; + } + + for _ in 0..length { + self.mouse_click(button); + } + } +} +fn keysequence<'a>(key: Key) -> Cow<'a, str> { + if let Key::Layout(c) = key { + return Cow::Owned(format!("U{:X}", c as u32)); + } + if let Key::Raw(k) = key { + return Cow::Owned(format!("{}", k as u16)); + } + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + // https://www.rubydoc.info/gems/xdo/XDo/Keyboard + // https://gitlab.com/cunidev/gestures/-/wikis/xdotool-list-of-key-codes + Cow::Borrowed(match key { + Key::Alt => "Alt", + Key::Backspace => "BackSpace", + Key::CapsLock => "Caps_Lock", + Key::Control => "Control", + Key::Delete => "Delete", + Key::DownArrow => "Down", + Key::End => "End", + Key::Escape => "Escape", + Key::F1 => "F1", + Key::F10 => "F10", + Key::F11 => "F11", + Key::F12 => "F12", + Key::F2 => "F2", + Key::F3 => "F3", + Key::F4 => "F4", + Key::F5 => "F5", + Key::F6 => "F6", + Key::F7 => "F7", + Key::F8 => "F8", + Key::F9 => "F9", + Key::Home => "Home", + //Key::Layout(_) => unreachable!(), + Key::LeftArrow => "Left", + Key::Option => "Option", + Key::PageDown => "Page_Down", + Key::PageUp => "Page_Up", + //Key::Raw(_) => unreachable!(), + Key::Return => "Return", + Key::RightArrow => "Right", + Key::Shift => "Shift", + Key::Space => "space", + Key::Tab => "Tab", + Key::UpArrow => "Up", + Key::Numpad0 => "U30", //"KP_0", + Key::Numpad1 => "U31", //"KP_1", + Key::Numpad2 => "U32", //"KP_2", + Key::Numpad3 => "U33", //"KP_3", + Key::Numpad4 => "U34", //"KP_4", + Key::Numpad5 => "U35", //"KP_5", + Key::Numpad6 => "U36", //"KP_6", + Key::Numpad7 => "U37", //"KP_7", + Key::Numpad8 => "U38", //"KP_8", + Key::Numpad9 => "U39", //"KP_9", + Key::Decimal => "U2E", //"KP_Decimal", + Key::Cancel => "Cancel", + Key::Clear => "Clear", + Key::Pause => "Pause", + Key::Kana => "Kana", + Key::Hangul => "Hangul", + Key::Junja => "", + Key::Final => "", + Key::Hanja => "Hanja", + Key::Kanji => "Kanji", + Key::Convert => "", + Key::Select => "Select", + Key::Print => "Print", + Key::Execute => "Execute", + Key::Snapshot => "3270_PrintScreen", + Key::Insert => "Insert", + Key::Help => "Help", + Key::Sleep => "", + Key::Separator => "KP_Separator", + Key::VolumeUp => "", + Key::VolumeDown => "", + Key::Mute => "", + Key::Scroll => "Scroll_Lock", + Key::NumLock => "Num_Lock", + Key::RWin => "Super_R", + Key::Apps => "Menu", + Key::Multiply => "KP_Multiply", + Key::Add => "KP_Add", + Key::Subtract => "KP_Subtract", + Key::Divide => "KP_Divide", + Key::Equals => "KP_Equal", + Key::NumpadEnter => "KP_Enter", + Key::RightShift => "Shift_R", + Key::RightControl => "Control_R", + Key::RightAlt => "Alt_R", + + Key::Command | Key::Super | Key::Windows | Key::Meta => "Super", + + _ => "", + }) +} +impl KeyboardControllable for Enigo { + fn get_key_state(&mut self, key: Key) -> bool { + if self.xdo.is_null() { + return false; + } + let mod_shift = 1 << 0; + let mod_lock = 1 << 1; + let mod_control = 1 << 2; + let mod_alt = 1 << 3; + let mod_numlock = 1 << 4; + let mod_meta = 1 << 6; + let mask = unsafe { xdo_get_input_state(self.xdo) }; + // println!("{:b}", mask); + match key { + Key::Shift => mask & mod_shift != 0, + Key::CapsLock => mask & mod_lock != 0, + Key::Control => mask & mod_control != 0, + Key::Alt => mask & mod_alt != 0, + Key::NumLock => mask & mod_numlock != 0, + Key::Meta => mask & mod_meta != 0, + _ => false, + } + } + + fn key_sequence(&mut self, sequence: &str) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(sequence) { + unsafe { + xdo_enter_text_window( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + } + } + fn key_down(&mut self, key: Key) -> crate::ResultType { + if self.xdo.is_null() { + return Ok(()); + } + let string = CString::new(&*keysequence(key))?; + unsafe { + xdo_send_keysequence_window_down( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + Ok(()) + } + fn key_up(&mut self, key: Key) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(&*keysequence(key)) { + unsafe { + xdo_send_keysequence_window_up( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + } + } + fn key_click(&mut self, key: Key) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(&*keysequence(key)) { + unsafe { + xdo_send_keysequence_window( + self.xdo, + CURRENT_WINDOW, + string.as_ptr(), + self.delay as useconds_t, + ); + } + } + } +} diff --git a/rust-rdp/rust-desk/libs/enigo/src/macos/keycodes.rs b/rust-rdp/rust-desk/libs/enigo/src/macos/keycodes.rs new file mode 100644 index 0000000..540d0e1 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/macos/keycodes.rs @@ -0,0 +1,120 @@ +// https://stackoverflow.com/questions/3202629/where-can-i-find-a-list-of-mac-virtual-key-codes + +/* keycodes for keys that are independent of keyboard layout */ + +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +pub const kVK_Return: u16 = 0x24; +pub const kVK_Tab: u16 = 0x30; +pub const kVK_Space: u16 = 0x31; +pub const kVK_Delete: u16 = 0x33; +pub const kVK_Escape: u16 = 0x35; +pub const kVK_Command: u16 = 0x37; +pub const kVK_Shift: u16 = 0x38; +pub const kVK_CapsLock: u16 = 0x39; +pub const kVK_Option: u16 = 0x3A; +pub const kVK_Control: u16 = 0x3B; +pub const kVK_RightShift: u16 = 0x3C; +pub const kVK_RightOption: u16 = 0x3D; +pub const kVK_RightControl: u16 = 0x3E; +pub const kVK_Function: u16 = 0x3F; +pub const kVK_F17: u16 = 0x40; +pub const kVK_VolumeUp: u16 = 0x48; +pub const kVK_VolumeDown: u16 = 0x49; +pub const kVK_Mute: u16 = 0x4A; +pub const kVK_F18: u16 = 0x4F; +pub const kVK_F19: u16 = 0x50; +pub const kVK_F20: u16 = 0x5A; +pub const kVK_F5: u16 = 0x60; +pub const kVK_F6: u16 = 0x61; +pub const kVK_F7: u16 = 0x62; +pub const kVK_F3: u16 = 0x63; +pub const kVK_F8: u16 = 0x64; +pub const kVK_F9: u16 = 0x65; +pub const kVK_F11: u16 = 0x67; +pub const kVK_F13: u16 = 0x69; +pub const kVK_F16: u16 = 0x6A; +pub const kVK_F14: u16 = 0x6B; +pub const kVK_F10: u16 = 0x6D; +pub const kVK_F12: u16 = 0x6F; +pub const kVK_F15: u16 = 0x71; +pub const kVK_Help: u16 = 0x72; +pub const kVK_Home: u16 = 0x73; +pub const kVK_PageUp: u16 = 0x74; +pub const kVK_ForwardDelete: u16 = 0x75; +pub const kVK_F4: u16 = 0x76; +pub const kVK_End: u16 = 0x77; +pub const kVK_F2: u16 = 0x78; +pub const kVK_PageDown: u16 = 0x79; +pub const kVK_F1: u16 = 0x7A; +pub const kVK_LeftArrow: u16 = 0x7B; +pub const kVK_RightArrow: u16 = 0x7C; +pub const kVK_DownArrow: u16 = 0x7D; +pub const kVK_UpArrow: u16 = 0x7E; +pub const kVK_ANSI_Keypad0: u16 = 0x52; +pub const kVK_ANSI_Keypad1: u16 = 0x53; +pub const kVK_ANSI_Keypad2: u16 = 0x54; +pub const kVK_ANSI_Keypad3: u16 = 0x55; +pub const kVK_ANSI_Keypad4: u16 = 0x56; +pub const kVK_ANSI_Keypad5: u16 = 0x57; +pub const kVK_ANSI_Keypad6: u16 = 0x58; +pub const kVK_ANSI_Keypad7: u16 = 0x59; +pub const kVK_ANSI_Keypad8: u16 = 0x5B; +pub const kVK_ANSI_Keypad9: u16 = 0x5C; +pub const kVK_ANSI_KeypadClear: u16 = 0x47; +pub const kVK_ANSI_KeypadDecimal: u16 = 0x41; +pub const kVK_ANSI_KeypadMultiply: u16 = 0x43; +pub const kVK_ANSI_KeypadPlus: u16 = 0x45; +pub const kVK_ANSI_KeypadDivide: u16 = 0x4B; +pub const kVK_ANSI_KeypadEnter: u16 = 0x4C; +pub const kVK_ANSI_KeypadMinus: u16 = 0x4E; +pub const kVK_ANSI_KeypadEquals: u16 = 0x51; +pub const kVK_RIGHT_COMMAND: u16 = 0x36; +pub const kVK_ANSI_A : u16 = 0x00; +pub const kVK_ANSI_S : u16 = 0x01; +pub const kVK_ANSI_D : u16 = 0x02; +pub const kVK_ANSI_F : u16 = 0x03; +pub const kVK_ANSI_H : u16 = 0x04; +pub const kVK_ANSI_G : u16 = 0x05; +pub const kVK_ANSI_Z : u16 = 0x06; +pub const kVK_ANSI_X : u16 = 0x07; +pub const kVK_ANSI_C : u16 = 0x08; +pub const kVK_ANSI_V : u16 = 0x09; +pub const kVK_ANSI_B : u16 = 0x0B; +pub const kVK_ANSI_Q : u16 = 0x0C; +pub const kVK_ANSI_W : u16 = 0x0D; +pub const kVK_ANSI_E : u16 = 0x0E; +pub const kVK_ANSI_R : u16 = 0x0F; +pub const kVK_ANSI_Y : u16 = 0x10; +pub const kVK_ANSI_T : u16 = 0x11; +pub const kVK_ANSI_1 : u16 = 0x12; +pub const kVK_ANSI_2 : u16 = 0x13; +pub const kVK_ANSI_3 : u16 = 0x14; +pub const kVK_ANSI_4 : u16 = 0x15; +pub const kVK_ANSI_6 : u16 = 0x16; +pub const kVK_ANSI_5 : u16 = 0x17; +pub const kVK_ANSI_Equal : u16 = 0x18; +pub const kVK_ANSI_9 : u16 = 0x19; +pub const kVK_ANSI_7 : u16 = 0x1A; +pub const kVK_ANSI_Minus : u16 = 0x1B; +pub const kVK_ANSI_8 : u16 = 0x1C; +pub const kVK_ANSI_0 : u16 = 0x1D; +pub const kVK_ANSI_RightBracket : u16 = 0x1E; +pub const kVK_ANSI_O : u16 = 0x1F; +pub const kVK_ANSI_U : u16 = 0x20; +pub const kVK_ANSI_LeftBracket : u16 = 0x21; +pub const kVK_ANSI_I : u16 = 0x22; +pub const kVK_ANSI_P : u16 = 0x23; +pub const kVK_ANSI_L : u16 = 0x25; +pub const kVK_ANSI_J : u16 = 0x26; +pub const kVK_ANSI_Quote : u16 = 0x27; +pub const kVK_ANSI_K : u16 = 0x28; +pub const kVK_ANSI_Semicolon : u16 = 0x29; +pub const kVK_ANSI_Backslash : u16 = 0x2A; +pub const kVK_ANSI_Comma : u16 = 0x2B; +pub const kVK_ANSI_Slash : u16 = 0x2C; +pub const kVK_ANSI_N : u16 = 0x2D; +pub const kVK_ANSI_M : u16 = 0x2E; +pub const kVK_ANSI_Period : u16 = 0x2F; +pub const kVK_ANSI_Grave : u16 = 0x32; diff --git a/rust-rdp/rust-desk/libs/enigo/src/macos/macos_impl.rs b/rust-rdp/rust-desk/libs/enigo/src/macos/macos_impl.rs new file mode 100644 index 0000000..328285f --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/macos/macos_impl.rs @@ -0,0 +1,667 @@ +use core_graphics; + +// TODO(dustin): use only the things i need + +use self::core_graphics::display::*; +use self::core_graphics::event::*; +use self::core_graphics::event_source::*; +use std::collections::HashMap as Map; +use std::ffi::c_void; +use std::ffi::CStr; +use std::os::raw::*; +use std::ptr::null_mut; + +use crate::macos::keycodes::*; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use objc::runtime::Class; + +struct MyCGEvent; +type TISInputSourceRef = *mut c_void; +type CFDataRef = *const c_void; +type OptionBits = u32; +type OSStatus = i32; +type UniChar = u16; +type UniCharCount = usize; +type Boolean = c_uchar; +type CFStringEncoding = u32; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct __CFString([u8; 0]); +type CFStringRef = *const __CFString; + +#[allow(non_upper_case_globals)] +const kCFStringEncodingUTF8: u32 = 134_217_984; +#[allow(non_upper_case_globals)] +const kUCKeyActionDisplay: u16 = 3; +#[allow(non_upper_case_globals)] +const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; +const BUF_LEN: usize = 4; + +#[allow(improper_ctypes)] +#[allow(non_snake_case)] +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8; + fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; + fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + fn TISCopyCurrentASCIICapableKeyboardLayoutInputSource() -> TISInputSourceRef; + static kTISPropertyUnicodeKeyLayoutData: *mut c_void; + static kTISPropertyInputSourceID: *mut c_void; + fn UCKeyTranslate( + keyLayoutPtr: *const u8, //*const UCKeyboardLayout, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut u32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut [UniChar; BUF_LEN], + ) -> OSStatus; + fn LMGetKbdType() -> u8; + fn CFStringGetCString( + theString: CFStringRef, + buffer: *mut c_char, + bufferSize: CFIndex, + encoding: CFStringEncoding, + ) -> Boolean; + + fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent); + // Actually return CFDataRef which is const here, but for coding convienence, return *mut c_void + fn TISGetInputSourceProperty(source: TISInputSourceRef, property: *const c_void) + -> *mut c_void; + // not present in servo/core-graphics + fn CGEventCreateScrollWheelEvent( + source: &CGEventSourceRef, + units: ScrollUnit, + wheelCount: u32, + wheel1: i32, + ... + ) -> *mut MyCGEvent; + fn CGEventSourceKeyState(stateID: i32, key: u16) -> bool; +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct NSPoint { + x: f64, + y: f64, +} + +// not present in servo/core-graphics +#[allow(dead_code)] +#[derive(Debug)] +enum ScrollUnit { + Pixel = 0, + Line = 1, +} +// hack + +/// The main struct for handling the event emitting +pub struct Enigo { + event_source: Option, + double_click_interval: u32, + last_click_time: Option, + multiple_click: i64, + flags: CGEventFlags, + char_to_vkey_map: Map>, +} + +impl Enigo { + /// + pub fn reset_flag(&mut self) { + self.flags = CGEventFlags::CGEventFlagNull; + } + + /// + pub fn add_flag(&mut self, key: &Key) { + let flag = match key { + &Key::CapsLock => CGEventFlags::CGEventFlagAlphaShift, + &Key::Shift => CGEventFlags::CGEventFlagShift, + &Key::Control => CGEventFlags::CGEventFlagControl, + &Key::Alt => CGEventFlags::CGEventFlagAlternate, + &Key::Meta => CGEventFlags::CGEventFlagCommand, + &Key::NumLock => CGEventFlags::CGEventFlagNumericPad, + _ => CGEventFlags::CGEventFlagNull, + }; + self.flags |= flag; + } + + fn post(&self, event: CGEvent) { + event.set_flags(self.flags); + event.post(CGEventTapLocation::HID); + } +} + +impl Default for Enigo { + fn default() -> Self { + let mut double_click_interval = 500; + if let Some(ns_event) = Class::get("NSEvent") { + let tm: f64 = unsafe { msg_send![ns_event, doubleClickInterval] }; + if tm > 0. { + double_click_interval = (tm * 1000.) as u32; + log::info!("double click interval: {}ms", double_click_interval); + } + } + Self { + // TODO(dustin): return error rather than panic here + event_source: if let Ok(src) = + CGEventSource::new(CGEventSourceStateID::CombinedSessionState) + { + Some(src) + } else { + None + }, + double_click_interval, + multiple_click: 1, + last_click_time: None, + flags: CGEventFlags::CGEventFlagNull, + char_to_vkey_map: Default::default(), + } + } +} + +impl MouseControllable for Enigo { + fn mouse_move_to(&mut self, x: i32, y: i32) { + let pressed = Self::pressed_buttons(); + + let event_type = if pressed & 1 > 0 { + CGEventType::LeftMouseDragged + } else if pressed & 2 > 0 { + CGEventType::RightMouseDragged + } else { + CGEventType::MouseMoved + }; + + let dest = CGPoint::new(x as f64, y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left) + { + self.post(event); + } + } + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + let (display_width, display_height) = Self::main_display_size(); + let (current_x, y_inv) = Self::mouse_location_raw_coords(); + let current_y = (display_height as i32) - y_inv; + let new_x = current_x + x; + let new_y = current_y + y; + + if new_x < 0 + || new_x as usize > display_width + || new_y < 0 + || new_y as usize > display_height + { + return; + } + + self.mouse_move_to(new_x, new_y); + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + let now = std::time::Instant::now(); + if let Some(t) = self.last_click_time { + if t.elapsed().as_millis() as u32 <= self.double_click_interval { + self.multiple_click += 1; + } else { + self.multiple_click = 1; + } + } + self.last_click_time = Some(now); + let (current_x, current_y) = Self::mouse_location(); + let (button, event_type) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), + _ => unimplemented!(), + }; + let dest = CGPoint::new(current_x as f64, current_y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) { + if self.multiple_click > 1 { + event.set_integer_value_field( + EventField::MOUSE_EVENT_CLICK_STATE, + self.multiple_click, + ); + } + self.post(event); + } + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + let (current_x, current_y) = Self::mouse_location(); + let (button, event_type) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), + _ => unimplemented!(), + }; + let dest = CGPoint::new(current_x as f64, current_y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) { + if self.multiple_click > 1 { + event.set_integer_value_field( + EventField::MOUSE_EVENT_CLICK_STATE, + self.multiple_click, + ); + } + self.post(event); + } + } + } + + fn mouse_click(&mut self, button: MouseButton) { + self.mouse_down(button).ok(); + self.mouse_up(button); + } + + fn mouse_scroll_x(&mut self, length: i32) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let mouse_ev = CGEventCreateScrollWheelEvent( + &src, + ScrollUnit::Line, + 2, // CGWheelCount 1 = y 2 = xy 3 = xyz + 0, + scroll_direction, + ); + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } + + fn mouse_scroll_y(&mut self, length: i32) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let mouse_ev = CGEventCreateScrollWheelEvent( + &src, + ScrollUnit::Line, + 1, // CGWheelCount 1 = y 2 = xy 3 = xyz + scroll_direction, + ); + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } +} + +// https://stackoverflow. +// com/questions/1918841/how-to-convert-ascii-character-to-cgkeycode + +impl KeyboardControllable for Enigo { + fn key_sequence(&mut self, sequence: &str) { + // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 + // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time + // but i am unsure what would happen for grapheme clusters greater than 20 bytes ... + use unicode_segmentation::UnicodeSegmentation; + let clusters = UnicodeSegmentation::graphemes(sequence, true).collect::>(); + for cluster in clusters { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), 0, true) { + event.set_string(cluster); + self.post(event); + } + } + } + } + + fn key_click(&mut self, key: Key) { + let keycode = self.key_to_keycode(key); + if keycode == 0 { + return; + } + + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, true) { + self.post(event); + } + + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, false) { + self.post(event); + } + } + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), true) + { + self.post(event); + } + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false) + { + self.post(event); + } + } + } + + fn get_key_state(&mut self, key: Key) -> bool { + let keycode = self.key_to_keycode(key); + unsafe { CGEventSourceKeyState(1, keycode) } + } +} + +impl Enigo { + fn pressed_buttons() -> usize { + if let Some(ns_event) = Class::get("NSEvent") { + unsafe { msg_send![ns_event, pressedMouseButtons] } + } else { + 0 + } + } + + /// Fetches the `(width, height)` in pixels of the main display + pub fn main_display_size() -> (usize, usize) { + let display_id = unsafe { CGMainDisplayID() }; + let width = unsafe { CGDisplayPixelsWide(display_id) }; + let height = unsafe { CGDisplayPixelsHigh(display_id) }; + (width, height) + } + + /// Returns the current mouse location in Cocoa coordinates which have Y + /// inverted from the Carbon coordinates used in the rest of the API. + /// This function exists so that mouse_move_relative only has to fetch + /// the screen size once. + fn mouse_location_raw_coords() -> (i32, i32) { + if let Some(ns_event) = Class::get("NSEvent") { + let pt: NSPoint = unsafe { msg_send![ns_event, mouseLocation] }; + (pt.x as i32, pt.y as i32) + } else { + (0, 0) + } + } + + /// The mouse coordinates in points, only works on the main display + pub fn mouse_location() -> (i32, i32) { + let (x, y_inv) = Self::mouse_location_raw_coords(); + let (_, display_height) = Self::main_display_size(); + (x, (display_height as i32) - y_inv) + } + + fn key_to_keycode(&mut self, key: Key) -> CGKeyCode { + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + match key { + Key::Alt => kVK_Option, + Key::Backspace => kVK_Delete, + Key::CapsLock => kVK_CapsLock, + Key::Control => kVK_Control, + Key::Delete => kVK_ForwardDelete, + Key::DownArrow => kVK_DownArrow, + Key::End => kVK_End, + Key::Escape => kVK_Escape, + Key::F1 => kVK_F1, + Key::F10 => kVK_F10, + Key::F11 => kVK_F11, + Key::F12 => kVK_F12, + Key::F2 => kVK_F2, + Key::F3 => kVK_F3, + Key::F4 => kVK_F4, + Key::F5 => kVK_F5, + Key::F6 => kVK_F6, + Key::F7 => kVK_F7, + Key::F8 => kVK_F8, + Key::F9 => kVK_F9, + Key::Home => kVK_Home, + Key::LeftArrow => kVK_LeftArrow, + Key::Option => kVK_Option, + Key::PageDown => kVK_PageDown, + Key::PageUp => kVK_PageUp, + Key::Return => kVK_Return, + Key::RightArrow => kVK_RightArrow, + Key::Shift => kVK_Shift, + Key::Space => kVK_Space, + Key::Tab => kVK_Tab, + Key::UpArrow => kVK_UpArrow, + Key::Numpad0 => kVK_ANSI_Keypad0, + Key::Numpad1 => kVK_ANSI_Keypad1, + Key::Numpad2 => kVK_ANSI_Keypad2, + Key::Numpad3 => kVK_ANSI_Keypad3, + Key::Numpad4 => kVK_ANSI_Keypad4, + Key::Numpad5 => kVK_ANSI_Keypad5, + Key::Numpad6 => kVK_ANSI_Keypad6, + Key::Numpad7 => kVK_ANSI_Keypad7, + Key::Numpad8 => kVK_ANSI_Keypad8, + Key::Numpad9 => kVK_ANSI_Keypad9, + Key::Mute => kVK_Mute, + Key::VolumeDown => kVK_VolumeUp, + Key::VolumeUp => kVK_VolumeDown, + Key::Help => kVK_Help, + Key::Snapshot => kVK_F13, + Key::Clear => kVK_ANSI_KeypadClear, + Key::Decimal => kVK_ANSI_KeypadDecimal, + Key::Multiply => kVK_ANSI_KeypadMultiply, + Key::Add => kVK_ANSI_KeypadPlus, + Key::Divide => kVK_ANSI_KeypadDivide, + Key::NumpadEnter => kVK_ANSI_KeypadEnter, + Key::Subtract => kVK_ANSI_KeypadMinus, + Key::Equals => kVK_ANSI_KeypadEquals, + Key::NumLock => kVK_ANSI_KeypadClear, + Key::RWin => kVK_RIGHT_COMMAND, + Key::RightShift => kVK_RightShift, + Key::RightControl => kVK_RightControl, + Key::RightAlt => kVK_RightOption, + + Key::Raw(raw_keycode) => raw_keycode, + Key::Layout(c) => self.map_key_board(c), + + Key::Super | Key::Command | Key::Windows | Key::Meta => kVK_Command, + _ => 0, + } + } + + #[inline] + fn map_key_board(&mut self, ch: char) -> CGKeyCode { + let mut code = 0; + unsafe { + let (keyboard, layout) = get_layout(); + if !keyboard.is_null() && !layout.is_null() { + let name_ref = TISGetInputSourceProperty(keyboard, kTISPropertyInputSourceID); + if !name_ref.is_null() { + let name = get_string(name_ref as _); + if let Some(name) = name { + if let Some(m) = self.char_to_vkey_map.get(&name) { + code = *m.get(&ch).unwrap_or(&0); + } else { + let m = get_map(&name, layout); + code = *m.get(&ch).unwrap_or(&0); + self.char_to_vkey_map.insert(name.clone(), m); + } + } + } + } + if !keyboard.is_null() { + CFRelease(keyboard); + } + } + if code > 0 { + return code; + } + match ch { + 'a' => kVK_ANSI_A, + 'b' => kVK_ANSI_B, + 'c' => kVK_ANSI_C, + 'd' => kVK_ANSI_D, + 'e' => kVK_ANSI_E, + 'f' => kVK_ANSI_F, + 'g' => kVK_ANSI_G, + 'h' => kVK_ANSI_H, + 'i' => kVK_ANSI_I, + 'j' => kVK_ANSI_J, + 'k' => kVK_ANSI_K, + 'l' => kVK_ANSI_L, + 'm' => kVK_ANSI_M, + 'n' => kVK_ANSI_N, + 'o' => kVK_ANSI_O, + 'p' => kVK_ANSI_P, + 'q' => kVK_ANSI_Q, + 'r' => kVK_ANSI_R, + 's' => kVK_ANSI_S, + 't' => kVK_ANSI_T, + 'u' => kVK_ANSI_U, + 'v' => kVK_ANSI_V, + 'w' => kVK_ANSI_W, + 'x' => kVK_ANSI_X, + 'y' => kVK_ANSI_Y, + 'z' => kVK_ANSI_Z, + '0' => kVK_ANSI_0, + '1' => kVK_ANSI_1, + '2' => kVK_ANSI_2, + '3' => kVK_ANSI_3, + '4' => kVK_ANSI_4, + '5' => kVK_ANSI_5, + '6' => kVK_ANSI_6, + '7' => kVK_ANSI_7, + '8' => kVK_ANSI_8, + '9' => kVK_ANSI_9, + '-' => kVK_ANSI_Minus, + '=' => kVK_ANSI_Equal, + '[' => kVK_ANSI_LeftBracket, + ']' => kVK_ANSI_RightBracket, + '\\' => kVK_ANSI_Backslash, + ';' => kVK_ANSI_Semicolon, + '\'' => kVK_ANSI_Quote, + ',' => kVK_ANSI_Comma, + '.' => kVK_ANSI_Period, + '/' => kVK_ANSI_Slash, + '`' => kVK_ANSI_Grave, + _ => 0, + } + } +} + +#[inline] +unsafe fn get_string(cf_string: CFStringRef) -> Option { + if !cf_string.is_null() { + let mut buf: [i8; 255] = [0; 255]; + let success = CFStringGetCString( + cf_string, + buf.as_mut_ptr(), + buf.len() as _, + kCFStringEncodingUTF8, + ); + if success != 0 { + let name: &CStr = CStr::from_ptr(buf.as_ptr()); + if let Ok(name) = name.to_str() { + return Some(name.to_string()); + } + } + } + None +} + +#[inline] +unsafe fn get_layout() -> (TISInputSourceRef, *const u8) { + let mut keyboard = TISCopyCurrentKeyboardInputSource(); + let mut layout = null_mut(); + if !keyboard.is_null() { + layout = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + } + if layout.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + // https://github.com/microsoft/vscode/issues/23833 + keyboard = TISCopyCurrentKeyboardLayoutInputSource(); + if !keyboard.is_null() { + layout = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + } + } + if layout.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + if !keyboard.is_null() { + layout = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + } + } + if layout.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + return (null_mut(), null_mut()); + } + let layout_ptr = CFDataGetBytePtr(layout as _); + if layout_ptr.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + return (null_mut(), null_mut()); + } + (keyboard, layout_ptr) +} + +#[inline] +fn get_map(name: &str, layout: *const u8) -> Map { + log::info!("Create keyboard map for {}", name); + let mut keys_down: u32 = 0; + let mut map = Map::new(); + for keycode in 0..128 { + let mut buff = [0_u16; BUF_LEN]; + let kb_type = unsafe { LMGetKbdType() }; + let mut length: UniCharCount = 0; + let _retval = unsafe { + UCKeyTranslate( + layout, + keycode, + kUCKeyActionDisplay as _, + 0, + kb_type as _, + kUCKeyTranslateDeadKeysBit as _, + &mut keys_down, + BUF_LEN, + &mut length, + &mut buff, + ) + }; + if length > 0 { + if let Ok(str) = String::from_utf16(&buff[..length]) { + if let Some(chr) = str.chars().next() { + map.insert(chr, keycode as _); + } + } + } + } + map +} +unsafe impl Send for Enigo {} diff --git a/rust-rdp/rust-desk/libs/enigo/src/macos/mod.rs b/rust-rdp/rust-desk/libs/enigo/src/macos/mod.rs new file mode 100644 index 0000000..286bd74 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/macos/mod.rs @@ -0,0 +1,4 @@ +mod macos_impl; + +pub mod keycodes; +pub use self::macos_impl::Enigo; diff --git a/rust-rdp/rust-desk/libs/enigo/src/win/keycodes.rs b/rust-rdp/rust-desk/libs/enigo/src/win/keycodes.rs new file mode 100644 index 0000000..351e82a --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/win/keycodes.rs @@ -0,0 +1,81 @@ +// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731 +// +// JP/KR mapping https://github.com/TigerVNC/tigervnc/blob/1a008c1380305648ab50f1d99e73439747e9d61d/vncviewer/win32.c#L267 +// altgr handle: https://github.com/TigerVNC/tigervnc/blob/dccb95f345f7a9c5aa785a19d1bfa3fdecd8f8e0/vncviewer/Viewport.cxx#L1066 + +pub const EVK_RETURN: u16 = 0x0D; +pub const EVK_TAB: u16 = 0x09; +pub const EVK_SPACE: u16 = 0x20; +pub const EVK_BACK: u16 = 0x08; +pub const EVK_ESCAPE: u16 = 0x1b; +pub const EVK_LWIN: u16 = 0x5b; +pub const EVK_SHIFT: u16 = 0x10; +//pub const EVK_LSHIFT: u16 = 0xa0; +pub const EVK_RSHIFT: u16 = 0xa1; +//pub const EVK_LMENU: u16 = 0xa4; +pub const EVK_RMENU: u16 = 0xa5; +pub const EVK_CAPITAL: u16 = 0x14; +pub const EVK_MENU: u16 = 0x12; +pub const EVK_LCONTROL: u16 = 0xa2; +pub const EVK_RCONTROL: u16 = 0xa3; +pub const EVK_HOME: u16 = 0x24; +pub const EVK_PRIOR: u16 = 0x21; +pub const EVK_NEXT: u16 = 0x22; +pub const EVK_END: u16 = 0x23; +pub const EVK_LEFT: u16 = 0x25; +pub const EVK_RIGHT: u16 = 0x27; +pub const EVK_UP: u16 = 0x26; +pub const EVK_DOWN: u16 = 0x28; +pub const EVK_DELETE: u16 = 0x2E; +pub const EVK_F1: u16 = 0x70; +pub const EVK_F2: u16 = 0x71; +pub const EVK_F3: u16 = 0x72; +pub const EVK_F4: u16 = 0x73; +pub const EVK_F5: u16 = 0x74; +pub const EVK_F6: u16 = 0x75; +pub const EVK_F7: u16 = 0x76; +pub const EVK_F8: u16 = 0x77; +pub const EVK_F9: u16 = 0x78; +pub const EVK_F10: u16 = 0x79; +pub const EVK_F11: u16 = 0x7a; +pub const EVK_F12: u16 = 0x7b; +pub const EVK_NUMPAD0: u16 = 0x60; +pub const EVK_NUMPAD1: u16 = 0x61; +pub const EVK_NUMPAD2: u16 = 0x62; +pub const EVK_NUMPAD3: u16 = 0x63; +pub const EVK_NUMPAD4: u16 = 0x64; +pub const EVK_NUMPAD5: u16 = 0x65; +pub const EVK_NUMPAD6: u16 = 0x66; +pub const EVK_NUMPAD7: u16 = 0x67; +pub const EVK_NUMPAD8: u16 = 0x68; +pub const EVK_NUMPAD9: u16 = 0x69; +pub const EVK_CANCEL: u16 = 0x03; +pub const EVK_CLEAR: u16 = 0x0C; +pub const EVK_PAUSE: u16 = 0x13; +pub const EVK_KANA: u16 = 0x15; +pub const EVK_HANGUL: u16 = 0x15; +pub const EVK_JUNJA: u16 = 0x17; +pub const EVK_FINAL: u16 = 0x18; +pub const EVK_HANJA: u16 = 0x19; +pub const EVK_KANJI: u16 = 0x19; +pub const EVK_CONVERT: u16 = 0x1C; +pub const EVK_SELECT: u16 = 0x29; +pub const EVK_PRINT: u16 = 0x2A; +pub const EVK_EXECUTE: u16 = 0x2B; +pub const EVK_SNAPSHOT: u16 = 0x2C; +pub const EVK_INSERT: u16 = 0x2D; +pub const EVK_HELP: u16 = 0x2F; +pub const EVK_SLEEP: u16 = 0x5F; +pub const EVK_SEPARATOR: u16 = 0x6C; +pub const EVK_VOLUME_MUTE: u16 = 0xAD; +pub const EVK_VOLUME_DOWN: u16 = 0xAE; +pub const EVK_VOLUME_UP: u16 = 0xAF; +pub const EVK_NUMLOCK: u16 = 0x90; +pub const EVK_SCROLL: u16 = 0x91; +pub const EVK_RWIN: u16 = 0x5C; +pub const EVK_APPS: u16 = 0x5D; +pub const EVK_ADD: u16 = 0x6B; +pub const EVK_MULTIPLY: u16 = 0x6A; +pub const EVK_SUBTRACT: u16 = 0x6D; +pub const EVK_DECIMAL: u16 = 0x6E; +pub const EVK_DIVIDE: u16 = 0x6F; diff --git a/rust-rdp/rust-desk/libs/enigo/src/win/mod.rs b/rust-rdp/rust-desk/libs/enigo/src/win/mod.rs new file mode 100644 index 0000000..024d7a3 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/win/mod.rs @@ -0,0 +1,4 @@ +mod win_impl; + +pub mod keycodes; +pub use self::win_impl::Enigo; diff --git a/rust-rdp/rust-desk/libs/enigo/src/win/win_impl.rs b/rust-rdp/rust-desk/libs/enigo/src/win/win_impl.rs new file mode 100644 index 0000000..4a5f9f8 --- /dev/null +++ b/rust-rdp/rust-desk/libs/enigo/src/win/win_impl.rs @@ -0,0 +1,378 @@ +use winapi; + +use self::winapi::ctypes::c_int; +use self::winapi::shared::{minwindef::*, windef::*}; +use self::winapi::um::winbase::*; +use self::winapi::um::winuser::*; + +use crate::win::keycodes::*; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use std::mem::*; + +extern "system" { + pub fn GetLastError() -> DWORD; +} + +/// The main struct for handling the event emitting +#[derive(Default)] +pub struct Enigo; +static mut LAYOUT: HKL = std::ptr::null_mut(); + +fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { + let mut input = INPUT { + type_: INPUT_MOUSE, + u: unsafe { + transmute(MOUSEINPUT { + dx, + dy, + mouseData: data, + dwFlags: flags, + time: 0, + dwExtraInfo: 0, + }) + }, + }; + unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } +} + +fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { + let mut scan = scan; + unsafe { + // https://github.com/rustdesk/rustdesk/issues/366 + if scan == 0 { + if LAYOUT.is_null() { + let current_window_thread_id = + GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()); + LAYOUT = GetKeyboardLayout(current_window_thread_id); + } + scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _; + } + } + let mut input = INPUT { + type_: INPUT_KEYBOARD, + u: unsafe { + transmute_copy(&KEYBDINPUT { + wVk: vk, + wScan: scan, + dwFlags: flags, + time: 0, + dwExtraInfo: 0, + }) + }, + }; + unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } +} + +fn get_error() -> String { + unsafe { + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let errno = GetLastError(); + let chars_copied = FormatMessageW( + FORMAT_MESSAGE_IGNORE_INSERTS + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + std::ptr::null(), + errno, + 0, + buff.as_mut_ptr(), + (buff_size + 1) as u32, + std::ptr::null_mut(), + ); + if chars_copied == 0 { + return "".to_owned(); + } + let mut curr_char: usize = chars_copied as usize; + while curr_char > 0 { + let ch = buff[curr_char]; + + if ch >= ' ' as u16 { + break; + } + curr_char -= 1; + } + let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char); + let err_msg = String::from_utf16(sl); + return err_msg.unwrap_or("".to_owned()); + } +} + +impl MouseControllable for Enigo { + fn mouse_move_to(&mut self, x: i32, y: i32) { + mouse_event( + MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, + 0, + (x - unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) }) * 65535 + / unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) }, + (y - unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) }) * 65535 + / unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) }, + ); + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + mouse_event(MOUSEEVENTF_MOVE, 0, x, y); + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + let res = mouse_event( + match button { + MouseButton::Left => MOUSEEVENTF_LEFTDOWN, + MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, + MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, + _ => unimplemented!(), + }, + 0, + 0, + 0, + ); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + mouse_event( + match button { + MouseButton::Left => MOUSEEVENTF_LEFTUP, + MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, + MouseButton::Right => MOUSEEVENTF_RIGHTUP, + _ => unimplemented!(), + }, + 0, + 0, + 0, + ); + } + + fn mouse_click(&mut self, button: MouseButton) { + self.mouse_down(button).ok(); + self.mouse_up(button); + } + + fn mouse_scroll_x(&mut self, length: i32) { + mouse_event(MOUSEEVENTF_HWHEEL, unsafe { transmute(length * 120) }, 0, 0); + } + + fn mouse_scroll_y(&mut self, length: i32) { + mouse_event(MOUSEEVENTF_WHEEL, unsafe { transmute(length * 120) }, 0, 0); + } +} + +impl KeyboardControllable for Enigo { + fn key_sequence(&mut self, sequence: &str) { + let mut buffer = [0; 2]; + + for c in sequence.chars() { + // Windows uses uft-16 encoding. We need to check + // for variable length characters. As such some + // characters can be 32 bit long and those are + // encoded in such called hight and low surrogates + // each 16 bit wide that needs to be send after + // another to the SendInput function without + // being interrupted by "keyup" + let result = c.encode_utf16(&mut buffer); + if result.len() == 1 { + self.unicode_key_click(result[0]); + } else { + for utf16_surrogate in result { + self.unicode_key_down(utf16_surrogate.clone()); + } + // do i need to produce a keyup? + // self.unicode_key_up(0); + } + } + } + + fn key_click(&mut self, key: Key) { + let vk = self.key_to_keycode(key); + keybd_event(0, vk, 0); + keybd_event(KEYEVENTF_KEYUP, vk, 0); + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + let res = keybd_event(0, self.key_to_keycode(key), 0); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); + } + + fn get_key_state(&mut self, key: Key) -> bool { + let keycode = self.key_to_keycode(key); + let x = unsafe { GetKeyState(keycode as _) }; + if key == Key::CapsLock || key == Key::NumLock || key == Key::Scroll { + return (x & 0x1) == 0x1; + } + return (x as u16 & 0x8000) == 0x8000; + } +} + +impl Enigo { + /// Gets the (width, height) of the main display in screen coordinates (pixels). + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut size = Enigo::main_display_size(); + /// ``` + pub fn main_display_size() -> (usize, usize) { + let w = unsafe { GetSystemMetrics(SM_CXSCREEN) as usize }; + let h = unsafe { GetSystemMetrics(SM_CYSCREEN) as usize }; + (w, h) + } + + /// Gets the location of mouse in screen coordinates (pixels). + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut location = Enigo::mouse_location(); + /// ``` + pub fn mouse_location() -> (i32, i32) { + let mut point = POINT { x: 0, y: 0 }; + let result = unsafe { GetCursorPos(&mut point) }; + if result != 0 { + (point.x, point.y) + } else { + (0, 0) + } + } + + fn unicode_key_click(&self, unicode_char: u16) { + self.unicode_key_down(unicode_char); + self.unicode_key_up(unicode_char); + } + + fn unicode_key_down(&self, unicode_char: u16) { + keybd_event(KEYEVENTF_UNICODE, 0, unicode_char); + } + + fn unicode_key_up(&self, unicode_char: u16) { + keybd_event(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, unicode_char); + } + + fn key_to_keycode(&self, key: Key) -> u16 { + unsafe { + LAYOUT = std::ptr::null_mut(); + } + // do not use the codes from crate winapi they're + // wrongly typed with i32 instead of i16 use the + // ones provided by win/keycodes.rs that are prefixed + // with an 'E' infront of the original name + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + match key { + Key::Alt => EVK_MENU, + Key::Backspace => EVK_BACK, + Key::CapsLock => EVK_CAPITAL, + Key::Control => EVK_LCONTROL, + Key::Delete => EVK_DELETE, + Key::DownArrow => EVK_DOWN, + Key::End => EVK_END, + Key::Escape => EVK_ESCAPE, + Key::F1 => EVK_F1, + Key::F10 => EVK_F10, + Key::F11 => EVK_F11, + Key::F12 => EVK_F12, + Key::F2 => EVK_F2, + Key::F3 => EVK_F3, + Key::F4 => EVK_F4, + Key::F5 => EVK_F5, + Key::F6 => EVK_F6, + Key::F7 => EVK_F7, + Key::F8 => EVK_F8, + Key::F9 => EVK_F9, + Key::Home => EVK_HOME, + Key::LeftArrow => EVK_LEFT, + Key::Option => EVK_MENU, + Key::PageDown => EVK_NEXT, + Key::PageUp => EVK_PRIOR, + Key::Return => EVK_RETURN, + Key::RightArrow => EVK_RIGHT, + Key::Shift => EVK_SHIFT, + Key::Space => EVK_SPACE, + Key::Tab => EVK_TAB, + Key::UpArrow => EVK_UP, + Key::Numpad0 => EVK_NUMPAD0, + Key::Numpad1 => EVK_NUMPAD1, + Key::Numpad2 => EVK_NUMPAD2, + Key::Numpad3 => EVK_NUMPAD3, + Key::Numpad4 => EVK_NUMPAD4, + Key::Numpad5 => EVK_NUMPAD5, + Key::Numpad6 => EVK_NUMPAD6, + Key::Numpad7 => EVK_NUMPAD7, + Key::Numpad8 => EVK_NUMPAD8, + Key::Numpad9 => EVK_NUMPAD9, + Key::Cancel => EVK_CANCEL, + Key::Clear => EVK_CLEAR, + Key::Pause => EVK_PAUSE, + Key::Kana => EVK_KANA, + Key::Hangul => EVK_HANGUL, + Key::Junja => EVK_JUNJA, + Key::Final => EVK_FINAL, + Key::Hanja => EVK_HANJA, + Key::Kanji => EVK_KANJI, + Key::Convert => EVK_CONVERT, + Key::Select => EVK_SELECT, + Key::Print => EVK_PRINT, + Key::Execute => EVK_EXECUTE, + Key::Snapshot => EVK_SNAPSHOT, + Key::Insert => EVK_INSERT, + Key::Help => EVK_HELP, + Key::Sleep => EVK_SLEEP, + Key::Separator => EVK_SEPARATOR, + Key::Mute => EVK_VOLUME_MUTE, + Key::VolumeDown => EVK_VOLUME_DOWN, + Key::VolumeUp => EVK_VOLUME_UP, + Key::Scroll => EVK_SCROLL, + Key::NumLock => EVK_NUMLOCK, + Key::RWin => EVK_RWIN, + Key::Apps => EVK_APPS, + Key::Add => EVK_ADD, + Key::Multiply => EVK_MULTIPLY, + Key::Decimal => EVK_DECIMAL, + Key::Subtract => EVK_SUBTRACT, + Key::Divide => EVK_DIVIDE, + Key::NumpadEnter => EVK_RETURN, + Key::Equals => '=' as _, + Key::RightShift => EVK_RSHIFT, + Key::RightControl => EVK_RCONTROL, + Key::RightAlt => EVK_RMENU, + + Key::Raw(raw_keycode) => raw_keycode, + Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()), + Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN, + } + } + + fn get_layoutdependent_keycode(&self, string: String) -> u16 { + // get the first char from the string ignore the rest + // ensure its not a multybyte char + if let Some(chr) = string.chars().nth(0) { + // NOTE VkKeyScanW uses the current keyboard LAYOUT + // to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout + // or load one with LoadKeyboardLayoutW + let current_window_thread_id = + unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) }; + unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) }; + let keycode_and_shiftstate = unsafe { VkKeyScanExW(chr as _, LAYOUT) }; + keycode_and_shiftstate as _ + } else { + 0 + } + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/.gitignore b/rust-rdp/rust-desk/libs/hbb_common/.gitignore new file mode 100644 index 0000000..b1cf151 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +src/protos/ diff --git a/rust-rdp/rust-desk/libs/hbb_common/Cargo.toml b/rust-rdp/rust-desk/libs/hbb_common/Cargo.toml new file mode 100644 index 0000000..c819347 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "hbb_common" +version = "0.1.0" +authors = ["rustdesk"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +protobuf = "3.0.0-alpha.2" +tokio = { version = "1.15", features = ["full"] } +tokio-util = { version = "0.6", features = ["full"] } +futures = "0.3" +bytes = "1.1" +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 = ["quinn"] + +[build-dependencies] +protobuf-codegen-pure = "3.0.0-alpha.2" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser"] } + +[dev-dependencies] +toml = "0.5" +serde_json = "1.0" diff --git a/rust-rdp/rust-desk/libs/hbb_common/build.rs b/rust-rdp/rust-desk/libs/hbb_common/build.rs new file mode 100644 index 0000000..99dacb7 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/build.rs @@ -0,0 +1,9 @@ +fn main() { + std::fs::create_dir_all("src/protos").unwrap(); + protobuf_codegen_pure::Codegen::new() + .out_dir("src/protos") + .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .include("protos") + .run() + .expect("Codegen failed."); +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/protos/message.proto b/rust-rdp/rust-desk/libs/hbb_common/protos/message.proto new file mode 100644 index 0000000..ac313cb --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/protos/message.proto @@ -0,0 +1,409 @@ +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; + } +} + +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; +} + +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; +} + +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; + } + + 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; +} + +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; } + +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; + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/protos/rendezvous.proto b/rust-rdp/rust-desk/libs/hbb_common/protos/rendezvous.proto new file mode 100644 index 0000000..4fce625 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/protos/rendezvous.proto @@ -0,0 +1,166 @@ +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; + ConnType conn_type = 4; +} + +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; +} + +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; + ConnType conn_type = 7; +} + +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 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; + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/bytes_codec.rs b/rust-rdp/rust-desk/libs/hbb_common/src/bytes_codec.rs new file mode 100644 index 0000000..e029f1c --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/bytes_codec.rs @@ -0,0 +1,274 @@ +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> { + 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> { + 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, 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 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 = 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 = 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 = 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 = 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 = 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 = 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); + } + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/compress.rs b/rust-rdp/rust-desk/libs/hbb_common/src/compress.rs new file mode 100644 index 0000000..a969ccf --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/compress.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use zstd::block::{Compressor, Decompressor}; + +thread_local! { + static COMPRESSOR: RefCell = RefCell::new(Compressor::new()); + static DECOMPRESSOR: RefCell = 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 { + 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 { + 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 +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/config.rs b/rust-rdp/rust-desk/libs/hbb_common/src/config.rs new file mode 100644 index 0000000..c3d7d23 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/config.rs @@ -0,0 +1,898 @@ +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 APP_NAME: &str = "RustDesk"; +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(windows)] // windows, 32x32, bigger very ugly after shrink +pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAolBMVEUAAAAAcf8Acf8Acf8AcP8Acf8Acf8Acf8AcP8Acf/////9/v/7/f+gyv9wr/8Ld/8GdP/A3P+nzv+Qwf9mqv8lhv8UfP+Kvv88k/8zjv/x+P/Y6f/U5//J4f/F3/+x1P+pz/+izP+Cuv9Zov9Qnv9FmP8gg//f7f+42P96tv9fpv8ui//1+f/q8//o8v/Q5f+ax/+Zxv+VxP+Fu/9rrf8rif+x1o3FAAAACXRSTlMAv/RPTPKHioRsIqhAAAABNklEQVQ4y4WT6XaCQAyFB7S2GUD2RaxSKGDV7sv7v1on4djEluL9Ncn5biaZk1FKzSwbRmVbM2V0DRNaGD9Maq6sacBS9jRwpUTw1Ww7SEOvkwQDaeVofQeJ1nrXjgD3pTaqCDCg/xs4OBrlwUGTHvxzoHAx69Y9+Hk5oGdAsCNXSoHfEN1JYEme4KcfvO9WAmtMBNx6jIaCgSPGLbAyvKRhAEcrAxAKTSZiACu+gNSbyWwZeKdQKj91yRXWIOWZTMgATun6EtjgczNQaKMEWD0+xJ6B4AnvECVqdKwYoJ50hIPyw25AANkzpsLB00cYOIUEoHU0uVaQVcMxBwlgVZJ3Orz+3ahH6gP2tBgfYzsZmdo1fGIzRx5Irn2WxKaHeJnKtb/4cS5/PbWACd0oo/n/39/4vwGFYSxtSYV4OAAAAABJRU5ErkJggg== +"; +#[cfg(target_os = "linux")] // 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")] +pub const ORG: &str = "com.carriez"; + +type Size = (i32, i32, i32, i32); + +lazy_static::lazy_static! { + static ref CONFIG: Arc> = Arc::new(RwLock::new(Config::load())); + static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); + pub static ref ONLINE: Arc>> = Default::default(); +} +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref APP_DIR: Arc> = 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)] +pub struct Config { + #[serde(default)] + id: String, + #[serde(default)] + password: String, + #[serde(default)] + salt: String, + #[serde(default)] + key_pair: (Vec, Vec), // sk, pk + #[serde(default)] + key_confirmed: bool, + #[serde(default)] + keys_confirmed: HashMap, +} + +#[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)] +pub struct Config2 { + #[serde(default)] + remote_id: String, // latest used one + #[serde(default)] + size: Size, + #[serde(default)] + rendezvous_server: String, + #[serde(default)] + nat_type: i32, + #[serde(default)] + serial: i32, + + #[serde(default)] + socks: Option, + + // the other scalar value must before this + #[serde(default)] + pub options: HashMap, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct PeerConfig { + #[serde(default)] + pub password: Vec, + #[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, + #[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, + + // the other scalar value must before this + #[serde(default)] + pub options: HashMap, + #[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_::("2") + } + + fn reload(&mut self) { + let new_config = Config2::load(); + *self = new_config; + } + + fn store(&self) { + Config::store_(self, "2"); + } +} + +impl Config { + fn load_( + suffix: &str, + ) -> T { + let file = Self::file_(suffix); + log::debug!("Configuration path: {}", file.display()); + let cfg = match confy::load_path(&file) { + Ok(config) => config, + Err(err) => { + log::error!("Failed to load config: {}", err); + T::default() + } + }; + if suffix.is_empty() { + log::debug!("{:?}", cfg); + } + cfg + } + + fn reload(&mut self) { + let new_config = Config::load(); + *self = new_config; + } + + fn store_(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_::("") + } + + fn store(&self) { + Config::store_(self, ""); + } + + pub fn file() -> PathBuf { + Self::file_("") + } + + pub fn import(from: &str) { + log::info!("import {}", from); + // load first to create path + Self::load(); + crate::allow_err!(std::fs::copy(from, Self::file())); + crate::allow_err!(std::fs::copy( + from.replace(".toml", "2.toml"), + Self::file_("2") + )); + } + + pub fn save_tmp() -> String { + let _lock = CONFIG.read().unwrap(); // do not use let _, which will be dropped immediately + let path = Self::file_("2").to_str().unwrap_or("").to_owned(); + let path2 = format!("{}_tmp", path); + crate::allow_err!(std::fs::copy(&path, &path2)); + let path = Self::file().to_str().unwrap_or("").to_owned(); + let path2 = format!("{}_tmp", path); + crate::allow_err!(std::fs::copy(&path, &path2)); + path2 + } + + fn file_(suffix: &str) -> PathBuf { + let name = format!("{}{}", APP_NAME, suffix); + Self::path(name).with_extension("toml") + } + + pub fn get_home() -> PathBuf { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Self::path(""); + 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: 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; + // /var/root for root + if let Some(project) = ProjectDirs::from("", org, APP_NAME) { + 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)); + return path.clone(); + } + } + #[cfg(target_os = "linux")] + { + let mut path = Self::get_home(); + path.push(format!(".local/share/logs/{}", APP_NAME)); + 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, postfix) + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + let mut path: PathBuf = format!("/tmp/{}", APP_NAME).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 = CONFIG2.write().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 { + let s = Self::get_option("custom-rendezvous-server"); + if !s.is_empty() { + return vec![s]; + } + let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; + if serial_obsolute { + let ss: Vec = 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 { + #[cfg(any(target_os = "android", target_os = "ios"))] + return None; + 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::() % 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, Vec)) { + let mut config = CONFIG.write().unwrap(); + if config.key_pair == pair { + return; + } + config.key_pair = pair; + config.store(); + } + + pub fn get_key_pair() -> (Vec, Vec) { + // 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_options() -> HashMap { + CONFIG2.read().unwrap().options.clone() + } + + pub fn set_options(v: HashMap) { + 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 get_size() -> Size { + CONFIG2.read().unwrap().size + } + + pub fn set_size(x: i32, y: i32, w: i32, h: i32) { + let mut config = CONFIG2.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 = CONFIG2.write().unwrap(); + if remote_id == config.remote_id { + return; + } + config.remote_id = remote_id.into(); + config.store(); + } + + pub fn get_remote_id() -> String { + CONFIG2.read().unwrap().remote_id.clone() + } + + pub fn set_socks(socks: Option) { + let mut config = CONFIG2.write().unwrap(); + if config.socks == socks { + return; + } + config.socks = socks; + config.store(); + } + + pub fn get_socks() -> Option { + CONFIG2.read().unwrap().socks.clone() + } + + pub fn get_network_type() -> NetworkType { + match &CONFIG2.read().unwrap().socks { + None => NetworkType::Direct, + Some(_) => NetworkType::ProxySocks, + } + } + + pub fn sync_config_to_user>(target_username: String, to_dir: P) -> bool { + let config1_root_file_path = Config::file_(""); + let config1_filename = config1_root_file_path.file_name(); + + let config2_root_file_path = Config::file_("2"); + let config2_filename = config2_root_file_path.file_name(); + + let config1_to_file_path = to_dir + .as_ref() + .join(PathBuf::from(&config1_filename.unwrap())); + let config2_to_file_path = to_dir + .as_ref() + .join(PathBuf::from(&config2_filename.unwrap())); + + log::info!( + "config1_root_path:{}", + &config1_root_file_path.as_path().to_str().unwrap() + ); + log::info!( + "config2_root_path:{}", + &config2_root_file_path.as_path().to_str().unwrap() + ); + log::info!( + "config1_to_path:{}", + &config1_to_file_path.as_path().to_str().unwrap() + ); + log::info!( + "config2_to_path:{}", + &config2_to_file_path.as_path().to_str().unwrap() + ); + + match std::fs::copy(&config1_root_file_path, &config1_to_file_path) { + Err(e) => log::error!( + "copy config {} to user failed: {}", + config1_filename.unwrap().to_str().unwrap(), + e + ), + _ => {} + } + + match std::fs::copy(&config2_root_file_path, &config2_to_file_path) { + Err(e) => log::error!( + "copy config {} to user failed: {}", + config2_filename.unwrap().to_str().unwrap(), + e + ), + _ => {} + } + + let success = std::process::Command::new("chown") + .arg(&target_username.to_string()) + .arg(&config1_to_file_path.to_str().unwrap().to_string()) + .arg(&config2_to_file_path.to_str().unwrap().to_string()) + .spawn() + .is_ok(); + + if success { + CONFIG.write().unwrap().reload(); + CONFIG2.write().unwrap().reload(); + } + + return success; + } + + pub fn sync_config_to_root>(from_file_path: P) -> bool { + if let Some(filename) = from_file_path.as_ref().file_name() { + let to = Config::path(filename); + return match std::fs::copy(from_file_path, &to) { + Ok(count) => { + if count > 0 { + return std::process::Command::new("chown") + .arg("root") + .arg(&to.to_str().unwrap().to_string()) + .spawn() + .is_ok(); + } + false + } + Err(e) => { + log::error!("sync_config_to_root failed: {}", e); + false + } + }; + } + false + } +} + +const PEERS: &str = "peers"; + +impl PeerConfig { + pub fn load(id: &str) -> PeerConfig { + let _ = 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 _ = 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::path(path).with_extension("toml") + } + + 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::, _>>() + { + 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 = fs::metadata(p) + .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH)) + .unwrap_or(SystemTime::UNIX_EPOCH); + 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 Fav { + #[serde(default)] + pub peers: Vec, +} + +impl Fav { + pub fn load() -> Fav { + let _ = CONFIG.read().unwrap(); // for lock + match confy::load_path(&Config::file_("_fav")) { + Ok(fav) => fav, + Err(err) => { + log::error!("Failed to load fav: {}", err); + Default::default() + } + } + } + + pub fn store(peers: Vec) { + let f = Fav { peers }; + if let Err(err) = confy::store_path(Config::file_("_fav"), f) { + log::error!("Failed to store fav: {}", err); + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct LanPeers { + #[serde(default)] + pub peers: String, +} + +impl LanPeers { + pub fn load() -> LanPeers { + let _ = 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 { + 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()); + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/fs.rs b/rust-rdp/rust-desk/libs/hbb_common/src/fs.rs new file mode 100644 index 0000000..99e5947 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/fs.rs @@ -0,0 +1,554 @@ +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 { + 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> { + 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> { + read_dir_recursive(&get_path(path), &get_path(""), include_hidden) +} + +#[derive(Default)] +pub struct TransferJob { + id: i32, + path: PathBuf, + files: Vec, + file_num: i32, + file: Option, + 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) -> 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 { + 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 { + &self.files + } + + #[inline] + pub fn set_files(&mut self, files: Vec) { + 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) -> 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?); + } + if block.compressed { + let tmp = decompress(&block.data); + self.file.as_mut().unwrap().write_all(&tmp).await?; + self.finished_size += tmp.len() as u64; + } else { + self.file.as_mut().unwrap().write_all(&block.data).await?; + self.finished_size += block.data.len() as u64; + } + self.transferred += block.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> { + 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 = 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(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, files: Vec) -> Message { + let mut resp = FileResponse::new(); + resp.set_dir(FileDirectory { + id, + 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) -> 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) { + *jobs = jobs.drain(0..).filter(|x| x.id() != id).collect(); +} + +#[inline] +pub fn get_job(id: i32, jobs: &mut Vec) -> Option<&mut TransferJob> { + jobs.iter_mut().filter(|x| x.id() == id).next() +} + +pub async fn handle_read_jobs( + jobs: &mut Vec, + 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(()) +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/lib.rs b/rust-rdp/rust-desk/libs/hbb_common/src/lib.rs new file mode 100644 index 0000000..e66eed7 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/lib.rs @@ -0,0 +1,197 @@ +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; +pub use rand; +pub use regex; +pub use sodiumoxide; +pub use tokio_socks; +pub use tokio_socks::IntoTargetAddr; +pub use tokio_socks::TargetAddr; +pub use mac_address; + +#[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(ms: u64, future: T) -> tokio::time::Timeout { + tokio::time::timeout(std::time::Duration::from_millis(ms), future) +} + +pub type ResultType = anyhow::Result; + +/// 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 { + 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::() + .parse::() + .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

(filename: P) -> io::Result>> +where + P: AsRef, +{ + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + +pub fn get_version_number(v: &str) -> i64 { + let mut n = 0; + for x in v.split(".") { + n = n * 1000 + x.parse::().unwrap_or(0); + } + n +} + +#[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))); + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/quic.rs b/rust-rdp/rust-desk/libs/hbb_common/src/quic.rs new file mode 100644 index 0000000..ada2acd --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/quic.rs @@ -0,0 +1,135 @@ +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; +type Receiver = mpsc::UnboundedReceiver; + +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 { + 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> { + Connection::new_for_server(&mut self.incoming).await + } +} + +pub struct Connection { + conn: quinn::Connection, + tx: quinn::SendStream, + rx: Receiver, +} + +type Value = ResultType>; + +impl Connection { + async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType> { + 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::(); + 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 { + let (tx, rx_quic) = conn.open_bi().await?; + let (tx_mpsc, rx) = mpsc::unbounded_channel::(); + 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 { + // 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(()) +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/socket_client.rs b/rust-rdp/rust-desk/libs/hbb_common/src/socket_client.rs new file mode 100644 index 0000000..0375b71 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/socket_client.rs @@ -0,0 +1,91 @@ +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 { + use std::net::ToSocketAddrs; + host.to_socket_addrs()?.next().context("Failed to solve") +} + +pub fn get_target_addr(host: &str) -> ResultType> { + 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 { + 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)? + .next() + .context("Invalid target addr")?; + Ok(FramedStream::new(addr, local, ms_timeout).await?) + } +} + +pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType { + 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(local: T) -> ResultType> { + match Config::get_network_type() { + NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)), + _ => Ok(None), + } +} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/tcp.rs b/rust-rdp/rust-desk/libs/hbb_common/src/tcp.rs new file mode 100644 index 0000000..7966920 --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/tcp.rs @@ -0,0 +1,285 @@ +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); + +pub struct FramedStream( + Framed, + SocketAddr, + Option<(Key, u64, u64)>, + u64, +); + +impl Deref for FramedStream { + type Target = Framed; + + 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; + + 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 { + 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( + remote_addr: T1, + local_addr: T2, + ms_timeout: u64, + ) -> ResultType { + 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 + 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) -> 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> { + 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> { + 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(addr: T, reuse: bool) -> ResultType { + 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> { + 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> { + AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.0), cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx) + } +} + +impl TcpStreamTrait for R {} diff --git a/rust-rdp/rust-desk/libs/hbb_common/src/udp.rs b/rust-rdp/rust-desk/libs/hbb_common/src/udp.rs new file mode 100644 index 0000000..d1896bd --- /dev/null +++ b/rust-rdp/rust-desk/libs/hbb_common/src/udp.rs @@ -0,0 +1,144 @@ +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), + ProxySocks(Socks5UdpFramed), +} + +fn new_socket(addr: SocketAddr, reuse: bool) -> Result { + 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)?; + } + socket.bind(&addr.into())?; + Ok(socket) +} + +impl FramedSocket { + pub async fn new(addr: T) -> ResultType { + let socket = UdpSocket::bind(addr).await?; + Ok(Self::Direct(UdpFramed::new(socket, BytesCodec::new()))) + } + + #[allow(clippy::never_loop)] + pub async fn new_reuse(addr: T) -> ResultType { + for addr in addr.to_socket_addrs()? { + let socket = new_socket(addr, true)?.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_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( + proxy: P, + local: T, + username: &'a str, + password: &'a str, + ms_timeout: u64, + ) -> ResultType { + 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?, + _ => unreachable!(), + }, + 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?, + _ => unreachable!(), + }, + Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, + }; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option)>> { + 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)>> { + if let Ok(res) = + tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await + { + res + } else { + None + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/.gitignore b/rust-rdp/rust-desk/libs/scrap/.gitignore new file mode 100644 index 0000000..5e10219 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/.gitignore @@ -0,0 +1,4 @@ +/target/ +**/*.rs.bk +Cargo.lock +generated/ diff --git a/rust-rdp/rust-desk/libs/scrap/Cargo.toml b/rust-rdp/rust-desk/libs/scrap/Cargo.toml new file mode 100644 index 0000000..aaee9b3 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "scrap" +description = "Screen capture made easy." +version = "0.5.0" +repository = "https://github.com/quadrupleslap/scrap" +documentation = "https://docs.rs/scrap" +keywords = ["screen", "capture", "record"] +license = "MIT" +authors = ["Ram "] +edition = "2018" + +[features] +wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"] + +[dependencies] +block = "0.1" +cfg-if = "1.0" +libc = "0.2" +num_cpus = "1.13" + +[dependencies.winapi] +version = "0.3" +default-features = true +features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser"] + +[dev-dependencies] +repng = "0.2" +docopt = "1.1" +webm = "1.0" +serde = {version="1.0", features=["derive"]} +quest = "0.3" + +[build-dependencies] +target_build_utils = "0.3" +bindgen = "0.59" + +[target.'cfg(target_os = "linux")'.dependencies] +dbus = { version = "0.9", optional = true } +tracing = { version = "0.1", optional = true } +gstreamer = { version = "0.16", optional = true } +gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } +gstreamer-video = { version = "0.16", optional = true } diff --git a/rust-rdp/rust-desk/libs/scrap/README.md b/rust-rdp/rust-desk/libs/scrap/README.md new file mode 100644 index 0000000..5809cb8 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/README.md @@ -0,0 +1,62 @@ +Derived from https://github.com/quadrupleslap/scrap + +# scrap + +Scrap records your screen! At least it does if you're on Windows, macOS, or Linux. + +## Usage + +```toml +[dependencies] +scrap = "0.5" +``` + +Its API is as simple as it gets! + +```rust +struct Display; /// A screen. +struct Frame; /// An array of the pixels that were on-screen. +struct Capturer; /// A recording instance. + +impl Capturer { + /// Begin recording. + pub fn new(display: Display) -> io::Result; + + /// Try to get a frame. + /// Returns WouldBlock if it's not ready yet. + pub fn frame<'a>(&'a mut self) -> io::Result>; + + pub fn width(&self) -> usize; + pub fn height(&self) -> usize; +} + +impl Display { + /// The primary screen. + pub fn primary() -> io::Result; + + /// All the screens. + pub fn all() -> io::Result>; + + pub fn width(&self) -> usize; + pub fn height(&self) -> usize; +} + +impl<'a> ops::Deref for Frame<'a> { + /// A frame is just an array of bytes. + type Target = [u8]; +} +``` + +## The Frame Format + +- The frame format is guaranteed to be **packed BGRA**. +- The width and height are guaranteed to remain constant. +- The stride might be greater than the width, and it may also vary between frames. + +## System Requirements + +OS | Minimum Requirements +--------|--------------------- +macOS | macOS 10.8 +Linux | XCB + SHM + RandR +Windows | DirectX 11.1 diff --git a/rust-rdp/rust-desk/libs/scrap/build.rs b/rust-rdp/rust-desk/libs/scrap/build.rs new file mode 100644 index 0000000..a2707d5 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/build.rs @@ -0,0 +1,114 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +fn find_package(name: &str) -> Vec { + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: PathBuf = vcpkg_root.into(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } + let mut target = if target_os == "macos" { + "x64-osx".to_owned() + } else if target_os == "windows" { + "x64-windows-static".to_owned() + } else { + format!("{}-{}", target_arch, target_os) + }; + if target_arch == "x86" { + target = target.replace("x64", "x86"); + } + println!("cargo:info={}", target); + path.push("installed"); + path.push(target); + let lib = name.trim_start_matches("lib").to_string(); + println!("{}", format!("cargo:rustc-link-lib=static={}", lib)); + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + let include = path.join("include"); + println!("{}", format!("cargo:include={}", include.to_str().unwrap())); + vec![include] +} + +fn generate_bindings( + ffi_header: &Path, + include_paths: &[PathBuf], + ffi_rs: &Path, + exact_file: &Path, +) { + let mut b = bindgen::builder() + .header(ffi_header.to_str().unwrap()) + .allowlist_type("^[vV].*") + .allowlist_var("^[vV].*") + .allowlist_function("^[vV].*") + .rustified_enum("^v.*") + .trust_clang_mangling(false) + .layout_tests(false) // breaks 32/64-bit compat + .generate_comments(false); // vpx comments have prefix /*!\ + + for dir in include_paths { + b = b.clang_arg(format!("-I{}", dir.display())); + } + + b.generate().unwrap().write_to_file(ffi_rs).unwrap(); + fs::copy(ffi_rs, exact_file).ok(); // ignore failure +} + +fn gen_vpx() { + let includes = find_package("libvpx"); + let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); + let src_dir = Path::new(&src_dir); + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + let ffi_header = src_dir.join("vpx_ffi.h"); + println!("rerun-if-changed={}", ffi_header.display()); + for dir in &includes { + println!("rerun-if-changed={}", dir.display()); + } + + let ffi_rs = out_dir.join("vpx_ffi.rs"); + let exact_file = src_dir.join("generated").join("vpx_ffi.rs"); + generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file); +} + +fn main() { + // note: all link symbol names in x86 (32-bit) are prefixed wth "_". + // run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc, + // please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc", + // then set x64 to default by "rustup default stable-x86_64-pc-windows-msvc" + let target = target_build_utils::TargetInfo::new(); + if target.unwrap().target_pointer_width() != "64" { + // panic!("Only support 64bit system"); + } + env::remove_var("CARGO_CFG_TARGET_FEATURE"); + env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static"); + + find_package("libyuv"); + gen_vpx(); + + // there is problem with cfg(target_os) in build.rs, so use our workaround + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os == "android" || target_os == "ios" { + // nothing + } else if cfg!(windows) { + // The first choice is Windows because DXGI is amazing. + println!("cargo:rustc-cfg=dxgi"); + } else if cfg!(target_os = "macos") { + // Quartz is second because macOS is the (annoying) exception. + println!("cargo:rustc-cfg=quartz"); + } else if cfg!(unix) { + // On UNIX we pray that X11 (with XCB) is available. + println!("cargo:rustc-cfg=x11"); + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/examples/ffplay.rs b/rust-rdp/rust-desk/libs/scrap/examples/ffplay.rs new file mode 100644 index 0000000..a4ca1b3 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/examples/ffplay.rs @@ -0,0 +1,51 @@ +extern crate scrap; + +fn main() { + use scrap::{Capturer, Display}; + use std::io::ErrorKind::WouldBlock; + use std::io::Write; + use std::process::{Command, Stdio}; + + let d = Display::primary().unwrap(); + let (w, h) = (d.width(), d.height()); + + let child = Command::new("ffplay") + .args(&[ + "-f", + "rawvideo", + "-pixel_format", + "bgr0", + "-video_size", + &format!("{}x{}", w, h), + "-framerate", + "60", + "-", + ]) + .stdin(Stdio::piped()) + .spawn() + .expect("This example requires ffplay."); + + let mut capturer = Capturer::new(d, false).unwrap(); + let mut out = child.stdin.unwrap(); + + loop { + match capturer.frame(0) { + Ok(frame) => { + // Write the frame, removing end-of-row padding. + let stride = frame.len() / h; + let rowlen = 4 * w; + for row in frame.chunks(stride) { + let row = &row[..rowlen]; + out.write_all(row).unwrap(); + } + } + Err(ref e) if e.kind() == WouldBlock => { + // Wait for the frame. + } + Err(_) => { + // We're done here. + break; + } + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/examples/list.rs b/rust-rdp/rust-desk/libs/scrap/examples/list.rs new file mode 100644 index 0000000..af5bb45 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/examples/list.rs @@ -0,0 +1,16 @@ +extern crate scrap; + +use scrap::Display; + +fn main() { + let displays = Display::all().unwrap(); + + for (i, display) in displays.iter().enumerate() { + println!( + "Display {} [{}x{}]", + i + 1, + display.width(), + display.height() + ); + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/examples/record-screen.rs b/rust-rdp/rust-desk/libs/scrap/examples/record-screen.rs new file mode 100644 index 0000000..2a56c0d --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/examples/record-screen.rs @@ -0,0 +1,160 @@ +extern crate docopt; +extern crate quest; +extern crate repng; +extern crate scrap; +extern crate serde; +extern crate webm; + +use std::fs::{File, OpenOptions}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use std::{io, thread}; + +use docopt::Docopt; +use webm::mux; +use webm::mux::Track; + +use scrap::codec as vpx_encode; +use scrap::{Capturer, Display, STRIDE_ALIGN}; + +const USAGE: &'static str = " +Simple WebM screen capture. + +Usage: + record-screen [--time=] [--fps=] [--bv=] [--ba=] [--codec CODEC] + record-screen (-h | --help) + +Options: + -h --help Show this screen. + --time= Recording duration in seconds. + --fps= Frames per second [default: 30]. + --bv= Video bitrate in kilobits per second [default: 5000]. + --ba= Audio bitrate in kilobits per second [default: 96]. + --codec CODEC Configure the codec used. [default: vp9] + Valid values: vp8, vp9. +"; + +#[derive(Debug, serde::Deserialize)] +struct Args { + arg_path: PathBuf, + flag_codec: Codec, + flag_time: Option, + flag_fps: u64, + flag_bv: u32, +} + +#[derive(Debug, serde::Deserialize)] +enum Codec { + Vp8, + Vp9, +} + +fn main() -> io::Result<()> { + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + + let duration = args.flag_time.map(Duration::from_secs); + + let d = Display::primary().unwrap(); + let (width, height) = (d.width() as u32, d.height() as u32); + + // Setup the multiplexer. + + let out = match { + OpenOptions::new() + .write(true) + .create_new(true) + .open(&args.arg_path) + } { + Ok(file) => file, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => { + if loop { + quest::ask("Overwrite the existing file? [y/N] "); + if let Some(b) = quest::yesno(false)? { + break b; + } + } { + File::create(&args.arg_path)? + } else { + return Ok(()); + } + } + Err(e) => return Err(e.into()), + }; + + let mut webm = + mux::Segment::new(mux::Writer::new(out)).expect("Could not initialize the multiplexer."); + + let (vpx_codec, mux_codec) = match args.flag_codec { + Codec::Vp8 => (vpx_encode::VideoCodecId::VP8, mux::VideoCodecId::VP8), + Codec::Vp9 => (vpx_encode::VideoCodecId::VP9, mux::VideoCodecId::VP9), + }; + + let mut vt = webm.add_video_track(width, height, None, mux_codec); + + // Setup the encoder. + + let mut vpx = vpx_encode::Encoder::new( + &vpx_encode::Config { + width, + height, + timebase: [1, 1000], + bitrate: args.flag_bv, + codec: vpx_codec, + rc_min_quantizer: 0, + rc_max_quantizer: 0, + speed: 6, + }, + 0, + ) + .unwrap(); + + // Start recording. + + let start = Instant::now(); + let stop = Arc::new(AtomicBool::new(false)); + + thread::spawn({ + let stop = stop.clone(); + move || { + let _ = quest::ask("Recording! Press ⏎ to stop."); + let _ = quest::text(); + stop.store(true, Ordering::Release); + } + }); + + let spf = Duration::from_nanos(1_000_000_000 / args.flag_fps); + + // Capturer object is expensive, avoiding to create it frequently. + let mut c = Capturer::new(d, true).unwrap(); + while !stop.load(Ordering::Acquire) { + let now = Instant::now(); + let time = now - start; + + if Some(true) == duration.map(|d| time > d) { + break; + } + + if let Ok(frame) = c.frame(0) { + let ms = time.as_secs() * 1000 + time.subsec_millis() as u64; + + for frame in vpx.encode(ms as i64, &frame, STRIDE_ALIGN).unwrap() { + vt.add_frame(frame.data, frame.pts as u64 * 1_000_000, frame.key); + } + } + + let dt = now.elapsed(); + if dt < spf { + thread::sleep(spf - dt); + } + } + + // End things. + + let _ = webm.finalize(None); + + Ok(()) +} diff --git a/rust-rdp/rust-desk/libs/scrap/examples/screenshot.rs b/rust-rdp/rust-desk/libs/scrap/examples/screenshot.rs new file mode 100644 index 0000000..e2da3b3 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/examples/screenshot.rs @@ -0,0 +1,126 @@ +extern crate repng; +extern crate scrap; + +use std::fs::File; +use std::io::ErrorKind::WouldBlock; +use std::thread; +use std::time::Duration; + +use scrap::{i420_to_rgb, Capturer, Display}; + +fn main() { + let n = Display::all().unwrap().len(); + for i in 0..n { + record(i); + } +} + +fn get_display(i: usize) -> Display { + Display::all().unwrap().remove(i) +} + +fn record(i: usize) { + let one_second = Duration::new(1, 0); + let one_frame = one_second / 60; + + for d in Display::all().unwrap() { + println!("{:?} {} {}", d.origin(), d.width(), d.height()); + } + + let display = get_display(i); + let mut capturer = Capturer::new(display, false).expect("Couldn't begin capture."); + let (w, h) = (capturer.width(), capturer.height()); + + loop { + // Wait until there's a frame. + + let buffer = match capturer.frame(0) { + Ok(buffer) => buffer, + Err(error) => { + if error.kind() == WouldBlock { + // Keep spinning. + thread::sleep(one_frame); + continue; + } else { + panic!("Error: {}", error); + } + } + }; + + println!("Captured! Saving..."); + + // Flip the BGRA image into a RGBA image. + + let mut bitflipped = Vec::with_capacity(w * h * 4); + let stride = buffer.len() / h; + + for y in 0..h { + for x in 0..w { + let i = stride * y + 4 * x; + bitflipped.extend_from_slice(&[buffer[i + 2], buffer[i + 1], buffer[i], 255]); + } + } + + // Save the image. + + let name = format!("screenshot{}_1.png", i); + repng::encode( + File::create(name.clone()).unwrap(), + w as u32, + h as u32, + &bitflipped, + ) + .unwrap(); + + println!("Image saved to `{}`.", name); + break; + } + + drop(capturer); + let display = get_display(i); + let mut capturer = Capturer::new(display, true).expect("Couldn't begin capture."); + let (w, h) = (capturer.width(), capturer.height()); + + loop { + // Wait until there's a frame. + + let buffer = match capturer.frame(0) { + Ok(buffer) => buffer, + Err(error) => { + if error.kind() == WouldBlock { + // Keep spinning. + thread::sleep(one_frame); + continue; + } else { + panic!("Error: {}", error); + } + } + }; + + println!("Captured! Saving..."); + + let mut frame = Default::default(); + i420_to_rgb(w, h, &buffer, &mut frame); + + let mut bitflipped = Vec::with_capacity(w * h * 4); + let stride = frame.len() / h; + + for y in 0..h { + for x in 0..w { + let i = stride * y + 3 * x; + bitflipped.extend_from_slice(&[frame[i], frame[i + 1], frame[i + 2], 255]); + } + } + let name = format!("screenshot{}_2.png", i); + repng::encode( + File::create(name.clone()).unwrap(), + w as u32, + h as u32, + &bitflipped, + ) + .unwrap(); + + println!("Image saved to `{}`.", name); + break; + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/codec.rs b/rust-rdp/rust-desk/libs/scrap/src/common/codec.rs new file mode 100644 index 0000000..60f19e2 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/codec.rs @@ -0,0 +1,536 @@ +// https://github.com/astraw/vpx-encode +// https://github.com/astraw/env-libvpx-sys +// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs + +use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; +use std::os::raw::{c_int, c_uint}; +use std::{ptr, slice}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum VideoCodecId { + VP8, + VP9, +} + +impl Default for VideoCodecId { + fn default() -> VideoCodecId { + VideoCodecId::VP9 + } +} + +pub struct Encoder { + ctx: vpx_codec_ctx_t, + width: usize, + height: usize, +} + +pub struct Decoder { + ctx: vpx_codec_ctx_t, +} + +#[derive(Debug)] +pub enum Error { + FailedCall(String), + BadPtr(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +macro_rules! call_vpx { + ($x:expr) => {{ + let result = unsafe { $x }; // original expression + let result_int = unsafe { std::mem::transmute::<_, i32>(result) }; + if result_int != 0 { + return Err(Error::FailedCall(format!( + "errcode={} {}:{}:{}:{}", + result_int, + module_path!(), + file!(), + line!(), + column!() + )) + .into()); + } + result + }}; +} + +macro_rules! call_vpx_ptr { + ($x:expr) => {{ + let result = unsafe { $x }; // original expression + let result_int = unsafe { std::mem::transmute::<_, isize>(result) }; + if result_int == 0 { + return Err(Error::BadPtr(format!( + "errcode={} {}:{}:{}:{}", + result_int, + module_path!(), + file!(), + line!(), + column!() + )) + .into()); + } + result + }}; +} + +impl Encoder { + pub fn new(config: &Config, num_threads: u32) -> Result { + let i; + if cfg!(feature = "VP8") { + i = match config.codec { + VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), + VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()), + }; + } else { + i = call_vpx_ptr!(vpx_codec_vp9_cx()); + } + let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0)); + + // https://www.webmproject.org/docs/encoder-parameters/ + // default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63 + // try rc_resize_allowed later + + c.g_w = config.width; + c.g_h = config.height; + c.g_timebase.num = config.timebase[0]; + c.g_timebase.den = config.timebase[1]; + c.rc_target_bitrate = config.bitrate; + c.rc_undershoot_pct = 95; + c.rc_dropframe_thresh = 25; + if config.rc_min_quantizer > 0 { + c.rc_min_quantizer = config.rc_min_quantizer; + } + if config.rc_max_quantizer > 0 { + c.rc_max_quantizer = config.rc_max_quantizer; + } + let mut speed = config.speed; + if speed <= 0 { + speed = 6; + } + + c.g_threads = if num_threads == 0 { + num_cpus::get() as _ + } else { + num_threads + }; + c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; + // https://developers.google.com/media/vp9/bitrate-modes/ + // Constant Bitrate mode (CBR) is recommended for live streaming with VP9. + c.rc_end_usage = vpx_rc_mode::VPX_CBR; + // c.kf_min_dist = 0; + // c.kf_max_dist = 999999; + c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot + + /* + VPX encoder支持two-pass encode,这是为了rate control的。 + 对于两遍编码,就是需要整个编码过程做两次,第一次会得到一些新的控制参数来进行第二遍的编码, + 这样可以在相同的bitrate下得到最好的PSNR + */ + + let mut ctx = Default::default(); + call_vpx!(vpx_codec_enc_init_ver( + &mut ctx, + i, + &c, + 0, + VPX_ENCODER_ABI_VERSION as _ + )); + + if config.codec == VideoCodecId::VP9 { + // set encoder internal speed settings + // in ffmpeg, it is --speed option + /* + set to 0 or a positive value 1-16, the codec will try to adapt its + complexity depending on the time it spends encoding. Increasing this + number will make the speed go up and the quality go down. + Negative values mean strict enforcement of this + while positive values are adaptive + */ + /* https://developers.google.com/media/vp9/live-encoding + Speed 5 to 8 should be used for live / real-time encoding. + Lower numbers (5 or 6) are higher quality but require more CPU power. + Higher numbers (7 or 8) will be lower quality but more manageable for lower latency + use cases and also for lower CPU power devices such as mobile. + */ + call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, speed,)); + // set row level multi-threading + /* + as some people in comments and below have already commented, + more recent versions of libvpx support -row-mt 1 to enable tile row + multi-threading. This can increase the number of tiles by up to 4x in VP9 + (since the max number of tile rows is 4, regardless of video height). + To enable this, use -tile-rows N where N is the number of tile rows in + log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile + rows). The total number of active threads will then be equal to + $tile_rows * $tile_columns + */ + call_vpx!(vpx_codec_control_( + &mut ctx, + VP9E_SET_ROW_MT as _, + 1 as c_int + )); + + call_vpx!(vpx_codec_control_( + &mut ctx, + VP9E_SET_TILE_COLUMNS as _, + 4 as c_int + )); + } + + Ok(Self { + ctx, + width: config.width as _, + height: config.height as _, + }) + } + + pub fn encode(&mut self, pts: i64, data: &[u8], stride_align: usize) -> Result { + assert!(2 * data.len() >= 3 * self.width * self.height); + + let mut image = Default::default(); + call_vpx_ptr!(vpx_img_wrap( + &mut image, + vpx_img_fmt::VPX_IMG_FMT_I420, + self.width as _, + self.height as _, + stride_align as _, + data.as_ptr() as _, + )); + + call_vpx!(vpx_codec_encode( + &mut self.ctx, + &image, + pts as _, + 1, // Duration + 0, // Flags + VPX_DL_REALTIME as _, + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the encoder to return any pending packets + pub fn flush(&mut self) -> Result { + call_vpx!(vpx_codec_encode( + &mut self.ctx, + ptr::null(), + -1, // PTS + 1, // Duration + 0, // Flags + VPX_DL_REALTIME as _, + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } +} + +impl Drop for Encoder { + fn drop(&mut self) { + unsafe { + let result = vpx_codec_destroy(&mut self.ctx); + if result != VPX_CODEC_OK { + panic!("failed to destroy vpx codec"); + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct EncodeFrame<'a> { + /// Compressed data. + pub data: &'a [u8], + /// Whether the frame is a keyframe. + pub key: bool, + /// Presentation timestamp (in timebase units). + pub pts: i64, +} + +#[derive(Clone, Copy, Debug)] +pub struct Config { + /// The width (in pixels). + pub width: c_uint, + /// The height (in pixels). + pub height: c_uint, + /// The timebase numerator and denominator (in seconds). + pub timebase: [c_int; 2], + /// The target bitrate (in kilobits per second). + pub bitrate: c_uint, + /// The codec + pub codec: VideoCodecId, + pub rc_min_quantizer: u32, + pub rc_max_quantizer: u32, + pub speed: i32, +} + +pub struct EncodeFrames<'a> { + ctx: &'a mut vpx_codec_ctx_t, + iter: vpx_codec_iter_t, +} + +impl<'a> Iterator for EncodeFrames<'a> { + type Item = EncodeFrame<'a>; + fn next(&mut self) -> Option { + loop { + unsafe { + let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter); + if pkt.is_null() { + return None; + } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT { + let f = &(*pkt).data.frame; + return Some(Self::Item { + data: slice::from_raw_parts(f.buf as _, f.sz as _), + key: (f.flags & VPX_FRAME_IS_KEY) != 0, + pts: f.pts, + }); + } else { + // Ignore the packet. + } + } + } + } +} + +impl Decoder { + /// Create a new decoder + /// + /// # Errors + /// + /// The function may fail if the underlying libvpx does not provide + /// the VP9 decoder. + pub fn new(codec: VideoCodecId, num_threads: u32) -> Result { + // This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can + // cause UB if uninitialized. + let i; + if cfg!(feature = "VP8") { + i = match codec { + VideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), + VideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), + }; + } else { + i = call_vpx_ptr!(vpx_codec_vp9_dx()); + } + let mut ctx = Default::default(); + let cfg = vpx_codec_dec_cfg_t { + threads: if num_threads == 0 { + num_cpus::get() as _ + } else { + num_threads + }, + w: 0, + h: 0, + }; + /* + unsafe { + println!("{}", vpx_codec_get_caps(i)); + } + */ + call_vpx!(vpx_codec_dec_init_ver( + &mut ctx, + i, + &cfg, + 0, + VPX_DECODER_ABI_VERSION as _, + )); + Ok(Self { ctx }) + } + + pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> { + let mut img = Image::new(); + for frame in self.decode(data)? { + drop(img); + img = frame; + } + for frame in self.flush()? { + drop(img); + img = frame; + } + if img.is_null() { + Ok(Vec::new()) + } else { + let mut out = Default::default(); + img.rgb(1, rgba, &mut out); + Ok(out) + } + } + + /// Feed some compressed data to the encoder + /// + /// The `data` slice is sent to the decoder + /// + /// It matches a call to `vpx_codec_decode`. + pub fn decode(&mut self, data: &[u8]) -> Result { + call_vpx!(vpx_codec_decode( + &mut self.ctx, + data.as_ptr(), + data.len() as _, + ptr::null_mut(), + 0, + )); + + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the decoder to return any pending frame + pub fn flush(&mut self) -> Result { + call_vpx!(vpx_codec_decode( + &mut self.ctx, + ptr::null(), + 0, + ptr::null_mut(), + 0 + )); + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } +} + +impl Drop for Decoder { + fn drop(&mut self) { + unsafe { + let result = vpx_codec_destroy(&mut self.ctx); + if result != VPX_CODEC_OK { + panic!("failed to destroy vpx codec"); + } + } + } +} + +pub struct DecodeFrames<'a> { + ctx: &'a mut vpx_codec_ctx_t, + iter: vpx_codec_iter_t, +} + +impl<'a> Iterator for DecodeFrames<'a> { + type Item = Image; + fn next(&mut self) -> Option { + let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) }; + if img.is_null() { + return None; + } else { + return Some(Image(img)); + } + } +} + +// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c +pub struct Image(*mut vpx_image_t); +impl Image { + #[inline] + pub fn new() -> Self { + Self(std::ptr::null_mut()) + } + + #[inline] + pub fn is_null(&self) -> bool { + self.0.is_null() + } + + #[inline] + pub fn width(&self) -> usize { + self.inner().d_w as _ + } + + #[inline] + pub fn height(&self) -> usize { + self.inner().d_h as _ + } + + #[inline] + pub fn format(&self) -> vpx_img_fmt_t { + // VPX_IMG_FMT_I420 + self.inner().fmt + } + + #[inline] + pub fn inner(&self) -> &vpx_image_t { + unsafe { &*self.0 } + } + + #[inline] + pub fn stride(&self, iplane: usize) -> i32 { + self.inner().stride[iplane] + } + + pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) { + let h = self.height(); + let mut w = self.width(); + let bps = if rgba { 4 } else { 3 }; + w = (w + stride_align - 1) & !(stride_align - 1); + dst.resize(h * w * bps, 0); + let img = self.inner(); + unsafe { + if rgba { + super::I420ToARGB( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } else { + super::I420ToRAW( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } + } + } + + #[inline] + pub fn data(&self) -> (&[u8], &[u8], &[u8]) { + unsafe { + let img = self.inner(); + let h = (img.d_h as usize + 1) & !1; + let n = img.stride[0] as usize * h; + let y = slice::from_raw_parts(img.planes[0], n); + let n = img.stride[1] as usize * (h >> 1); + let u = slice::from_raw_parts(img.planes[1], n); + let v = slice::from_raw_parts(img.planes[2], n); + (y, u, v) + } + } +} + +impl Drop for Image { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { vpx_img_free(self.0) }; + } + } +} + +unsafe impl Send for vpx_codec_ctx_t {} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/convert.rs b/rust-rdp/rust-desk/libs/scrap/src/common/convert.rs new file mode 100644 index 0000000..fdc90a8 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/convert.rs @@ -0,0 +1,188 @@ +use super::vpx::*; +use std::os::raw::c_int; + +extern "C" { + // seems libyuv uses reverse byte order compared with our view + + pub fn ARGBRotate( + src_argb: *const u8, + src_stride_argb: c_int, + dst_argb: *mut u8, + dst_stride_argb: c_int, + src_width: c_int, + src_height: c_int, + mode: c_int, + ) -> c_int; + + pub fn ARGBMirror( + src_argb: *const u8, + src_stride_argb: c_int, + dst_argb: *mut u8, + dst_stride_argb: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + pub fn ARGBToI420( + src_bgra: *const u8, + src_stride_bgra: c_int, + dst_y: *mut u8, + dst_stride_y: c_int, + dst_u: *mut u8, + dst_stride_u: c_int, + dst_v: *mut u8, + dst_stride_v: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + pub fn NV12ToI420( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + dst_y: *mut u8, + dst_stride_y: c_int, + dst_u: *mut u8, + dst_stride_u: c_int, + dst_v: *mut u8, + dst_stride_v: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + // I420ToRGB24: RGB little endian (bgr in memory) + // I420ToRaw: RGB big endian (rgb in memory) to RGBA. + pub fn I420ToRAW( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_raw: c_int, + width: c_int, + height: c_int, + ) -> c_int; + + pub fn I420ToARGB( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; +} + +// https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c +#[inline] +fn get_vpx_i420_stride( + width: usize, + height: usize, + stride_align: usize, +) -> (usize, usize, usize, usize, usize, usize) { + let mut img = Default::default(); + unsafe { + vpx_img_wrap( + &mut img, + vpx_img_fmt::VPX_IMG_FMT_I420, + width as _, + height as _, + stride_align as _, + 0x1 as _, + ); + } + ( + img.w as _, + img.h as _, + img.stride[0] as _, + img.stride[1] as _, + img.planes[1] as usize - img.planes[0] as usize, + img.planes[2] as usize - img.planes[0] as usize, + ) +} + +pub fn i420_to_rgb(width: usize, height: usize, src: &[u8], dst: &mut Vec) { + let (_, _, src_stride_y, src_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let src_y = src.as_ptr(); + let src_u = src[u..].as_ptr(); + let src_v = src[v..].as_ptr(); + dst.resize(width * height * 3, 0); + unsafe { + super::I420ToRAW( + src_y, + src_stride_y as _, + src_u, + src_stride_uv as _, + src_v, + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 3) as _, + width as _, + height as _, + ); + }; +} + +pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec) { + let (_, h, dst_stride_y, dst_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let bps = 12; + dst.resize(h * dst_stride_y * bps / 8, 0); + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[u..].as_mut_ptr(); + let dst_v = dst[v..].as_mut_ptr(); + unsafe { + ARGBToI420( + src.as_ptr(), + (src.len() / height) as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ); + } +} + +pub unsafe fn nv12_to_i420( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + width: usize, + height: usize, + dst: &mut Vec, +) { + let (w, h, dst_stride_y, dst_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let bps = 12; + dst.resize(h * w * bps / 8, 0); + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[u..].as_mut_ptr(); + let dst_v = dst[v..].as_mut_ptr(); + NV12ToI420( + src_y, + src_stride_y, + src_uv, + src_stride_uv, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ); +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/dxgi.rs b/rust-rdp/rust-desk/libs/scrap/src/common/dxgi.rs new file mode 100644 index 0000000..4683ad5 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/dxgi.rs @@ -0,0 +1,113 @@ +use crate::dxgi; +use std::io::ErrorKind::{NotFound, TimedOut, WouldBlock}; +use std::{io, ops}; + +pub struct Capturer { + inner: dxgi::Capturer, + width: usize, + height: usize, +} + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + let width = display.width(); + let height = display.height(); + let inner = dxgi::Capturer::new(display.0, yuv)?; + Ok(Capturer { + inner, + width, + height, + }) + } + + pub fn is_gdi(&self) -> bool { + self.inner.is_gdi() + } + + pub fn set_gdi(&mut self) -> bool { + self.inner.set_gdi() + } + + pub fn cancel_gdi(&mut self) { + self.inner.cancel_gdi() + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } + + pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> { + match self.inner.frame(timeout_ms) { + Ok(frame) => Ok(Frame(frame)), + Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()), + Err(error) => Err(error), + } + } +} + +pub struct Frame<'a>(&'a [u8]); + +impl<'a> ops::Deref for Frame<'a> { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.0 + } +} + +pub struct Display(dxgi::Display); + +impl Display { + pub fn primary() -> io::Result { + // not implemented yet + Err(NotFound.into()) + } + + pub fn all() -> io::Result> { + let tmp = Self::all_().unwrap_or(Default::default()); + if tmp.is_empty() { + println!("Display got from gdi"); + return Ok(dxgi::Displays::get_from_gdi() + .drain(..) + .map(Display) + .collect::>()); + } + Ok(tmp) + } + + fn all_() -> io::Result> { + Ok(dxgi::Displays::new()?.map(Display).collect::>()) + } + + pub fn width(&self) -> usize { + self.0.width() as usize + } + + pub fn height(&self) -> usize { + self.0.height() as usize + } + + pub fn name(&self) -> String { + use std::ffi::OsString; + use std::os::windows::prelude::*; + OsString::from_wide(self.0.name()) + .to_string_lossy() + .to_string() + } + + pub fn is_online(&self) -> bool { + self.0.is_online() + } + + pub fn origin(&self) -> (i32, i32) { + self.0.origin() + } + + pub fn is_primary(&self) -> bool { + // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodea + self.origin() == (0, 0) + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/linux.rs b/rust-rdp/rust-desk/libs/scrap/src/common/linux.rs new file mode 100644 index 0000000..50bab09 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/linux.rs @@ -0,0 +1,117 @@ +use crate::common::{ + wayland, + x11::{self, Frame}, +}; +use std::io; + +pub enum Capturer { + X11(x11::Capturer), + WAYLAND(wayland::Capturer), +} + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + Ok(match display { + Display::X11(d) => Capturer::X11(x11::Capturer::new(d, yuv)?), + Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d, yuv)?), + }) + } + + pub fn width(&self) -> usize { + match self { + Capturer::X11(d) => d.width(), + Capturer::WAYLAND(d) => d.width(), + } + } + + pub fn height(&self) -> usize { + match self { + Capturer::X11(d) => d.height(), + Capturer::WAYLAND(d) => d.height(), + } + } + + pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> { + match self { + Capturer::X11(d) => d.frame(timeout_ms), + Capturer::WAYLAND(d) => d.frame(timeout_ms), + } + } +} + +pub enum Display { + X11(x11::Display), + WAYLAND(wayland::Display), +} + +#[inline] +fn is_wayland() -> bool { + std::env::var("IS_WAYLAND").is_ok() + || std::env::var("XDG_SESSION_TYPE") == Ok("wayland".to_owned()) +} + +impl Display { + pub fn primary() -> io::Result { + Ok(if is_wayland() { + Display::WAYLAND(wayland::Display::primary()?) + } else { + Display::X11(x11::Display::primary()?) + }) + } + + pub fn all() -> io::Result> { + Ok(if is_wayland() { + wayland::Display::all()? + .drain(..) + .map(|x| Display::WAYLAND(x)) + .collect() + } else { + x11::Display::all()? + .drain(..) + .map(|x| Display::X11(x)) + .collect() + }) + } + + pub fn width(&self) -> usize { + match self { + Display::X11(d) => d.width(), + Display::WAYLAND(d) => d.width(), + } + } + + pub fn height(&self) -> usize { + match self { + Display::X11(d) => d.height(), + Display::WAYLAND(d) => d.height(), + } + } + + pub fn origin(&self) -> (i32, i32) { + match self { + Display::X11(d) => d.origin(), + Display::WAYLAND(d) => d.origin(), + } + } + + pub fn is_online(&self) -> bool { + match self { + Display::X11(d) => d.is_online(), + Display::WAYLAND(d) => d.is_online(), + } + } + + pub fn is_primary(&self) -> bool { + match self { + Display::X11(d) => d.is_primary(), + Display::WAYLAND(d) => d.is_primary(), + } + } + + pub fn name(&self) -> String { + match self { + Display::X11(d) => d.name(), + Display::WAYLAND(d) => d.name(), + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/mod.rs b/rust-rdp/rust-desk/libs/scrap/src/common/mod.rs new file mode 100644 index 0000000..ef709c0 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/mod.rs @@ -0,0 +1,32 @@ +pub use self::codec::*; + +cfg_if! { + if #[cfg(quartz)] { + mod quartz; + pub use self::quartz::*; + } else if #[cfg(x11)] { + cfg_if! { + if #[cfg(feature="wayland")] { + mod linux; + mod wayland; + mod x11; + pub use self::linux::*; + } else { + mod x11; + pub use self::x11::*; + } + } + } else if #[cfg(dxgi)] { + mod dxgi; + pub use self::dxgi::*; + } else { + //TODO: Fallback implementation. + } +} + +pub mod codec; +mod convert; +pub use self::convert::*; +pub const STRIDE_ALIGN: usize = 16; // commonly used in libvpx vpx_img_alloc caller + +mod vpx; diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/quartz.rs b/rust-rdp/rust-desk/libs/scrap/src/common/quartz.rs new file mode 100644 index 0000000..f0095a8 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/quartz.rs @@ -0,0 +1,125 @@ +use crate::quartz; +use std::marker::PhantomData; +use std::sync::{Arc, Mutex, TryLockError}; +use std::{io, mem, ops}; + +pub struct Capturer { + inner: quartz::Capturer, + frame: Arc>>, + use_yuv: bool, + i420: Vec, +} + +impl Capturer { + pub fn new(display: Display, use_yuv: bool) -> io::Result { + let frame = Arc::new(Mutex::new(None)); + + let f = frame.clone(); + let inner = quartz::Capturer::new( + display.0, + display.width(), + display.height(), + if use_yuv { + quartz::PixelFormat::YCbCr420Full + } else { + quartz::PixelFormat::Argb8888 + }, + Default::default(), + move |inner| { + if let Ok(mut f) = f.lock() { + *f = Some(inner); + } + }, + ) + .map_err(|_| io::Error::from(io::ErrorKind::Other))?; + + Ok(Capturer { + inner, + frame, + use_yuv, + i420: Vec::new(), + }) + } + + pub fn width(&self) -> usize { + self.inner.width() + } + + pub fn height(&self) -> usize { + self.inner.height() + } + + pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> { + match self.frame.try_lock() { + Ok(mut handle) => { + let mut frame = None; + mem::swap(&mut frame, &mut handle); + + match frame { + Some(mut frame) => { + if self.use_yuv { + frame.nv12_to_i420(self.width(), self.height(), &mut self.i420); + } + Ok(Frame(frame, PhantomData)) + } + + None => Err(io::ErrorKind::WouldBlock.into()), + } + } + + Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()), + + Err(TryLockError::Poisoned(..)) => Err(io::ErrorKind::Other.into()), + } + } +} + +pub struct Frame<'a>(quartz::Frame, PhantomData<&'a [u8]>); + +impl<'a> ops::Deref for Frame<'a> { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &*self.0 + } +} + +pub struct Display(quartz::Display); + +impl Display { + pub fn primary() -> io::Result { + Ok(Display(quartz::Display::primary())) + } + + pub fn all() -> io::Result> { + Ok(quartz::Display::online() + .map_err(|_| io::Error::from(io::ErrorKind::Other))? + .into_iter() + .map(Display) + .collect()) + } + + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } + + pub fn name(&self) -> String { + self.0.id().to_string() + } + + pub fn is_online(&self) -> bool { + self.0.is_online() + } + + pub fn origin(&self) -> (i32, i32) { + let o = self.0.bounds().origin; + (o.x as _, o.y as _) + } + + pub fn is_primary(&self) -> bool { + self.0.is_primary() + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/vpx.rs b/rust-rdp/rust-desk/libs/scrap/src/common/vpx.rs new file mode 100644 index 0000000..eb65531 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/vpx.rs @@ -0,0 +1,25 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(improper_ctypes)] +#![allow(dead_code)] + +impl Default for vpx_codec_enc_cfg { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for vpx_codec_ctx { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for vpx_image_t { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs")); diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/wayland.rs b/rust-rdp/rust-desk/libs/scrap/src/common/wayland.rs new file mode 100644 index 0000000..ff6bf80 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/wayland.rs @@ -0,0 +1,81 @@ +use crate::common::x11::Frame; +use crate::wayland::{capturable::*, *}; +use std::io; + +pub struct Capturer(Display, Box, bool, Vec); + +fn map_err(err: E) -> io::Error { + io::Error::new(io::ErrorKind::Other, err.to_string()) +} + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + let r = display.0.recorder(false).map_err(map_err)?; + Ok(Capturer(display, r, yuv, Default::default())) + } + + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } + + pub fn frame<'a>(&'a mut self, timeout_ms: u32) -> io::Result> { + match self.1.capture(timeout_ms as _).map_err(map_err)? { + PixelProvider::BGR0(w, h, x) => Ok(Frame(if self.2 { + crate::common::bgra_to_i420(w as _, h as _, &x, &mut self.3); + &self.3[..] + } else { + x + })), + PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), + _ => Err(map_err("Invalid data")), + } + } +} + +pub struct Display(pipewire::PipeWireCapturable); + +impl Display { + pub fn primary() -> io::Result { + let mut all = Display::all()?; + if all.is_empty() { + return Err(io::ErrorKind::NotFound.into()); + } + Ok(all.remove(0)) + } + + pub fn all() -> io::Result> { + Ok(pipewire::get_capturables(false) + .map_err(map_err)? + .drain(..) + .map(|x| Display(x)) + .collect()) + } + + pub fn width(&self) -> usize { + self.0.size.0 + } + + pub fn height(&self) -> usize { + self.0.size.1 + } + + pub fn origin(&self) -> (i32, i32) { + self.0.position + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + false + } + + pub fn name(&self) -> String { + "".to_owned() + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/common/x11.rs b/rust-rdp/rust-desk/libs/scrap/src/common/x11.rs new file mode 100644 index 0000000..e9640cb --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/common/x11.rs @@ -0,0 +1,87 @@ +use crate::x11; +use std::{io, ops}; + +pub struct Capturer(x11::Capturer); + +impl Capturer { + pub fn new(display: Display, yuv: bool) -> io::Result { + x11::Capturer::new(display.0, yuv).map(Capturer) + } + + pub fn width(&self) -> usize { + self.0.display().rect().w as usize + } + + pub fn height(&self) -> usize { + self.0.display().rect().h as usize + } + + pub fn frame<'a>(&'a mut self, _timeout_ms: u32) -> io::Result> { + Ok(Frame(self.0.frame())) + } +} + +pub struct Frame<'a>(pub(crate) &'a [u8]); + +impl<'a> ops::Deref for Frame<'a> { + type Target = [u8]; + fn deref(&self) -> &[u8] { + self.0 + } +} + +pub struct Display(x11::Display); + +impl Display { + pub fn primary() -> io::Result { + let server = match x11::Server::default() { + Ok(server) => server, + Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), + }; + + let mut displays = x11::Server::displays(server); + let mut best = displays.next(); + if best.as_ref().map(|x| x.is_default()) == Some(false) { + best = displays.find(|x| x.is_default()).or(best); + } + + match best { + Some(best) => Ok(Display(best)), + None => Err(io::ErrorKind::NotFound.into()), + } + } + + pub fn all() -> io::Result> { + let server = match x11::Server::default() { + Ok(server) => server, + Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), + }; + + Ok(x11::Server::displays(server).map(Display).collect()) + } + + pub fn width(&self) -> usize { + self.0.rect().w as usize + } + + pub fn height(&self) -> usize { + self.0.rect().h as usize + } + + pub fn origin(&self) -> (i32, i32) { + let r = self.0.rect(); + (r.x as _, r.y as _) + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + self.0.is_default() + } + + pub fn name(&self) -> String { + "".to_owned() + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/dxgi/gdi.rs b/rust-rdp/rust-desk/libs/scrap/src/dxgi/gdi.rs new file mode 100644 index 0000000..9fb0611 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/dxgi/gdi.rs @@ -0,0 +1,213 @@ +use std::mem::size_of; +use winapi::{ + shared::windef::{HBITMAP, HDC}, + um::wingdi::{ + BitBlt, + CreateCompatibleBitmap, + CreateCompatibleDC, + CreateDCW, + DeleteDC, + DeleteObject, + GetDIBits, + SelectObject, + BITMAPINFO, + BITMAPINFOHEADER, + BI_RGB, + CAPTUREBLT, + DIB_RGB_COLORS, //CAPTUREBLT, + HGDI_ERROR, + RGBQUAD, + SRCCOPY, + }, +}; + +const PIXEL_WIDTH: i32 = 4; + +pub struct CapturerGDI { + screen_dc: HDC, + dc: HDC, + bmp: HBITMAP, + width: i32, + height: i32, +} + +impl CapturerGDI { + pub fn new(name: &[u16], width: i32, height: i32) -> Result> { + /* or Enumerate monitors with EnumDisplayMonitors, + https://stackoverflow.com/questions/34987695/how-can-i-get-an-hmonitor-handle-from-a-display-device-name + #[no_mangle] + pub extern "C" fn callback(m: HMONITOR, dc: HDC, rect: LPRECT, lp: LPARAM) -> BOOL {} + */ + /* + shared::windef::HMONITOR, + winuser::{GetMonitorInfoW, GetSystemMetrics, MONITORINFOEXW}, + let mut mi: MONITORINFOEXW = std::mem::MaybeUninit::uninit().assume_init(); + mi.cbSize = size_of::() as _; + if GetMonitorInfoW(m, &mut mi as *mut MONITORINFOEXW as _) == 0 { + return Err(format!("Failed to get monitor information of: {:?}", m).into()); + } + */ + unsafe { + if name.is_empty() { + return Err("Empty display name".into()); + } + let screen_dc = CreateDCW(&name[0], 0 as _, 0 as _, 0 as _); + if screen_dc.is_null() { + return Err("Failed to create dc from monitor name".into()); + } + + // Create a Windows Bitmap, and copy the bits into it + let dc = CreateCompatibleDC(screen_dc); + if dc.is_null() { + DeleteDC(screen_dc); + return Err("Can't get a Windows display".into()); + } + + let bmp = CreateCompatibleBitmap(screen_dc, width, height); + if bmp.is_null() { + DeleteDC(screen_dc); + DeleteDC(dc); + return Err("Can't create a Windows buffer".into()); + } + + let res = SelectObject(dc, bmp as _); + if res.is_null() || res == HGDI_ERROR { + DeleteDC(screen_dc); + DeleteDC(dc); + DeleteObject(bmp as _); + return Err("Can't select Windows buffer".into()); + } + Ok(Self { + screen_dc, + dc, + bmp, + width, + height, + }) + } + } + + pub fn frame(&self, data: &mut Vec) -> Result<(), Box> { + unsafe { + let res = BitBlt( + self.dc, + 0, + 0, + self.width, + self.height, + self.screen_dc, + 0, + 0, + SRCCOPY | CAPTUREBLT, // CAPTUREBLT enable layered window but also make cursor blinking + ); + if res == 0 { + return Err("Failed to copy screen to Windows buffer".into()); + } + + let stride = self.width * PIXEL_WIDTH; + let size: usize = (stride * self.height) as usize; + let mut data1: Vec = Vec::with_capacity(size); + data1.set_len(size); + data.resize(size, 0); + + let mut bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: size_of::() as _, + biWidth: self.width as _, + biHeight: self.height as _, + biPlanes: 1, + biBitCount: (8 * PIXEL_WIDTH) as _, + biCompression: BI_RGB, + biSizeImage: (self.width * self.height * PIXEL_WIDTH) as _, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [RGBQUAD { + rgbBlue: 0, + rgbGreen: 0, + rgbRed: 0, + rgbReserved: 0, + }], + }; + + // copy bits into Vec + let res = GetDIBits( + self.dc, + self.bmp, + 0, + self.height as _, + &mut data[0] as *mut u8 as _, + &mut bmi as _, + DIB_RGB_COLORS, + ); + if res == 0 { + return Err("GetDIBits failed".into()); + } + crate::common::ARGBMirror( + data.as_ptr(), + stride, + data1.as_mut_ptr(), + stride, + self.width, + self.height, + ); + crate::common::ARGBRotate( + data1.as_ptr(), + stride, + data.as_mut_ptr(), + stride, + self.width, + self.height, + 180, + ); + Ok(()) + } + } +} + +impl Drop for CapturerGDI { + fn drop(&mut self) { + unsafe { + DeleteDC(self.screen_dc); + DeleteDC(self.dc); + DeleteObject(self.bmp as _); + } + } +} + +#[cfg(test)] +mod tests { + use super::super::*; + use super::*; + #[test] + fn test() { + match Displays::new().unwrap().next() { + Some(d) => { + let w = d.width(); + let h = d.height(); + let c = CapturerGDI::new(d.name(), w, h).unwrap(); + let mut data = Vec::new(); + c.frame(&mut data).unwrap(); + let mut bitflipped = Vec::with_capacity((w * h * 4) as usize); + for y in 0..h { + for x in 0..w { + let i = (w * 4 * y + 4 * x) as usize; + bitflipped.extend_from_slice(&[data[i + 2], data[i + 1], data[i], 255]); + } + } + repng::encode( + std::fs::File::create("gdi_screen.png").unwrap(), + d.width() as u32, + d.height() as u32, + &bitflipped, + ) + .unwrap(); + } + _ => { + assert!(false); + } + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/dxgi/mod.rs b/rust-rdp/rust-desk/libs/scrap/src/dxgi/mod.rs new file mode 100644 index 0000000..677811b --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/dxgi/mod.rs @@ -0,0 +1,568 @@ +use std::{io, mem, ptr, slice}; +pub mod gdi; +pub use gdi::CapturerGDI; + +use winapi::{ + shared::{ + dxgi::*, + dxgi1_2::*, + dxgitype::*, + minwindef::{DWORD, FALSE, TRUE, UINT}, + ntdef::LONG, + windef::HMONITOR, + winerror::*, + // dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE}, + }, + um::{ + d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*, + winnt::HRESULT, winuser::*, + }, +}; + +pub struct ComPtr(*mut T); +impl ComPtr { + fn is_null(&self) -> bool { + self.0.is_null() + } +} +impl Drop for ComPtr { + fn drop(&mut self) { + unsafe { + if !self.is_null() { + (*(self.0 as *mut IUnknown)).Release(); + } + } + } +} + +pub struct Capturer { + device: ComPtr, + display: Display, + context: ComPtr, + duplication: ComPtr, + fastlane: bool, + surface: ComPtr, + width: usize, + height: usize, + use_yuv: bool, + yuv: Vec, + rotated: Vec, + gdi_capturer: Option, + gdi_buffer: Vec, +} + +impl Capturer { + pub fn new(display: Display, use_yuv: bool) -> io::Result { + let mut device = ptr::null_mut(); + let mut context = ptr::null_mut(); + let mut duplication = ptr::null_mut(); + let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() }; + let mut gdi_capturer = None; + + let mut res = if display.gdi { + wrap_hresult(1) + } else { + wrap_hresult(unsafe { + D3D11CreateDevice( + display.adapter.0 as *mut _, + D3D_DRIVER_TYPE_UNKNOWN, + ptr::null_mut(), // No software rasterizer. + 0, // No device flags. + ptr::null_mut(), // Feature levels. + 0, // Feature levels' length. + D3D11_SDK_VERSION, + &mut device, + ptr::null_mut(), + &mut context, + ) + }) + }; + let device = ComPtr(device); + let context = ComPtr(context); + + if res.is_err() { + gdi_capturer = display.create_gdi(); + println!("Fallback to GDI"); + if gdi_capturer.is_some() { + res = Ok(()); + } + } else { + res = wrap_hresult(unsafe { + let hres = (*display.inner.0).DuplicateOutput(device.0 as *mut _, &mut duplication); + if hres != S_OK { + gdi_capturer = display.create_gdi(); + println!("Fallback to GDI"); + if gdi_capturer.is_some() { + S_OK + } else { + hres + } + } else { + hres + } + // NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk + + // also try high version DXGI for better performance, e.g. + // https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/dxgi-1-2-improvements + // dxgi-1-6 may too high, only support win10 (2018) + // https://docs.microsoft.com/zh-cn/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + // DXGI_FORMAT_420_OPAQUE + // IDXGIOutputDuplication::GetFrameDirtyRects and IDXGIOutputDuplication::GetFrameMoveRects + // can help us update screen incrementally + + /* // not supported on my PC, try in the future + let format : Vec = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE]; + (*display.inner).DuplicateOutput1( + device as *mut _, + 0 as UINT, + 2 as UINT, + format.as_ptr(), + &mut duplication + ) + */ + + // if above not work, I think below should not work either, try later + // https://developer.nvidia.com/capture-sdk deprecated + // examples using directx + nvideo sdk for GPU-accelerated video encoding/decoding + // https://github.com/NVIDIA/video-sdk-samples + }); + } + + res?; + + if !duplication.is_null() { + unsafe { + (*duplication).GetDesc(&mut desc); + } + } + + Ok(Capturer { + device, + context, + duplication: ComPtr(duplication), + fastlane: desc.DesktopImageInSystemMemory == TRUE, + surface: ComPtr(ptr::null_mut()), + width: display.width() as usize, + height: display.height() as usize, + display, + use_yuv, + yuv: Vec::new(), + rotated: Vec::new(), + gdi_capturer, + gdi_buffer: Vec::new(), + }) + } + + pub fn is_gdi(&self) -> bool { + self.gdi_capturer.is_some() + } + + pub fn set_gdi(&mut self) -> bool { + self.gdi_capturer = self.display.create_gdi(); + self.is_gdi() + } + + pub fn cancel_gdi(&mut self) { + self.gdi_buffer = Vec::new(); + self.gdi_capturer.take(); + } + + unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> { + let mut frame = ptr::null_mut(); + let mut info = mem::MaybeUninit::uninit().assume_init(); + + wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; + let frame = ComPtr(frame); + + if *info.LastPresentTime.QuadPart() == 0 { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + + let mut rect = mem::MaybeUninit::uninit().assume_init(); + if self.fastlane { + wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?; + } else { + self.surface = ComPtr(self.ohgodwhat(frame.0)?); + wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?; + } + Ok((rect.pBits, rect.Pitch)) + } + + // copy from GPU memory to system memory + unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<*mut IDXGISurface> { + let mut texture: *mut ID3D11Texture2D = ptr::null_mut(); + (*frame).QueryInterface( + &IID_ID3D11Texture2D, + &mut texture as *mut *mut _ as *mut *mut _, + ); + let texture = ComPtr(texture); + + let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); + (*texture.0).GetDesc(&mut texture_desc); + + texture_desc.Usage = D3D11_USAGE_STAGING; + texture_desc.BindFlags = 0; + texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + texture_desc.MiscFlags = 0; + + let mut readable = ptr::null_mut(); + wrap_hresult((*self.device.0).CreateTexture2D( + &mut texture_desc, + ptr::null(), + &mut readable, + ))?; + (*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM); + let readable = ComPtr(readable); + + let mut surface = ptr::null_mut(); + (*readable.0).QueryInterface( + &IID_IDXGISurface, + &mut surface as *mut *mut _ as *mut *mut _, + ); + + (*self.context.0).CopyResource(readable.0 as *mut _, texture.0 as *mut _); + + Ok(surface) + } + + pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result<&'a [u8]> { + unsafe { + // Release last frame. + // No error checking needed because we don't care. + // None of the errors crash anyway. + let result = { + if let Some(gdi_capturer) = &self.gdi_capturer { + match gdi_capturer.frame(&mut self.gdi_buffer) { + Ok(_) => &self.gdi_buffer, + Err(err) => { + return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); + } + } + } else { + self.unmap(); + let r = self.load_frame(timeout)?; + let rotate = match self.display.rotation() { + DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => 0, + DXGI_MODE_ROTATION_ROTATE90 => 90, + DXGI_MODE_ROTATION_ROTATE180 => 180, + DXGI_MODE_ROTATION_ROTATE270 => 270, + _ => { + return Err(io::Error::new( + io::ErrorKind::Other, + "Unknown roration".to_string(), + )); + } + }; + if rotate == 0 { + slice::from_raw_parts(r.0, r.1 as usize * self.height) + } else { + self.rotated.resize(self.width * self.height * 4, 0); + crate::common::ARGBRotate( + r.0, + r.1, + self.rotated.as_mut_ptr(), + 4 * self.width as i32, + if rotate == 180 { + self.width + } else { + self.height + } as _, + if rotate != 180 { + self.width + } else { + self.height + } as _, + rotate, + ); + &self.rotated[..] + } + } + }; + Ok({ + if self.use_yuv { + crate::common::bgra_to_i420( + self.width as usize, + self.height as usize, + &result, + &mut self.yuv, + ); + &self.yuv[..] + } else { + result + } + }) + } + } + + fn unmap(&self) { + unsafe { + (*self.duplication.0).ReleaseFrame(); + if self.fastlane { + (*self.duplication.0).UnMapDesktopSurface(); + } else { + if !self.surface.is_null() { + (*self.surface.0).Unmap(); + } + } + } + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + if !self.duplication.is_null() { + self.unmap(); + } + } +} + +pub struct Displays { + factory: ComPtr, + adapter: ComPtr, + /// Index of the CURRENT adapter. + nadapter: UINT, + /// Index of the NEXT display to fetch. + ndisplay: UINT, +} + +impl Displays { + pub fn new() -> io::Result { + let mut factory = ptr::null_mut(); + wrap_hresult(unsafe { CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory) })?; + + let factory = factory as *mut IDXGIFactory1; + let mut adapter = ptr::null_mut(); + unsafe { + // On error, our adapter is null, so it's fine. + (*factory).EnumAdapters1(0, &mut adapter); + }; + + Ok(Displays { + factory: ComPtr(factory), + adapter: ComPtr(adapter), + nadapter: 0, + ndisplay: 0, + }) + } + + pub fn get_from_gdi() -> Vec { + let mut all = Vec::new(); + let mut i: DWORD = 0; + loop { + let mut d: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + d.cb = std::mem::size_of::() as _; + let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut d as _, 0) }; + if ok == FALSE { + break; + } + i += 1; + if 0 == (d.StateFlags & DISPLAY_DEVICE_ACTIVE) + || (d.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) > 0 + { + continue; + } + // let is_primary = (d.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) > 0; + let mut disp = Display { + inner: ComPtr(std::ptr::null_mut()), + adapter: ComPtr(std::ptr::null_mut()), + desc: unsafe { std::mem::zeroed() }, + gdi: true, + }; + disp.desc.DeviceName = d.DeviceName; + let mut m: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + m.dmSize = std::mem::size_of::() as _; + m.dmDriverExtra = 0; + let ok = unsafe { + EnumDisplaySettingsExW( + disp.desc.DeviceName.as_ptr(), + ENUM_CURRENT_SETTINGS, + &mut m as _, + 0, + ) + }; + if ok == FALSE { + continue; + } + disp.desc.DesktopCoordinates.left = unsafe { m.u1.s2().dmPosition.x }; + disp.desc.DesktopCoordinates.top = unsafe { m.u1.s2().dmPosition.y }; + disp.desc.DesktopCoordinates.right = + disp.desc.DesktopCoordinates.left + m.dmPelsWidth as i32; + disp.desc.DesktopCoordinates.bottom = + disp.desc.DesktopCoordinates.top + m.dmPelsHeight as i32; + disp.desc.AttachedToDesktop = 1; + all.push(disp); + } + all + } + + // No Adapter => Some(None) + // Non-Empty Adapter => Some(Some(OUTPUT)) + // End of Adapter => None + fn read_and_invalidate(&mut self) -> Option> { + // If there is no adapter, there is nothing left for us to do. + + if self.adapter.is_null() { + return Some(None); + } + + // Otherwise, we get the next output of the current adapter. + + let output = unsafe { + let mut output = ptr::null_mut(); + (*self.adapter.0).EnumOutputs(self.ndisplay, &mut output); + ComPtr(output) + }; + + // If the current adapter is done, we free it. + // We return None so the caller gets the next adapter and tries again. + + if output.is_null() { + self.adapter = ComPtr(ptr::null_mut()); + return None; + } + + // Advance to the next display. + + self.ndisplay += 1; + + // We get the display's details. + + let desc = unsafe { + let mut desc = mem::MaybeUninit::uninit().assume_init(); + (*output.0).GetDesc(&mut desc); + desc + }; + + // We cast it up to the version needed for desktop duplication. + + let mut inner: *mut IDXGIOutput1 = ptr::null_mut(); + unsafe { + (*output.0).QueryInterface(&IID_IDXGIOutput1, &mut inner as *mut *mut _ as *mut *mut _); + } + + // If it's null, we have an error. + // So we act like the adapter is done. + + if inner.is_null() { + self.adapter = ComPtr(ptr::null_mut()); + return None; + } + + unsafe { + (*self.adapter.0).AddRef(); + } + + Some(Some(Display { + inner: ComPtr(inner), + adapter: ComPtr(self.adapter.0), + desc, + gdi: false, + })) + } +} + +impl Iterator for Displays { + type Item = Display; + fn next(&mut self) -> Option { + if let Some(res) = self.read_and_invalidate() { + res + } else { + // We need to replace the adapter. + + self.ndisplay = 0; + self.nadapter += 1; + + self.adapter = unsafe { + let mut adapter = ptr::null_mut(); + (*self.factory.0).EnumAdapters1(self.nadapter, &mut adapter); + ComPtr(adapter) + }; + + if let Some(res) = self.read_and_invalidate() { + res + } else { + // All subsequent adapters will also be empty. + None + } + } + } +} + +pub struct Display { + inner: ComPtr, + adapter: ComPtr, + desc: DXGI_OUTPUT_DESC, + gdi: bool, +} + +// optimized for updated region +// https://github.com/dchapyshev/aspia/blob/master/source/base/desktop/win/dxgi_output_duplicator.cc +// rotation +// https://github.com/bryal/dxgcap-rs/blob/master/src/lib.rs + +impl Display { + pub fn width(&self) -> LONG { + self.desc.DesktopCoordinates.right - self.desc.DesktopCoordinates.left + } + + pub fn height(&self) -> LONG { + self.desc.DesktopCoordinates.bottom - self.desc.DesktopCoordinates.top + } + + pub fn attached_to_desktop(&self) -> bool { + self.desc.AttachedToDesktop != 0 + } + + pub fn rotation(&self) -> DXGI_MODE_ROTATION { + self.desc.Rotation + } + + fn create_gdi(&self) -> Option { + if let Ok(res) = CapturerGDI::new(self.name(), self.width(), self.height()) { + Some(res) + } else { + None + } + } + + pub fn hmonitor(&self) -> HMONITOR { + self.desc.Monitor + } + + pub fn name(&self) -> &[u16] { + let s = &self.desc.DeviceName; + let i = s.iter().position(|&x| x == 0).unwrap_or(s.len()); + &s[..i] + } + + pub fn is_online(&self) -> bool { + self.desc.AttachedToDesktop != 0 + } + + pub fn origin(&self) -> (LONG, LONG) { + ( + self.desc.DesktopCoordinates.left, + self.desc.DesktopCoordinates.top, + ) + } +} + +fn wrap_hresult(x: HRESULT) -> io::Result<()> { + use std::io::ErrorKind::*; + Err((match x { + S_OK => return Ok(()), + DXGI_ERROR_ACCESS_LOST => ConnectionReset, + DXGI_ERROR_WAIT_TIMEOUT => TimedOut, + DXGI_ERROR_INVALID_CALL => InvalidData, + E_ACCESSDENIED => PermissionDenied, + DXGI_ERROR_UNSUPPORTED => ConnectionRefused, + DXGI_ERROR_NOT_CURRENTLY_AVAILABLE => Interrupted, + DXGI_ERROR_SESSION_DISCONNECTED => ConnectionAborted, + E_INVALIDARG => InvalidInput, + _ => { + // 0x8000ffff https://www.auslogics.com/en/articles/windows-10-update-error-0x8000ffff-fixed/ + return Err(io::Error::new(Other, format!("Error code: {:#X}", x))); + } + }) + .into()) +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/lib.rs b/rust-rdp/rust-desk/libs/scrap/src/lib.rs new file mode 100644 index 0000000..8db2a57 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/lib.rs @@ -0,0 +1,23 @@ +#[cfg(quartz)] +extern crate block; +#[macro_use] +extern crate cfg_if; +pub extern crate libc; +#[cfg(dxgi)] +extern crate winapi; + +pub use common::*; + +#[cfg(quartz)] +pub mod quartz; + +#[cfg(x11)] +pub mod x11; + +#[cfg(all(x11, feature="wayland"))] +pub mod wayland; + +#[cfg(dxgi)] +pub mod dxgi; + +mod common; diff --git a/rust-rdp/rust-desk/libs/scrap/src/quartz/capturer.rs b/rust-rdp/rust-desk/libs/scrap/src/quartz/capturer.rs new file mode 100644 index 0000000..5be55ea --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/quartz/capturer.rs @@ -0,0 +1,111 @@ +use std::ptr; + +use block::{Block, ConcreteBlock}; +use libc::c_void; +use std::sync::{Arc, Mutex}; + +use super::config::Config; +use super::display::Display; +use super::ffi::*; +use super::frame::Frame; + +pub struct Capturer { + stream: CGDisplayStreamRef, + queue: DispatchQueue, + + width: usize, + height: usize, + format: PixelFormat, + display: Display, + stopped: Arc>, +} + +impl Capturer { + pub fn new( + display: Display, + width: usize, + height: usize, + format: PixelFormat, + config: Config, + handler: F, + ) -> Result { + let stopped = Arc::new(Mutex::new(false)); + let cloned_stopped = stopped.clone(); + let handler: FrameAvailableHandler = ConcreteBlock::new(move |status, _, surface, _| { + use self::CGDisplayStreamFrameStatus::*; + if status == Stopped { + let mut lock = cloned_stopped.lock().unwrap(); + *lock = true; + return; + } + if status == FrameComplete { + handler(unsafe { Frame::new(surface) }); + } + }) + .copy(); + + let queue = unsafe { + dispatch_queue_create( + b"quadrupleslap.scrap\0".as_ptr() as *const i8, + ptr::null_mut(), + ) + }; + + let stream = unsafe { + let config = config.build(); + let stream = CGDisplayStreamCreateWithDispatchQueue( + display.id(), + width, + height, + format, + config, + queue, + &*handler as *const Block<_, _> as *const c_void, + ); + CFRelease(config); + stream + }; + + match unsafe { CGDisplayStreamStart(stream) } { + CGError::Success => Ok(Capturer { + stream, + queue, + width, + height, + format, + display, + stopped, + }), + x => Err(x), + } + } + + pub fn width(&self) -> usize { + self.width + } + pub fn height(&self) -> usize { + self.height + } + pub fn format(&self) -> PixelFormat { + self.format + } + pub fn display(&self) -> Display { + self.display + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + let _ = CGDisplayStreamStop(self.stream); + loop { + if *self.stopped.lock().unwrap() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(30)); + } + CFRelease(self.stream); + dispatch_release(self.queue); + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/quartz/config.rs b/rust-rdp/rust-desk/libs/scrap/src/quartz/config.rs new file mode 100644 index 0000000..11a6d5f --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/quartz/config.rs @@ -0,0 +1,75 @@ +use std::ptr; + +use libc::c_void; + +use super::ffi::*; + +//TODO: Color space, YCbCr matrix. +pub struct Config { + /// Whether the cursor is visible. + pub cursor: bool, + /// Whether it should letterbox or stretch. + pub letterbox: bool, + /// Minimum seconds per frame. + pub throttle: f64, + /// How many frames are allocated. + /// 3 is the recommended value. + /// 8 is the maximum value. + pub queue_length: i8, +} + +impl Config { + /// Don't forget to CFRelease this! + pub fn build(self) -> CFDictionaryRef { + unsafe { + let throttle = CFNumberCreate( + ptr::null_mut(), + CFNumberType::Float64, + &self.throttle as *const _ as *const c_void, + ); + let queue_length = CFNumberCreate( + ptr::null_mut(), + CFNumberType::SInt8, + &self.queue_length as *const _ as *const c_void, + ); + + let keys: [CFStringRef; 4] = [ + kCGDisplayStreamShowCursor, + kCGDisplayStreamPreserveAspectRatio, + kCGDisplayStreamMinimumFrameTime, + kCGDisplayStreamQueueDepth, + ]; + let values: [*mut c_void; 4] = [ + cfbool(self.cursor), + cfbool(self.letterbox), + throttle, + queue_length, + ]; + + let res = CFDictionaryCreate( + ptr::null_mut(), + keys.as_ptr(), + values.as_ptr(), + 4, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + + CFRelease(throttle); + CFRelease(queue_length); + + res + } + } +} + +impl Default for Config { + fn default() -> Config { + Config { + cursor: false, + letterbox: true, + throttle: 0.0, + queue_length: 3, + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/quartz/display.rs b/rust-rdp/rust-desk/libs/scrap/src/quartz/display.rs new file mode 100644 index 0000000..ff96b2c --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/quartz/display.rs @@ -0,0 +1,63 @@ +use std::mem; + +use super::ffi::*; + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[repr(C)] +pub struct Display(u32); + +impl Display { + pub fn primary() -> Display { + Display(unsafe { CGMainDisplayID() }) + } + + pub fn online() -> Result, CGError> { + unsafe { + let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); + let mut len: u32 = 0; + + match CGGetOnlineDisplayList(16, arr.as_mut_ptr(), &mut len) { + CGError::Success => (), + x => return Err(x), + } + + let mut res = Vec::with_capacity(16); + for i in 0..len as usize { + res.push(Display(*arr.get_unchecked(i))); + } + Ok(res) + } + } + + pub fn id(self) -> u32 { + self.0 + } + + pub fn width(self) -> usize { + unsafe { CGDisplayPixelsWide(self.0) } + } + + pub fn height(self) -> usize { + unsafe { CGDisplayPixelsHigh(self.0) } + } + + pub fn is_builtin(self) -> bool { + unsafe { CGDisplayIsBuiltin(self.0) != 0 } + } + + pub fn is_primary(self) -> bool { + unsafe { CGDisplayIsMain(self.0) != 0 } + } + + pub fn is_active(self) -> bool { + unsafe { CGDisplayIsActive(self.0) != 0 } + } + + pub fn is_online(self) -> bool { + unsafe { CGDisplayIsOnline(self.0) != 0 } + } + + pub fn bounds(self) -> CGRect { + unsafe { CGDisplayBounds(self.0) } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/quartz/ffi.rs b/rust-rdp/rust-desk/libs/scrap/src/quartz/ffi.rs new file mode 100644 index 0000000..ca39c0a --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/quartz/ffi.rs @@ -0,0 +1,240 @@ +#![allow(dead_code)] + +use block::RcBlock; +use libc::c_void; + +pub type CGDisplayStreamRef = *mut c_void; +pub type CFDictionaryRef = *mut c_void; +pub type CFBooleanRef = *mut c_void; +pub type CFNumberRef = *mut c_void; +pub type CFStringRef = *mut c_void; +pub type CGDisplayStreamUpdateRef = *mut c_void; +pub type IOSurfaceRef = *mut c_void; +pub type DispatchQueue = *mut c_void; +pub type DispatchQueueAttr = *mut c_void; +pub type CFAllocatorRef = *mut c_void; + +#[repr(C)] +pub struct CFDictionaryKeyCallBacks { + callbacks: [usize; 5], + version: i32, +} + +#[repr(C)] +pub struct CFDictionaryValueCallBacks { + callbacks: [usize; 4], + version: i32, +} + +macro_rules! pixel_format { + ($a:expr, $b:expr, $c:expr, $d:expr) => { + ($a as i32) << 24 | ($b as i32) << 16 | ($c as i32) << 8 | ($d as i32) + }; +} + +pub const SURFACE_LOCK_READ_ONLY: u32 = 0x0000_0001; +pub const SURFACE_LOCK_AVOID_SYNC: u32 = 0x0000_0002; + +pub fn cfbool(x: bool) -> CFBooleanRef { + unsafe { + if x { + kCFBooleanTrue + } else { + kCFBooleanFalse + } + } +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum CGDisplayStreamFrameStatus { + /// A new frame was generated. + FrameComplete = 0, + /// A new frame was not generated because the display did not change. + FrameIdle = 1, + /// A new frame was not generated because the display has gone blank. + FrameBlank = 2, + /// The display stream was stopped. + Stopped = 3, + #[doc(hidden)] + __Nonexhaustive, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum CFNumberType { + /* Fixed-width types */ + SInt8 = 1, + SInt16 = 2, + SInt32 = 3, + SInt64 = 4, + Float32 = 5, + Float64 = 6, + /* 64-bit IEEE 754 */ + /* Basic C types */ + Char = 7, + Short = 8, + Int = 9, + Long = 10, + LongLong = 11, + Float = 12, + Double = 13, + /* Other */ + CFIndex = 14, + NSInteger = 15, + CGFloat = 16, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[must_use] +pub enum CGError { + Success = 0, + Failure = 1000, + IllegalArgument = 1001, + InvalidConnection = 1002, + InvalidContext = 1003, + CannotComplete = 1004, + NotImplemented = 1006, + RangeCheck = 1007, + TypeCheck = 1008, + InvalidOperation = 1010, + NoneAvailable = 1011, + #[doc(hidden)] + __Nonexhaustive, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum PixelFormat { + /// Packed Little Endian ARGB8888 + Argb8888 = pixel_format!('B', 'G', 'R', 'A'), + /// Packed Little Endian ARGB2101010 + Argb2101010 = pixel_format!('l', '1', '0', 'r'), + /// 2-plane "video" range YCbCr 4:2:0 + YCbCr420Video = pixel_format!('4', '2', '0', 'v'), + /// 2-plane "full" range YCbCr 4:2:0 + YCbCr420Full = pixel_format!('4', '2', '0', 'f'), + #[doc(hidden)] + __Nonexhaustive, +} + +pub type CGDisplayStreamFrameAvailableHandler = *const c_void; + +pub type FrameAvailableHandler = RcBlock< + ( + CGDisplayStreamFrameStatus, // status + u64, // displayTime + IOSurfaceRef, // frameSurface + CGDisplayStreamUpdateRef, // updateRef + ), + (), +>; + +#[cfg(target_pointer_width = "64")] +pub type CGFloat = f64; +#[cfg(not(target_pointer_width = "64"))] +pub type CGFloat = f32; +#[repr(C)] +pub struct CGPoint { + pub x: CGFloat, + pub y: CGFloat, +} +#[repr(C)] +pub struct CGSize { + pub width: CGFloat, + pub height: CGFloat, +} +#[repr(C)] +pub struct CGRect { + pub origin: CGPoint, + pub size: CGSize, +} + +#[link(name = "System", kind = "dylib")] +#[link(name = "CoreGraphics", kind = "framework")] +#[link(name = "CoreFoundation", kind = "framework")] +#[link(name = "IOSurface", kind = "framework")] +extern "C" { + // CoreGraphics + + pub static kCGDisplayStreamShowCursor: CFStringRef; + pub static kCGDisplayStreamPreserveAspectRatio: CFStringRef; + pub static kCGDisplayStreamMinimumFrameTime: CFStringRef; + pub static kCGDisplayStreamQueueDepth: CFStringRef; + + pub fn CGDisplayStreamCreateWithDispatchQueue( + display: u32, + output_width: usize, + output_height: usize, + pixel_format: PixelFormat, + properties: CFDictionaryRef, + queue: DispatchQueue, + handler: CGDisplayStreamFrameAvailableHandler, + ) -> CGDisplayStreamRef; + + pub fn CGDisplayStreamStart(displayStream: CGDisplayStreamRef) -> CGError; + + pub fn CGDisplayStreamStop(displayStream: CGDisplayStreamRef) -> CGError; + + pub fn CGMainDisplayID() -> u32; + pub fn CGDisplayPixelsWide(display: u32) -> usize; + pub fn CGDisplayPixelsHigh(display: u32) -> usize; + + pub fn CGGetOnlineDisplayList( + max_displays: u32, + online_displays: *mut u32, + display_count: *mut u32, + ) -> CGError; + + pub fn CGDisplayIsBuiltin(display: u32) -> i32; + pub fn CGDisplayIsMain(display: u32) -> i32; + pub fn CGDisplayIsActive(display: u32) -> i32; + pub fn CGDisplayIsOnline(display: u32) -> i32; + + pub fn CGDisplayBounds(display: u32) -> CGRect; + + // IOSurface + + pub fn IOSurfaceGetAllocSize(buffer: IOSurfaceRef) -> usize; + pub fn IOSurfaceGetBaseAddress(buffer: IOSurfaceRef) -> *mut c_void; + pub fn IOSurfaceIncrementUseCount(buffer: IOSurfaceRef); + pub fn IOSurfaceDecrementUseCount(buffer: IOSurfaceRef); + pub fn IOSurfaceLock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; + pub fn IOSurfaceUnlock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; + pub fn IOSurfaceGetBaseAddressOfPlane(buffer: IOSurfaceRef, index: usize) -> *mut c_void; + pub fn IOSurfaceGetBytesPerRowOfPlane(buffer: IOSurfaceRef, index: usize) -> usize; + + // Dispatch + + pub fn dispatch_queue_create(label: *const i8, attr: DispatchQueueAttr) -> DispatchQueue; + + pub fn dispatch_release(object: DispatchQueue); + + // Core Foundation + + pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks; + pub static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks; + + // EVEN THE BOOLEANS ARE REFERENCES. + pub static kCFBooleanTrue: CFBooleanRef; + pub static kCFBooleanFalse: CFBooleanRef; + + pub fn CFNumberCreate( + allocator: CFAllocatorRef, + theType: CFNumberType, + valuePtr: *const c_void, + ) -> CFNumberRef; + + pub fn CFDictionaryCreate( + allocator: CFAllocatorRef, + keys: *const *mut c_void, + values: *const *mut c_void, + numValues: i64, + keyCallBacks: *const CFDictionaryKeyCallBacks, + valueCallBacks: *const CFDictionaryValueCallBacks, + ) -> CFDictionaryRef; + + pub fn CFRetain(cf: *const c_void); + pub fn CFRelease(cf: *const c_void); +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/quartz/frame.rs b/rust-rdp/rust-desk/libs/scrap/src/quartz/frame.rs new file mode 100644 index 0000000..61dd4b5 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/quartz/frame.rs @@ -0,0 +1,79 @@ +use std::{ops, ptr, slice}; + +use super::ffi::*; + +pub struct Frame { + surface: IOSurfaceRef, + inner: &'static [u8], + i420: *mut u8, + i420_len: usize, +} + +impl Frame { + pub unsafe fn new(surface: IOSurfaceRef) -> Frame { + CFRetain(surface); + IOSurfaceIncrementUseCount(surface); + + IOSurfaceLock(surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); + + let inner = slice::from_raw_parts( + IOSurfaceGetBaseAddress(surface) as *const u8, + IOSurfaceGetAllocSize(surface), + ); + + Frame { + surface, + inner, + i420: ptr::null_mut(), + i420_len: 0, + } + } + + pub fn nv12_to_i420<'a>(&'a mut self, w: usize, h: usize, i420: &'a mut Vec) { + if self.inner.is_empty() { + return; + } + unsafe { + let plane0 = IOSurfaceGetBaseAddressOfPlane(self.surface, 0); + let stride0 = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0); + let plane1 = IOSurfaceGetBaseAddressOfPlane(self.surface, 1); + let stride1 = IOSurfaceGetBytesPerRowOfPlane(self.surface, 1); + crate::common::nv12_to_i420( + plane0 as _, + stride0 as _, + plane1 as _, + stride1 as _, + w, + h, + i420, + ); + self.i420 = i420.as_mut_ptr() as _; + self.i420_len = i420.len(); + } + } +} + +impl ops::Deref for Frame { + type Target = [u8]; + fn deref<'a>(&'a self) -> &'a [u8] { + if self.i420.is_null() { + self.inner + } else { + unsafe { + let inner = slice::from_raw_parts(self.i420 as *const u8, self.i420_len); + inner + } + } + } +} + +impl Drop for Frame { + fn drop(&mut self) { + unsafe { + IOSurfaceUnlock(self.surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); + + IOSurfaceDecrementUseCount(self.surface); + CFRelease(self.surface); + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/quartz/mod.rs b/rust-rdp/rust-desk/libs/scrap/src/quartz/mod.rs new file mode 100644 index 0000000..94488e0 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/quartz/mod.rs @@ -0,0 +1,11 @@ +pub use self::capturer::Capturer; +pub use self::config::Config; +pub use self::display::Display; +pub use self::ffi::{CGError, PixelFormat}; +pub use self::frame::Frame; + +mod capturer; +mod config; +mod display; +pub mod ffi; +mod frame; diff --git a/rust-rdp/rust-desk/libs/scrap/src/wayland.rs b/rust-rdp/rust-desk/libs/scrap/src/wayland.rs new file mode 100644 index 0000000..82b2193 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/wayland.rs @@ -0,0 +1,3 @@ +pub mod pipewire; +mod pipewire_dbus; +pub mod capturable; diff --git a/rust-rdp/rust-desk/libs/scrap/src/wayland/README.md b/rust-rdp/rust-desk/libs/scrap/src/wayland/README.md new file mode 100644 index 0000000..3ea8ae1 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/wayland/README.md @@ -0,0 +1,11 @@ +# About + +Derived from https://github.com/H-M-H/Weylus/tree/master/src/capturable with the author's consent, https://github.com/rustdesk/rustdesk/issues/56#issuecomment-882727967 + +# Dep + +Works fine on Ubuntu 21.04 with pipewire 3 and xdg-desktop-portal 1.8 + +` +apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +` diff --git a/rust-rdp/rust-desk/libs/scrap/src/wayland/capturable.rs b/rust-rdp/rust-desk/libs/scrap/src/wayland/capturable.rs new file mode 100644 index 0000000..05a5ec7 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/wayland/capturable.rs @@ -0,0 +1,58 @@ +use std::boxed::Box; +use std::error::Error; + +pub enum PixelProvider<'a> { + // 8 bits per color + RGB(usize, usize, &'a [u8]), + BGR0(usize, usize, &'a [u8]), + // width, height, stride + BGR0S(usize, usize, usize, &'a [u8]), + NONE, +} + +impl<'a> PixelProvider<'a> { + pub fn size(&self) -> (usize, usize) { + match self { + PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::BGR0(w, h, _) => (*w, *h), + PixelProvider::BGR0S(w, h, _, _) => (*w, *h), + PixelProvider::NONE => (0, 0), + } + } +} + +pub trait Recorder { + fn capture(&mut self, timeout_ms: u64) -> Result>; +} + +pub trait BoxCloneCapturable { + fn box_clone(&self) -> Box; +} + +impl BoxCloneCapturable for T +where + T: Clone + Capturable + 'static, +{ + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub trait Capturable: Send + BoxCloneCapturable { + /// Name of the Capturable, for example the window title, if it is a window. + fn name(&self) -> String; + /// Return x, y, width, height of the Capturable as floats relative to the absolute size of the + /// screen. For example x=0.5, y=0.0, width=0.5, height=1.0 means the right half of the screen. + fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box>; + /// Callback that is called right before input is simulated. + /// Useful to focus the window on input. + fn before_input(&mut self) -> Result<(), Box>; + /// Return a Recorder that can record the current capturable. + fn recorder(&self, capture_cursor: bool) -> Result, Box>; +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire.rs b/rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire.rs new file mode 100644 index 0000000..844e8ee --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire.rs @@ -0,0 +1,530 @@ +use std::collections::HashMap; +use std::error::Error; +use std::os::unix::io::AsRawFd; +use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::time::Duration; +use tracing::{debug, trace, warn}; + +use dbus::{ + arg::{OwnedFd, PropMap, RefArg, Variant}, + blocking::{Proxy, SyncConnection}, + message::{MatchRule, MessageType}, + Message, +}; + +use gstreamer as gst; +use gstreamer::prelude::*; +use gstreamer_app::AppSink; + +use super::capturable::PixelProvider; +use super::capturable::{Capturable, Recorder}; + +use super::pipewire_dbus::{OrgFreedesktopPortalRequestResponse, OrgFreedesktopPortalScreenCast}; + +#[derive(Debug, Clone, Copy)] +struct PwStreamInfo { + path: u64, + source_type: u64, + position: (i32, i32), + size: (usize, usize), +} + +#[derive(Debug)] +pub struct DBusError(String); + +impl std::fmt::Display for DBusError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(s) = self; + write!(f, "{}", s) + } +} + +impl Error for DBusError {} + +#[derive(Debug)] +pub struct GStreamerError(String); + +impl std::fmt::Display for GStreamerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(s) = self; + write!(f, "{}", s) + } +} + +impl Error for GStreamerError {} + +#[derive(Clone)] +pub struct PipeWireCapturable { + // connection needs to be kept alive for recording + dbus_conn: Arc, + fd: OwnedFd, + path: u64, + source_type: u64, + pub position: (i32, i32), + pub size: (usize, usize), +} + +impl PipeWireCapturable { + fn new(conn: Arc, fd: OwnedFd, stream: PwStreamInfo) -> Self { + Self { + dbus_conn: conn, + fd, + path: stream.path, + source_type: stream.source_type, + position: stream.position, + size: stream.size, + } + } +} + +impl std::fmt::Debug for PipeWireCapturable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PipeWireCapturable {{dbus: {}, fd: {}, path: {}, source_type: {}}}", + self.dbus_conn.unique_name(), + self.fd.as_raw_fd(), + self.path, + self.source_type + ) + } +} + +impl Capturable for PipeWireCapturable { + fn name(&self) -> String { + let type_str = match self.source_type { + 1 => "Desktop", + 2 => "Window", + _ => "Unknow", + }; + format!("Pipewire {}, path: {}", type_str, self.path) + } + + fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box> { + Ok((0.0, 0.0, 1.0, 1.0)) + } + + fn before_input(&mut self) -> Result<(), Box> { + Ok(()) + } + + fn recorder(&self, _capture_cursor: bool) -> Result, Box> { + Ok(Box::new(PipeWireRecorder::new(self.clone())?)) + } +} + +pub struct PipeWireRecorder { + buffer: Option>, + buffer_cropped: Vec, + is_cropped: bool, + pipeline: gst::Pipeline, + appsink: AppSink, + width: usize, + height: usize, +} + +impl PipeWireRecorder { + pub fn new(capturable: PipeWireCapturable) -> Result> { + let pipeline = gst::Pipeline::new(None); + + let src = gst::ElementFactory::make("pipewiresrc", None)?; + src.set_property("fd", &capturable.fd.as_raw_fd())?; + src.set_property("path", &format!("{}", capturable.path))?; + + // For some reason pipewire blocks on destruction of AppSink if this is not set to true, + // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 + src.set_property("always-copy", &true)?; + + let sink = gst::ElementFactory::make("appsink", None)?; + sink.set_property("drop", &true)?; + sink.set_property("max-buffers", &1u32)?; + + pipeline.add_many(&[&src, &sink])?; + src.link(&sink)?; + let appsink = sink + .dynamic_cast::() + .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; + appsink.set_caps(Some(&gst::Caps::new_simple( + "video/x-raw", + &[("format", &"BGRx")], + ))); + + pipeline.set_state(gst::State::Playing)?; + Ok(Self { + pipeline, + appsink, + buffer: None, + width: 0, + height: 0, + buffer_cropped: vec![], + is_cropped: false, + }) + } +} + +impl Recorder for PipeWireRecorder { + fn capture(&mut self, timeout_ms: u64) -> Result> { + if let Some(sample) = self + .appsink + .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) + { + let cap = sample + .get_caps() + .ok_or("Failed get caps")? + .get_structure(0) + .ok_or("Failed to get structure")?; + let w: i32 = cap.get_value("width")?.get_some()?; + let h: i32 = cap.get_value("height")?.get_some()?; + let w = w as usize; + let h = h as usize; + let buf = sample + .get_buffer_owned() + .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; + let mut crop = buf + .get_meta::() + .map(|m| m.get_rect()); + // only crop if necessary + if Some((0, 0, w as u32, h as u32)) == crop { + crop = None; + } + let buf = buf + .into_mapped_buffer_readable() + .map_err(|_| GStreamerError("Failed to map buffer.".into()))?; + let buf_size = buf.get_size(); + // BGRx is 4 bytes per pixel + if buf_size != (w * h * 4) { + // for some reason the width and height of the caps do not guarantee correct buffer + // size, so ignore those buffers, see: + // https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/985 + trace!( + "Size of mapped buffer: {} does NOT match size of capturable {}x{}@BGRx, \ + dropping it!", + buf_size, + w, + h + ); + } else { + // Copy region specified by crop into self.buffer_cropped + // TODO: Figure out if ffmpeg provides a zero copy alternative + if let Some((x_off, y_off, w_crop, h_crop)) = crop { + let x_off = x_off as usize; + let y_off = y_off as usize; + let w_crop = w_crop as usize; + let h_crop = h_crop as usize; + self.buffer_cropped.clear(); + let data = buf.as_slice(); + // BGRx is 4 bytes per pixel + self.buffer_cropped.reserve(w_crop * h_crop * 4); + for y in y_off..(y_off + h_crop) { + let i = 4 * (w * y + x_off); + self.buffer_cropped.extend(&data[i..i + 4 * w_crop]); + } + self.width = w_crop; + self.height = h_crop; + } else { + self.width = w; + self.height = h; + } + self.is_cropped = crop.is_some(); + self.buffer = Some(buf); + } + } else { + return Ok(PixelProvider::NONE); + } + if self.buffer.is_none() { + return Err(Box::new(GStreamerError("No buffer available!".into()))); + } + Ok(PixelProvider::BGR0( + self.width, + self.height, + if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer.as_ref().unwrap().as_slice() + }, + )) + } +} + +impl Drop for PipeWireRecorder { + fn drop(&mut self) { + if let Err(err) = self.pipeline.set_state(gst::State::Null) { + warn!("Failed to stop GStreamer pipeline: {}.", err); + } + } +} + +fn handle_response( + conn: &SyncConnection, + path: dbus::Path<'static>, + mut f: F, + failure_out: Arc, +) -> Result +where + F: FnMut( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &Message, + ) -> Result<(), Box> + + Send + + Sync + + 'static, +{ + let mut m = MatchRule::new(); + m.path = Some(path); + m.msg_type = Some(MessageType::Signal); + m.sender = Some("org.freedesktop.portal.Desktop".into()); + m.interface = Some("org.freedesktop.portal.Request".into()); + conn.add_match(m, move |r: OrgFreedesktopPortalRequestResponse, c, m| { + debug!("Response from DBus: response: {:?}, message: {:?}", r, m); + match r.response { + 0 => {} + 1 => { + warn!("DBus response: User cancelled interaction."); + failure_out.store(true, std::sync::atomic::Ordering::Relaxed); + return true; + } + c => { + warn!("DBus response: Unknown error, code: {}.", c); + failure_out.store(true, std::sync::atomic::Ordering::Relaxed); + return true; + } + } + if let Err(err) = f(r, c, m) { + warn!("Error requesting screen capture via dbus: {}", err); + failure_out.store(true, std::sync::atomic::Ordering::Relaxed); + } + true + }) +} + +fn get_portal(conn: &SyncConnection) -> Proxy<&SyncConnection> { + conn.with_proxy( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + Duration::from_millis(1000), + ) +} + +fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec { + (move || { + Some( + response + .results + .get("streams")? + .as_iter()? + .next()? + .as_iter()? + .filter_map(|stream| { + let mut itr = stream.as_iter()?; + let path = itr.next()?.as_u64()?; + let (keys, values): (Vec<(usize, &dyn RefArg)>, Vec<(usize, &dyn RefArg)>) = + itr.next()? + .as_iter()? + .enumerate() + .partition(|(i, _)| i % 2 == 0); + let attributes = keys + .iter() + .filter_map(|(_, key)| Some(key.as_str()?.to_owned())) + .zip( + values + .iter() + .map(|(_, arg)| *arg) + .collect::>(), + ) + .collect::>(); + let mut info = PwStreamInfo { + path, + source_type: attributes + .get("source_type") + .map_or(Some(0), |v| v.as_u64())?, + position: (0, 0), + size: (0, 0), + }; + let v = attributes + .get("size")? + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.size.0 = v[0] as _; + info.size.1 = v[1] as _; + } + } + let v = attributes + .get("position")? + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.position.0 = v[0] as _; + info.position.1 = v[1] as _; + } + } + Some(info) + }) + .collect::>(), + ) + })() + .unwrap_or_default() +} + +static mut INIT: bool = false; + +// mostly inspired by https://gitlab.gnome.org/snippets/19 +fn request_screen_cast( + capture_cursor: bool, +) -> Result<(SyncConnection, OwnedFd, Vec), Box> { + unsafe { + if !INIT { + gstreamer::init()?; + INIT = true; + } + } + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + let mut args: PropMap = HashMap::new(); + let fd: Arc>> = Arc::new(Mutex::new(None)); + let fd_res = fd.clone(); + let streams: Arc>> = Arc::new(Mutex::new(Vec::new())); + let streams_res = streams.clone(); + let failure = Arc::new(AtomicBool::new(false)); + let failure_res = failure.clone(); + args.insert( + "session_handle_token".to_string(), + Variant(Box::new("u1".to_string())), + ); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u1".to_string())), + ); + let path = portal.create_session(args)?; + handle_response( + &conn, + path, + move |r: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u2".to_string())), + ); + // https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-method-org-freedesktop-portal-ScreenCast.SelectSources + args.insert("multiple".into(), Variant(Box::new(true))); + args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); + + let cursor_mode = if capture_cursor { 2u32 } else { 1u32 }; + let plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); + if plasma && capture_cursor { + // Warn the user if capturing the cursor is tried on kde as this can crash + // kwin_wayland and tear down the plasma desktop, see: + // https://bugs.kde.org/show_bug.cgi?id=435042 + warn!("You are attempting to capture the cursor under KDE Plasma, this may crash your \ + desktop, see https://bugs.kde.org/show_bug.cgi?id=435042 for details! \ + You have been warned."); + } + args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + let session: dbus::Path = r + .results + .get("session_handle") + .ok_or_else(|| { + DBusError(format!( + "Failed to obtain session_handle from response: {:?}", + r + )) + })? + .as_str() + .ok_or_else(|| DBusError("Failed to convert session_handle to string.".into()))? + .to_string() + .into(); + let path = portal.select_sources(session.clone(), args)?; + let fd = fd.clone(); + let streams = streams.clone(); + let failure = failure.clone(); + let failure_out = failure.clone(); + handle_response( + c, + path, + move |_: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u3".to_string())), + ); + let path = portal.start(session.clone(), "", args)?; + let session = session.clone(); + let fd = fd.clone(); + let streams = streams.clone(); + let failure = failure.clone(); + let failure_out = failure.clone(); + handle_response( + c, + path, + move |r: OrgFreedesktopPortalRequestResponse, c, _| { + streams + .clone() + .lock() + .unwrap() + .append(&mut streams_from_response(r)); + let portal = get_portal(c); + fd.clone().lock().unwrap().replace( + portal.open_pipe_wire_remote(session.clone(), HashMap::new())?, + ); + Ok(()) + }, + failure_out, + )?; + Ok(()) + }, + failure_out, + )?; + Ok(()) + }, + failure_res.clone(), + )?; + // wait 3 minutes for user interaction + for _ in 0..1800 { + conn.process(Duration::from_millis(100))?; + // Once we got a file descriptor we are done! + if fd_res.lock().unwrap().is_some() { + break; + } + + if failure_res.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + } + let fd_res = fd_res.lock().unwrap(); + let streams_res = streams_res.lock().unwrap(); + if fd_res.is_some() && !streams_res.is_empty() { + Ok((conn, fd_res.clone().unwrap(), streams_res.clone())) + } else { + Err(Box::new(DBusError( + "Failed to obtain screen capture.".into(), + ))) + } +} + +pub fn get_capturables(capture_cursor: bool) -> Result, Box> { + let (conn, fd, streams) = request_screen_cast(capture_cursor)?; + let conn = Arc::new(conn); + Ok(streams + .into_iter() + .map(|s| PipeWireCapturable::new(conn.clone(), fd.clone(), s)) + .collect()) +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire_dbus.rs b/rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire_dbus.rs new file mode 100644 index 0000000..3349ec8 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/wayland/pipewire_dbus.rs @@ -0,0 +1,144 @@ +// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs +use dbus; +#[allow(unused_imports)] +use dbus::arg; +use dbus::blocking; + +pub trait OrgFreedesktopPortalScreenCast { + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; + fn select_sources( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn open_pipe_wire_remote( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result; + fn available_source_types(&self) -> Result; + fn available_cursor_modes(&self) -> Result; + fn version(&self) -> Result; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> + OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C> +{ + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "CreateSession", + (options,), + ) + .map(|r: (dbus::Path<'static>,)| r.0) + } + + fn select_sources( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "SelectSources", + (session_handle, options), + ) + .map(|r: (dbus::Path<'static>,)| r.0) + } + + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "Start", + (session_handle, parent_window, options), + ) + .map(|r: (dbus::Path<'static>,)| r.0) + } + + fn open_pipe_wire_remote( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "OpenPipeWireRemote", + (session_handle, options), + ) + .map(|r: (arg::OwnedFd,)| r.0) + } + + fn available_source_types(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "AvailableSourceTypes", + ) + } + + fn available_cursor_modes(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "AvailableCursorModes", + ) + } + + fn version(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "version", + ) + } +} + +pub trait OrgFreedesktopPortalRequest { + fn close(&self) -> Result<(), dbus::Error>; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopPortalRequest + for blocking::Proxy<'a, C> +{ + fn close(&self) -> Result<(), dbus::Error> { + self.method_call("org.freedesktop.portal.Request", "Close", ()) + } +} + +#[derive(Debug)] +pub struct OrgFreedesktopPortalRequestResponse { + pub response: u32, + pub results: arg::PropMap, +} + +impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { + fn append(&self, i: &mut arg::IterAppend) { + arg::RefArg::append(&self.response, i); + arg::RefArg::append(&self.results, i); + } +} + +impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { + fn read(i: &mut arg::Iter) -> Result { + Ok(OrgFreedesktopPortalRequestResponse { + response: i.read()?, + results: i.read()?, + }) + } +} + +impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { + const NAME: &'static str = "Response"; + const INTERFACE: &'static str = "org.freedesktop.portal.Request"; +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/x11/capturer.rs b/rust-rdp/rust-desk/libs/scrap/src/x11/capturer.rs new file mode 100644 index 0000000..d989660 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/x11/capturer.rs @@ -0,0 +1,123 @@ +use std::{io, ptr, slice}; + +use libc; + +use super::ffi::*; +use super::Display; + +pub struct Capturer { + display: Display, + shmid: i32, + xcbid: u32, + buffer: *const u8, + + size: usize, + use_yuv: bool, + yuv: Vec, +} + +impl Capturer { + pub fn new(display: Display, use_yuv: bool) -> io::Result { + // Calculate dimensions. + + let pixel_width = 4; + let rect = display.rect(); + let size = (rect.w as usize) * (rect.h as usize) * pixel_width; + + // Create a shared memory segment. + + let shmid = unsafe { + libc::shmget( + libc::IPC_PRIVATE, + size, + // Everyone can do anything. + libc::IPC_CREAT | 0o777, + ) + }; + + if shmid == -1 { + return Err(io::Error::last_os_error()); + } + + // Attach the segment to a readable address. + + let buffer = unsafe { libc::shmat(shmid, ptr::null(), libc::SHM_RDONLY) } as *mut u8; + + if buffer as isize == -1 { + return Err(io::Error::last_os_error()); + } + + // Attach the segment to XCB. + + let server = display.server().raw(); + let xcbid = unsafe { xcb_generate_id(server) }; + unsafe { + xcb_shm_attach( + server, + xcbid, + shmid as u32, + 0, // False, i.e. not read-only. + ); + } + + let c = Capturer { + display, + shmid, + xcbid, + buffer, + size, + use_yuv, + yuv: Vec::new(), + }; + Ok(c) + } + + pub fn display(&self) -> &Display { + &self.display + } + + fn get_image(&self) { + let rect = self.display.rect(); + unsafe { + let request = xcb_shm_get_image_unchecked( + self.display.server().raw(), + self.display.root(), + rect.x, + rect.y, + rect.w, + rect.h, + !0, + XCB_IMAGE_FORMAT_Z_PIXMAP, + self.xcbid, + 0, + ); + let response = + xcb_shm_get_image_reply(self.display.server().raw(), request, ptr::null_mut()); + libc::free(response as *mut _); + } + } + + pub fn frame<'b>(&'b mut self) -> &'b [u8] { + self.get_image(); + let result = unsafe { slice::from_raw_parts(self.buffer, self.size) }; + if self.use_yuv { + crate::common::bgra_to_i420(self.display.w(), self.display.h(), &result, &mut self.yuv); + &self.yuv[..] + } else { + result + } + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + // Detach segment from XCB. + xcb_shm_detach(self.display.server().raw(), self.xcbid); + // Detach segment from our space. + libc::shmdt(self.buffer as *mut _); + // Destroy the shared memory segment. + libc::shmctl(self.shmid, libc::IPC_RMID, ptr::null_mut()); + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/x11/display.rs b/rust-rdp/rust-desk/libs/scrap/src/x11/display.rs new file mode 100644 index 0000000..0c5ba50 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/x11/display.rs @@ -0,0 +1,55 @@ +use std::rc::Rc; + +use super::ffi::*; +use super::Server; + +#[derive(Debug)] +pub struct Display { + server: Rc, + default: bool, + rect: Rect, + root: xcb_window_t, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct Rect { + pub x: i16, + pub y: i16, + pub w: u16, + pub h: u16, +} + +impl Display { + pub unsafe fn new( + server: Rc, + default: bool, + rect: Rect, + root: xcb_window_t, + ) -> Display { + Display { + server, + default, + rect, + root, + } + } + + pub fn server(&self) -> &Rc { + &self.server + } + pub fn is_default(&self) -> bool { + self.default + } + pub fn rect(&self) -> Rect { + self.rect + } + pub fn w(&self) -> usize { + self.rect.w as _ + } + pub fn h(&self) -> usize { + self.rect.h as _ + } + pub fn root(&self) -> xcb_window_t { + self.root + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/x11/ffi.rs b/rust-rdp/rust-desk/libs/scrap/src/x11/ffi.rs new file mode 100644 index 0000000..5df5c46 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/x11/ffi.rs @@ -0,0 +1,205 @@ +#![allow(non_camel_case_types)] + +use libc::c_void; + +#[link(name = "xcb")] +#[link(name = "xcb-shm")] +#[link(name = "xcb-randr")] +extern "C" { + pub fn xcb_connect(displayname: *const i8, screenp: *mut i32) -> *mut xcb_connection_t; + + pub fn xcb_disconnect(c: *mut xcb_connection_t); + + pub fn xcb_connection_has_error(c: *mut xcb_connection_t) -> i32; + + pub fn xcb_get_setup(c: *mut xcb_connection_t) -> *const xcb_setup_t; + + pub fn xcb_setup_roots_iterator(r: *const xcb_setup_t) -> xcb_screen_iterator_t; + + pub fn xcb_screen_next(i: *mut xcb_screen_iterator_t); + + pub fn xcb_generate_id(c: *mut xcb_connection_t) -> u32; + + pub fn xcb_shm_attach( + c: *mut xcb_connection_t, + shmseg: xcb_shm_seg_t, + shmid: u32, + read_only: u8, + ) -> xcb_void_cookie_t; + + pub fn xcb_shm_detach(c: *mut xcb_connection_t, shmseg: xcb_shm_seg_t) -> xcb_void_cookie_t; + + pub fn xcb_shm_get_image_unchecked( + c: *mut xcb_connection_t, + drawable: xcb_drawable_t, + x: i16, + y: i16, + width: u16, + height: u16, + plane_mask: u32, + format: u8, + shmseg: xcb_shm_seg_t, + offset: u32, + ) -> xcb_shm_get_image_cookie_t; + + pub fn xcb_shm_get_image_reply( + c: *mut xcb_connection_t, + cookie: xcb_shm_get_image_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_shm_get_image_reply_t; + + pub fn xcb_randr_get_monitors_unchecked( + c: *mut xcb_connection_t, + window: xcb_window_t, + get_active: u8, + ) -> xcb_randr_get_monitors_cookie_t; + + pub fn xcb_randr_get_monitors_reply( + c: *mut xcb_connection_t, + cookie: xcb_randr_get_monitors_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_randr_get_monitors_reply_t; + + pub fn xcb_randr_get_monitors_monitors_iterator( + r: *const xcb_randr_get_monitors_reply_t, + ) -> xcb_randr_monitor_info_iterator_t; + + pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); +} + +pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; + +pub type xcb_atom_t = u32; +pub type xcb_connection_t = c_void; +pub type xcb_window_t = u32; +pub type xcb_keycode_t = u8; +pub type xcb_visualid_t = u32; +pub type xcb_timestamp_t = u32; +pub type xcb_colormap_t = u32; +pub type xcb_shm_seg_t = u32; +pub type xcb_drawable_t = u32; + +#[repr(C)] +pub struct xcb_setup_t { + pub status: u8, + pub pad0: u8, + pub protocol_major_version: u16, + pub protocol_minor_version: u16, + pub length: u16, + pub release_number: u32, + pub resource_id_base: u32, + pub resource_id_mask: u32, + pub motion_buffer_size: u32, + pub vendor_len: u16, + pub maximum_request_length: u16, + pub roots_len: u8, + pub pixmap_formats_len: u8, + pub image_byte_order: u8, + pub bitmap_format_bit_order: u8, + pub bitmap_format_scanline_unit: u8, + pub bitmap_format_scanline_pad: u8, + pub min_keycode: xcb_keycode_t, + pub max_keycode: xcb_keycode_t, + pub pad1: [u8; 4], +} + +#[repr(C)] +pub struct xcb_screen_iterator_t { + pub data: *mut xcb_screen_t, + pub rem: i32, + pub index: i32, +} + +#[repr(C)] +pub struct xcb_screen_t { + pub root: xcb_window_t, + pub default_colormap: xcb_colormap_t, + pub white_pixel: u32, + pub black_pixel: u32, + pub current_input_masks: u32, + pub width_in_pixels: u16, + pub height_in_pixels: u16, + pub width_in_millimeters: u16, + pub height_in_millimeters: u16, + pub min_installed_maps: u16, + pub max_installed_maps: u16, + pub root_visual: xcb_visualid_t, + pub backing_stores: u8, + pub save_unders: u8, + pub root_depth: u8, + pub allowed_depths_len: u8, +} + +#[repr(C)] +pub struct xcb_randr_monitor_info_iterator_t { + pub data: *mut xcb_randr_monitor_info_t, + pub rem: i32, + pub index: i32, +} + +#[repr(C)] +pub struct xcb_randr_monitor_info_t { + pub name: xcb_atom_t, + pub primary: u8, + pub automatic: u8, + pub n_output: u16, + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub width_mm: u32, + pub height_mm: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_randr_get_monitors_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_shm_get_image_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_void_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +pub struct xcb_generic_error_t { + pub response_type: u8, + pub error_code: u8, + pub sequence: u16, + pub resource_id: u32, + pub minor_code: u16, + pub major_code: u8, + pub pad0: u8, + pub pad: [u32; 5], + pub full_sequence: u32, +} + +#[repr(C)] +pub struct xcb_shm_get_image_reply_t { + pub response_type: u8, + pub depth: u8, + pub sequence: u16, + pub length: u32, + pub visual: xcb_visualid_t, + pub size: u32, +} + +#[repr(C)] +pub struct xcb_randr_get_monitors_reply_t { + pub response_type: u8, + pub pad0: u8, + pub sequence: u16, + pub length: u32, + pub timestamp: xcb_timestamp_t, + pub n_monitors: u32, + pub n_outputs: u32, + pub pad1: [u8; 12], +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/x11/iter.rs b/rust-rdp/rust-desk/libs/scrap/src/x11/iter.rs new file mode 100644 index 0000000..cb3310b --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/x11/iter.rs @@ -0,0 +1,93 @@ +use std::ptr; +use std::rc::Rc; + +use libc; + +use super::ffi::*; +use super::{Display, Rect, Server}; + +//TODO: Do I have to free the displays? + +pub struct DisplayIter { + outer: xcb_screen_iterator_t, + inner: Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)>, + server: Rc, +} + +impl DisplayIter { + pub unsafe fn new(server: Rc) -> DisplayIter { + let mut outer = xcb_setup_roots_iterator(server.setup()); + let inner = Self::next_screen(&mut outer, &server); + DisplayIter { + outer, + inner, + server, + } + } + + fn next_screen( + outer: &mut xcb_screen_iterator_t, + server: &Server, + ) -> Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)> { + if outer.rem == 0 { + return None; + } + + unsafe { + let root = (*outer.data).root; + + let cookie = xcb_randr_get_monitors_unchecked( + server.raw(), + root, + 1, //TODO: I don't know if this should be true or false. + ); + + let response = xcb_randr_get_monitors_reply(server.raw(), cookie, ptr::null_mut()); + + let inner = xcb_randr_get_monitors_monitors_iterator(response); + + libc::free(response as *mut _); + xcb_screen_next(outer); + + Some((inner, root)) + } + } +} + +impl Iterator for DisplayIter { + type Item = Display; + + fn next(&mut self) -> Option { + loop { + if let Some((ref mut inner, root)) = self.inner { + // If there is something in the current screen, return that. + if inner.rem != 0 { + unsafe { + let data = &*inner.data; + + let display = Display::new( + self.server.clone(), + data.primary != 0, + Rect { + x: data.x, + y: data.y, + w: data.width, + h: data.height, + }, + root, + ); + + xcb_randr_monitor_info_next(inner); + return Some(display); + } + } + } else { + // If there is no current screen, the screen iterator is empty. + return None; + } + + // The current screen was empty, so try the next screen. + self.inner = Self::next_screen(&mut self.outer, &self.server); + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/src/x11/mod.rs b/rust-rdp/rust-desk/libs/scrap/src/x11/mod.rs new file mode 100644 index 0000000..382d1f6 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/x11/mod.rs @@ -0,0 +1,10 @@ +pub use self::capturer::*; +pub use self::display::*; +pub use self::iter::*; +pub use self::server::*; + +mod capturer; +mod display; +mod ffi; +mod iter; +mod server; diff --git a/rust-rdp/rust-desk/libs/scrap/src/x11/server.rs b/rust-rdp/rust-desk/libs/scrap/src/x11/server.rs new file mode 100644 index 0000000..df7d2ba --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/src/x11/server.rs @@ -0,0 +1,122 @@ +use std::ptr; +use std::rc::Rc; + +use super::ffi::*; +use super::DisplayIter; + +#[derive(Debug)] +pub struct Server { + raw: *mut xcb_connection_t, + screenp: i32, + setup: *const xcb_setup_t, +} + +/* +use std::cell::RefCell; +thread_local! { + static SERVER: RefCell>> = RefCell::new(None); +} +*/ + +impl Server { + pub fn displays(slf: Rc) -> DisplayIter { + unsafe { DisplayIter::new(slf) } + } + + pub fn default() -> Result, Error> { + Ok(Rc::new(Server::connect(ptr::null())?)) + /* + let mut res = Err(Error::from(0)); + SERVER.with(|xdo| { + if let Ok(mut server) = xdo.try_borrow_mut() { + if server.is_some() { + unsafe { + if 0 != xcb_connection_has_error(server.as_ref().unwrap().raw) { + *server = None; + println!("Reset x11 connection"); + } + } + } + if server.is_none() { + println!("New x11 connection"); + match Server::connect(ptr::null()) { + Ok(s) => { + let s = Rc::new(s); + res = Ok(s.clone()); + *server = Some(s); + } + Err(err) => { + res = Err(err); + } + } + } else { + res = Ok(server.as_ref().map(|x| x.clone()).unwrap()); + } + } + }); + res + */ + } + + pub fn connect(addr: *const i8) -> Result { + unsafe { + let mut screenp = 0; + let raw = xcb_connect(addr, &mut screenp); + + let error = xcb_connection_has_error(raw); + if error != 0 { + xcb_disconnect(raw); + Err(Error::from(error)) + } else { + let setup = xcb_get_setup(raw); + Ok(Server { + raw, + screenp, + setup, + }) + } + } + } + + pub fn raw(&self) -> *mut xcb_connection_t { + self.raw + } + pub fn screenp(&self) -> i32 { + self.screenp + } + pub fn setup(&self) -> *const xcb_setup_t { + self.setup + } +} + +impl Drop for Server { + fn drop(&mut self) { + unsafe { + xcb_disconnect(self.raw); + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Error { + Generic, + UnsupportedExtension, + InsufficientMemory, + RequestTooLong, + ParseError, + InvalidScreen, +} + +impl From for Error { + fn from(x: i32) -> Error { + use self::Error::*; + match x { + 2 => UnsupportedExtension, + 3 => InsufficientMemory, + 4 => RequestTooLong, + 5 => ParseError, + 6 => InvalidScreen, + _ => Generic, + } + } +} diff --git a/rust-rdp/rust-desk/libs/scrap/vpx_ffi.h b/rust-rdp/rust-desk/libs/scrap/vpx_ffi.h new file mode 100644 index 0000000..cd44f98 --- /dev/null +++ b/rust-rdp/rust-desk/libs/scrap/vpx_ffi.h @@ -0,0 +1,9 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/rust-rdp/rust-desk/logo-header.svg b/rust-rdp/rust-desk/logo-header.svg new file mode 100644 index 0000000..4a2d299 --- /dev/null +++ b/rust-rdp/rust-desk/logo-header.svg @@ -0,0 +1,151 @@ + +image/svg+xml + + +RUSTDESKYour remote desktop diff --git a/rust-rdp/rust-desk/logo.svg b/rust-rdp/rust-desk/logo.svg new file mode 100644 index 0000000..e87d086 --- /dev/null +++ b/rust-rdp/rust-desk/logo.svg @@ -0,0 +1 @@ + diff --git a/rust-rdp/rust-desk/setup.nsi b/rust-rdp/rust-desk/setup.nsi new file mode 100644 index 0000000..6622176 --- /dev/null +++ b/rust-rdp/rust-desk/setup.nsi @@ -0,0 +1,112 @@ +Unicode true + +#################################################################### +# Includes + +!include nsDialogs.nsh +!include MUI2.nsh +!include x64.nsh +!include LogicLib.nsh + +#################################################################### +# File Info + +!define PRODUCT_NAME "RustDesk" +!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}" +!define COPYRIGHT "Copyright © 2021" +!define VERSION "1.1.6" + +VIProductVersion "${VERSION}.0" +VIAddVersionKey "ProductName" "${PRODUCT_NAME}" +VIAddVersionKey "ProductVersion" "${VERSION}" +VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileVersion" "${VERSION}.0" + +#################################################################### +# Installer Attributes + +Name "${PRODUCT_NAME}" +Outfile "rustdesk-${VERSION}-setup.exe" +Caption "Setup - ${PRODUCT_NAME}" +BrandingText "${PRODUCT_NAME}" + +ShowInstDetails show +RequestExecutionLevel admin +SetOverwrite on + +InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" + +#################################################################### +# Pages + +!define MUI_ICON "src\tray-icon.ico" +!define MUI_ABORTWARNING +!define MUI_LANGDLL_ALLLANGUAGES +!define MUI_FINISHPAGE_SHOWREADME "" +!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED +!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut" +!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut +!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" +!insertmacro MUI_LANGUAGE "SimpChinese" + +#################################################################### +# Sections + +Section "Install" + SetOutPath $INSTDIR + + # Regkeys + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} (x64)" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" '"$INSTDIR\${PRODUCT_NAME}.exe" --uninstall' + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Carriez, Inc." + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "https://www.rustdesk.com/" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://www.rustdesk.com/" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLUpdateInfo" "https://www.rustdesk.com/" + + nsExec::Exec "taskkill /F /IM ${PRODUCT_NAME}.exe" + Sleep 500 ; Give time for process to be completely killed + File "${PRODUCT_NAME}.exe" + + SetShellVarContext all + CreateShortCut "$INSTDIR\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" + CreateShortCut "$SMSTARTUP\${PRODUCT_NAME} Tray.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--tray" + + nsExec::Exec 'sc create ${PRODUCT_NAME} start=auto DisplayName="${PRODUCT_NAME} Service" binPath= "\"$INSTDIR\${PRODUCT_NAME}.exe\" --service"' + nsExec::Exec 'netsh advfirewall firewall add rule name="${PRODUCT_NAME} Service" dir=in action=allow program="$INSTDIR\${PRODUCT_NAME}.exe" enable=yes' + nsExec::Exec 'sc start ${PRODUCT_NAME}' +SectionEnd + +#################################################################### +# Functions + +Function .onInit + # RustDesk is 64-bit only + ${IfNot} ${RunningX64} + MessageBox MB_ICONSTOP "${PRODUCT_NAME} is 64-bit only!" + Quit + ${EndIf} + ${DisableX64FSRedirection} + SetRegView 64 + + !insertmacro MUI_LANGDLL_DISPLAY +FunctionEnd + +Function CreateDesktopShortcut + CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" +FunctionEnd \ No newline at end of file diff --git a/rust-rdp/rust-desk/src/cli.rs b/rust-rdp/rust-desk/src/cli.rs new file mode 100644 index 0000000..1ce1187 --- /dev/null +++ b/rust-rdp/rust-desk/src/cli.rs @@ -0,0 +1,94 @@ +use crate::client::*; +use hbb_common::{ + config::PeerConfig, + log, + message_proto::*, + protobuf::Message as _, + tokio::{self, sync::mpsc}, + Stream, +}; +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] +pub struct Session { + id: String, + lc: Arc>, + sender: mpsc::UnboundedSender, + password: String, +} + +impl Session { + pub fn new(id: &str, sender: mpsc::UnboundedSender) -> Self { + let mut password = "".to_owned(); + if PeerConfig::load(id).password.is_empty() { + password = rpassword::read_password_from_tty(Some("Enter password: ")).unwrap(); + } + let session = Self { + id: id.to_owned(), + sender, + password, + lc: Default::default(), + }; + session + .lc + .write() + .unwrap() + .initialize(id.to_owned(), false, true); + session + } +} + +#[async_trait] +impl Interface for Session { + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + if msgtype == "input-password" { + self.sender + .send(Data::Login((self.password.clone(), true))) + .ok(); + } else if msgtype == "re-input-password" { + log::error!("{}: {}", title, text); + let pass = rpassword::read_password_from_tty(Some("Enter password: ")).unwrap(); + self.sender.send(Data::Login((pass, true))).ok(); + } else if msgtype.contains("error") { + log::error!("{}: {}: {}", msgtype, title, text); + } else { + log::info!("{}: {}: {}", msgtype, title, text); + } + } + + fn handle_login_error(&mut self, err: &str) -> bool { + self.lc.write().unwrap().handle_login_error(err, self) + } + + fn handle_peer_info(&mut self, pi: PeerInfo) { + let username = self.lc.read().unwrap().get_username(&pi); + self.lc.write().unwrap().handle_peer_info(username, pi); + } + + async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + handle_test_delay(t, peer).await; + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn start_one_port_forward(id: String, port: i32, remote_host: String, remote_port: i32) { + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + handler.lc.write().unwrap().port_forward = (remote_host, remote_port); + if let Err(err) = + crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await + { + log::error!("Failed to listen on {}: {}", port, err); + } + log::info!("port forward (:{}) exit", port); +} diff --git a/rust-rdp/rust-desk/src/client.rs b/rust-rdp/rust-desk/src/client.rs new file mode 100644 index 0000000..4c1ea0f --- /dev/null +++ b/rust-rdp/rust-desk/src/client.rs @@ -0,0 +1,1299 @@ +pub use async_trait::async_trait; +#[cfg(not(any(target_os = "android")))] +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, StreamConfig, +}; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + bail, + config::{Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT}, + log, + message_proto::{option_message::BoolOption, *}, + protobuf::Message as _, + rendezvous_proto::*, + socket_client, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, + tokio::time::Duration, + AddrMangle, ResultType, Stream, +}; +use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +use scrap::{Decoder, Image, VideoCodecId}; +use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + net::SocketAddr, + ops::Deref, + sync::{mpsc, Arc, Mutex, RwLock}, +}; +use uuid::Uuid; + +pub const SEC30: Duration = Duration::from_secs(30); + +pub struct Client; + +pub use super::lang::*; + +#[cfg(not(any(target_os = "android")))] +lazy_static::lazy_static! { +static ref AUDIO_HOST: Host = cpal::default_host(); +} + +cfg_if::cfg_if! { + if #[cfg(target_os = "android")] { + +use libc::{c_float, c_int, c_void}; +use std::cell::RefCell; +type Oboe = *mut c_void; +extern "C" { + fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; + fn push_oboe_data(oboe: Oboe, d: *const c_float, n: c_int); + fn destroy_oboe_player(oboe: Oboe); +} + +struct OboePlayer { + raw: Oboe, +} + +impl Default for OboePlayer { + fn default() -> Self { + Self { + raw: std::ptr::null_mut(), + } + } +} + +impl OboePlayer { + fn new(channels: i32, sample_rate: i32) -> Self { + unsafe { + Self { + raw: create_oboe_player(channels, sample_rate), + } + } + } + + fn is_null(&self) -> bool { + self.raw.is_null() + } + + fn push(&mut self, d: &[f32]) { + if self.raw.is_null() { + return; + } + unsafe { + push_oboe_data(self.raw, d.as_ptr(), d.len() as _); + } + } +} + +impl Drop for OboePlayer { + fn drop(&mut self) { + unsafe { + if !self.raw.is_null() { + destroy_oboe_player(self.raw); + } + } + } +} + +} +} + +impl Client { + pub async fn start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> { + match Self::_start(peer, conn_type).await { + Err(err) => { + let err_str = err.to_string(); + if err_str.starts_with("Failed") { + bail!(err_str + ": Please try later"); + } else { + return Err(err); + } + } + Ok(x) => Ok(x), + } + } + + async fn _start(peer: &str, conn_type: ConnType) -> ResultType<(Stream, bool)> { + // to-do: remember the port for each peer, so that we can retry easier + let any_addr = Config::get_any_listen_addr(); + if crate::is_ip(peer) { + return Ok(( + socket_client::connect_tcp( + crate::check_port(peer, RELAY_PORT + 1), + any_addr, + RENDEZVOUS_TIMEOUT, + ) + .await?, + true, + )); + } + let rendezvous_server = crate::get_rendezvous_server(1_000).await; + log::info!("rendezvous server: {}", rendezvous_server); + + let mut socket = + socket_client::connect_tcp(&*rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT).await?; + let my_addr = socket.local_addr(); + let mut pk = Vec::new(); + let mut relay_server = "".to_owned(); + + let start = std::time::Instant::now(); + let mut peer_addr = any_addr; + let mut peer_nat_type = NatType::UNKNOWN_NAT; + let my_nat_type = crate::get_nat_type(100).await; + let mut is_local = false; + for i in 1..=3 { + log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); + let mut msg_out = RendezvousMessage::new(); + use hbb_common::protobuf::ProtobufEnum; + let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT); + msg_out.set_punch_hole_request(PunchHoleRequest { + id: peer.to_owned(), + nat_type: nat_type.into(), + conn_type: conn_type.into(), + ..Default::default() + }); + socket.send(&msg_out).await?; + if let Some(Ok(bytes)) = socket.next_timeout(i * 6000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::punch_hole_response(ph)) => { + if ph.socket_addr.is_empty() { + match ph.failure.enum_value_or_default() { + punch_hole_response::Failure::ID_NOT_EXIST => { + bail!("ID does not exist"); + } + punch_hole_response::Failure::OFFLINE => { + bail!("Remote desktop is offline"); + } + punch_hole_response::Failure::LICENSE_MISMATCH => { + bail!("Key mismatch"); + } + _ => { + if !ph.other_failure.is_empty() { + bail!(ph.other_failure); + } + } + } + } else { + peer_nat_type = ph.get_nat_type(); + is_local = ph.get_is_local(); + pk = ph.pk; + relay_server = ph.relay_server; + peer_addr = AddrMangle::decode(&ph.socket_addr); + log::info!("Hole Punched {} = {}", peer, peer_addr); + break; + } + } + Some(rendezvous_message::Union::relay_response(rr)) => { + log::info!( + "relay requested from peer, time used: {:?}, relay_server: {}", + start.elapsed(), + rr.relay_server + ); + pk = rr.get_pk().into(); + let mut conn = + Self::create_relay(peer, rr.uuid, rr.relay_server, conn_type) + .await?; + Self::secure_connection(peer, pk, &mut conn).await?; + return Ok((conn, false)); + } + _ => { + log::error!("Unexpected protobuf msg received: {:?}", msg_in); + } + } + } else { + log::error!("Non-protobuf message bytes received: {:?}", bytes); + } + } + } + drop(socket); + if peer_addr.port() == 0 { + bail!("Failed to connect via rendezvous server"); + } + let time_used = start.elapsed().as_millis() as u64; + log::info!( + "{} ms used to punch hole, relay_server: {}, {}", + time_used, + relay_server, + if is_local { + "is_local: true".to_owned() + } else { + format!("nat_type: {:?}", peer_nat_type) + } + ); + Self::connect( + my_addr, + peer_addr, + peer, + pk, + &relay_server, + &rendezvous_server, + time_used, + peer_nat_type, + my_nat_type, + is_local, + conn_type, + ) + .await + } + + async fn connect( + local_addr: SocketAddr, + peer: SocketAddr, + peer_id: &str, + pk: Vec, + relay_server: &str, + rendezvous_server: &str, + punch_time_used: u64, + peer_nat_type: NatType, + my_nat_type: i32, + is_local: bool, + conn_type: ConnType, + ) -> ResultType<(Stream, bool)> { + let direct_failures = PeerConfig::load(peer_id).direct_failures; + let mut connect_timeout = 0; + const MIN: u64 = 1000; + if is_local || peer_nat_type == NatType::SYMMETRIC { + connect_timeout = MIN; + } else { + if relay_server.is_empty() { + connect_timeout = CONNECT_TIMEOUT; + } else { + if peer_nat_type == NatType::ASYMMETRIC { + let mut my_nat_type = my_nat_type; + if my_nat_type == NatType::UNKNOWN_NAT as i32 { + my_nat_type = crate::get_nat_type(100).await; + } + if my_nat_type == NatType::ASYMMETRIC as i32 { + connect_timeout = CONNECT_TIMEOUT; + if direct_failures > 0 { + connect_timeout = punch_time_used * 6; + } + } else if my_nat_type == NatType::SYMMETRIC as i32 { + connect_timeout = MIN; + } + } + if connect_timeout == 0 { + let n = if direct_failures > 0 { 3 } else { 6 }; + connect_timeout = punch_time_used * (n as u64); + } + } + if connect_timeout < MIN { + connect_timeout = MIN; + } + } + log::info!("peer address: {}, timeout: {}", peer, connect_timeout); + let start = std::time::Instant::now(); + // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. + let mut conn = socket_client::connect_tcp(peer, local_addr, connect_timeout).await; + let direct = !conn.is_err(); + if conn.is_err() { + if !relay_server.is_empty() { + conn = Self::request_relay( + peer_id, + relay_server.to_owned(), + rendezvous_server, + pk.len() == sign::PUBLICKEYBYTES, + conn_type, + ) + .await; + if conn.is_err() { + bail!( + "Failed to connect via relay server: {}", + conn.err().unwrap() + ); + } + } else { + bail!("Failed to make direct connection to remote desktop"); + } + } + if !relay_server.is_empty() && (direct_failures == 0) != direct { + let mut config = PeerConfig::load(peer_id); + config.direct_failures = if direct { 0 } else { 1 }; + log::info!("direct_failures updated to {}", config.direct_failures); + config.store(peer_id); + } + let mut conn = conn?; + log::info!("{:?} used to establish connection", start.elapsed()); + Self::secure_connection(peer_id, pk, &mut conn).await?; + Ok((conn, direct)) + } + + async fn secure_connection(peer_id: &str, pk: Vec, conn: &mut Stream) -> ResultType<()> { + let mut pk = pk; + let rs_pk = get_rs_pk("OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="); + if !pk.is_empty() && rs_pk.is_some() { + if let Ok(data) = sign::verify(&pk, &rs_pk.unwrap()) { + pk = data; + } else { + log::error!("Handshake failed: invalid public key from rendezvous server"); + pk.clear(); + } + } + if pk.len() != sign::PUBLICKEYBYTES { + // send an empty message out in case server is setting up secure and waiting for first message + conn.send(&Message::new()).await?; + return Ok(()); + } + let mut tmp = [0u8; sign::PUBLICKEYBYTES]; + tmp[..].copy_from_slice(&pk); + let sign_pk = sign::PublicKey(tmp); + match timeout(CONNECT_TIMEOUT, conn.next()).await? { + Some(res) => { + let bytes = res?; + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if let Some(message::Union::signed_id(si)) = msg_in.union { + if let Ok(data) = sign::verify(&si.id, &sign_pk) { + let s = String::from_utf8_lossy(&data); + let mut it = s.split("\0"); + let id = it.next().unwrap_or_default(); + let pk = + base64::decode(it.next().unwrap_or_default()).unwrap_or_default(); + let their_pk_b = if pk.len() == box_::PUBLICKEYBYTES { + let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; + pk_[..].copy_from_slice(&pk); + box_::PublicKey(pk_) + } else { + log::error!( + "Handshake failed: invalid public box key length from peer" + ); + conn.send(&Message::new()).await?; + return Ok(()); + }; + if id == peer_id { + let (our_pk_b, out_sk_b) = box_::gen_keypair(); + let key = secretbox::gen_key(); + let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); + let sealed_key = box_::seal(&key.0, &nonce, &their_pk_b, &out_sk_b); + let mut msg_out = Message::new(); + msg_out.set_public_key(PublicKey { + asymmetric_value: our_pk_b.0.into(), + symmetric_value: sealed_key, + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; + conn.set_key(key); + } else { + log::error!("Handshake failed: sign failure"); + conn.send(&Message::new()).await?; + } + } else { + // fall back to non-secure connection in case pk mismatch + log::info!("pk mismatch, fall back to non-secure"); + let mut msg_out = Message::new(); + msg_out.set_public_key(PublicKey::new()); + timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; + } + } else { + log::error!("Handshake failed: invalid message type"); + conn.send(&Message::new()).await?; + } + } else { + log::error!("Handshake failed: invalid message format"); + conn.send(&Message::new()).await?; + } + } + None => { + bail!("Reset by the peer"); + } + } + Ok(()) + } + + async fn request_relay( + peer: &str, + relay_server: String, + rendezvous_server: &str, + secure: bool, + conn_type: ConnType, + ) -> ResultType { + let any_addr = Config::get_any_listen_addr(); + let mut succeed = false; + let mut uuid = "".to_owned(); + for i in 1..=3 { + // use different socket due to current hbbs implement requiring different nat address for each attempt + let mut socket = + socket_client::connect_tcp(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + + let mut msg_out = RendezvousMessage::new(); + uuid = Uuid::new_v4().to_string(); + log::info!( + "#{} request relay attempt, id: {}, uuid: {}, relay_server: {}, secure: {}", + i, + peer, + uuid, + relay_server, + secure, + ); + msg_out.set_request_relay(RequestRelay { + id: peer.to_owned(), + uuid: uuid.clone(), + relay_server: relay_server.clone(), + secure, + ..Default::default() + }); + socket.send(&msg_out).await?; + if let Some(Ok(bytes)) = socket.next_timeout(CONNECT_TIMEOUT).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + if let Some(rendezvous_message::Union::relay_response(rs)) = msg_in.union { + if !rs.refuse_reason.is_empty() { + bail!(rs.refuse_reason); + } + succeed = true; + break; + } + } + } + } + if !succeed { + bail!("Timeout"); + } + Self::create_relay(peer, uuid, relay_server, conn_type).await + } + + async fn create_relay( + peer: &str, + uuid: String, + relay_server: String, + conn_type: ConnType, + ) -> ResultType { + let mut conn = socket_client::connect_tcp( + crate::check_port(relay_server, RELAY_PORT), + Config::get_any_listen_addr(), + CONNECT_TIMEOUT, + ) + .await + .with_context(|| "Failed to connect to relay server")?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_request_relay(RequestRelay { + id: peer.to_owned(), + uuid, + conn_type: conn_type.into(), + ..Default::default() + }); + conn.send(&msg_out).await?; + Ok(conn) + } +} + +#[derive(Default)] +pub struct AudioHandler { + audio_decoder: Option<(AudioDecoder, Vec)>, + #[cfg(any(target_os = "android"))] + oboe: RefCell, + #[cfg(not(any(target_os = "android")))] + audio_buffer: Arc>>, + sample_rate: (u32, u32), + #[cfg(not(any(target_os = "android")))] + audio_stream: Option>, + channels: u16, +} + +impl AudioHandler { + #[cfg(any(target_os = "android"))] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + self.sample_rate = (format0.sample_rate, format0.sample_rate); + Ok(()) + } + + #[cfg(not(any(target_os = "android")))] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + let device = AUDIO_HOST + .default_output_device() + .with_context(|| "Failed to get default output device")?; + log::info!( + "Using default output device: \"{}\"", + device.name().unwrap_or("".to_owned()) + ); + let config = device.default_output_config().map_err(|e| anyhow!(e))?; + let sample_format = config.sample_format(); + log::info!("Default output format: {:?}", config); + log::info!("Remote input format: {:?}", format0); + let mut config: StreamConfig = config.into(); + config.channels = format0.channels as _; + match sample_format { + cpal::SampleFormat::F32 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::I16 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::U16 => self.build_output_stream::(&config, &device)?, + } + self.sample_rate = (format0.sample_rate, config.sample_rate.0); + Ok(()) + } + + pub fn handle_format(&mut self, f: AudioFormat) { + match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { + Ok(d) => { + let buffer = vec![0.; f.sample_rate as usize * f.channels as usize]; + self.audio_decoder = Some((d, buffer)); + self.channels = f.channels as _; + allow_err!(self.start_audio(f)); + } + Err(err) => { + log::error!("Failed to create audio decoder: {}", err); + } + } + } + + pub fn handle_frame(&mut self, frame: AudioFrame) { + #[cfg(not(any(target_os = "android")))] + if self.audio_stream.is_none() { + return; + } + let sample_rate0 = self.sample_rate.0; + let sample_rate = self.sample_rate.1; + let channels = self.channels; + cfg_if::cfg_if! { + if #[cfg(not(target_os = "android"))] { + let audio_buffer = self.audio_buffer.clone(); + // avoiding memory overflow if audio_buffer consumer side has problem + if audio_buffer.lock().unwrap().len() as u32 > sample_rate * 120 { + *audio_buffer.lock().unwrap() = Default::default(); + } + } else { + if self.oboe.borrow().is_null() { + self.oboe = RefCell::new(OboePlayer::new( + channels as _, + sample_rate0 as _, + )); + } + let mut oboe = self.oboe.borrow_mut(); + } + } + self.audio_decoder.as_mut().map(|(d, buffer)| { + if let Ok(n) = d.decode_float(&frame.data, buffer, false) { + let n = n * (channels as usize); + #[cfg(not(any(target_os = "android")))] + { + if sample_rate != sample_rate0 { + let buffer = crate::resample_channels( + &buffer[0..n], + sample_rate0, + sample_rate, + channels, + ); + audio_buffer.lock().unwrap().extend(buffer); + } else { + audio_buffer + .lock() + .unwrap() + .extend(buffer[0..n].iter().cloned()); + } + } + #[cfg(any(target_os = "android"))] + { + oboe.push(&buffer[0..n]); + } + } + }); + } + + #[cfg(not(any(target_os = "android")))] + fn build_output_stream( + &mut self, + config: &StreamConfig, + device: &Device, + ) -> ResultType<()> { + let err_fn = move |err| { + // too many errors, will improve later + log::trace!("an error occurred on stream: {}", err); + }; + let audio_buffer = self.audio_buffer.clone(); + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &_| { + let mut lock = audio_buffer.lock().unwrap(); + let mut n = data.len(); + if lock.len() < n { + n = lock.len(); + } + let mut input = lock.drain(0..n); + for sample in data.iter_mut() { + *sample = match input.next() { + Some(x) => T::from(&x), + _ => T::from(&0.), + }; + } + }, + err_fn, + )?; + stream.play()?; + self.audio_stream = Some(Box::new(stream)); + Ok(()) + } +} + +pub struct VideoHandler { + decoder: Decoder, + pub rgb: Vec, +} + +impl VideoHandler { + pub fn new() -> Self { + VideoHandler { + decoder: Decoder::new(VideoCodecId::VP9, (num_cpus::get() / 2) as _).unwrap(), + rgb: Default::default(), + } + } + + pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { + let mut last_frame = Image::new(); + for vp9 in vp9s.frames.iter() { + for frame in self.decoder.decode(&vp9.data)? { + drop(last_frame); + last_frame = frame; + } + } + for frame in self.decoder.flush()? { + drop(last_frame); + last_frame = frame; + } + if last_frame.is_null() { + Ok(false) + } else { + last_frame.rgb(1, true, &mut self.rgb); + Ok(true) + } + } + + pub fn reset(&mut self) { + self.decoder = Decoder::new(VideoCodecId::VP9, 1).unwrap(); + } +} + +#[derive(Default)] +pub struct LoginConfigHandler { + id: String, + is_file_transfer: bool, + is_port_forward: bool, + hash: Hash, + password: Vec, // remember password for reconnect + pub remember: bool, + config: PeerConfig, + pub port_forward: (String, i32), + pub version: i64, +} + +impl Deref for LoginConfigHandler { + type Target = PeerConfig; + + fn deref(&self) -> &Self::Target { + &self.config + } +} + +#[inline] +pub fn load_config(id: &str) -> PeerConfig { + PeerConfig::load(id) +} + +impl LoginConfigHandler { + pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { + self.id = id; + self.is_file_transfer = is_file_transfer; + self.is_port_forward = is_port_forward; + let config = self.load_config(); + self.remember = !config.password.is_empty(); + self.config = config; + } + + fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + pub fn save_config(&mut self, config: PeerConfig) { + config.store(&self.id); + self.config = config; + } + + pub fn set_option(&mut self, k: String, v: String) { + let mut config = self.load_config(); + config.options.insert(k, v); + self.save_config(config); + } + + pub fn save_view_style(&mut self, value: String) { + let mut config = self.load_config(); + config.view_style = value; + self.save_config(config); + } + + pub fn toggle_option(&mut self, name: String) -> Option { + let mut option = OptionMessage::default(); + let mut config = self.load_config(); + if name == "show-remote-cursor" { + config.show_remote_cursor = !config.show_remote_cursor; + option.show_remote_cursor = (if config.show_remote_cursor { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "disable-audio" { + config.disable_audio = !config.disable_audio; + option.disable_audio = (if config.disable_audio { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "disable-clipboard" { + config.disable_clipboard = !config.disable_clipboard; + option.disable_clipboard = (if config.disable_clipboard { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "lock-after-session-end" { + config.lock_after_session_end = !config.lock_after_session_end; + option.lock_after_session_end = (if config.lock_after_session_end { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "privacy-mode" { + config.privacy_mode = !config.privacy_mode; + option.privacy_mode = (if config.privacy_mode { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "block-input" { + option.block_input = BoolOption::Yes.into(); + } else if name == "unblock-input" { + option.block_input = BoolOption::No.into(); + } else { + let v = self.options.get(&name).is_some(); + if v { + self.config.options.remove(&name); + } else { + self.config.options.insert(name, "Y".to_owned()); + } + self.config.store(&self.id); + return None; + } + if !name.contains("block-input") { + self.save_config(config); + } + let mut misc = Misc::new(); + misc.set_option(option); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + Some(msg_out) + } + + fn get_option_message(&self, ignore_default: bool) -> Option { + if self.is_port_forward || self.is_file_transfer { + return None; + } + let mut n = 0; + let mut msg = OptionMessage::new(); + let q = self.image_quality.clone(); + if let Some(q) = self.get_image_quality_enum(&q, ignore_default) { + msg.image_quality = q.into(); + n += 1; + } else if q == "custom" { + let config = PeerConfig::load(&self.id); + let mut it = config.custom_image_quality.iter(); + let bitrate = it.next(); + let quantizer = it.next(); + if let Some(bitrate) = bitrate { + if let Some(quantizer) = quantizer { + msg.custom_image_quality = bitrate << 8 | quantizer; + n += 1; + } + } + } + if self.get_toggle_option("show-remote-cursor") { + msg.show_remote_cursor = BoolOption::Yes.into(); + n += 1; + } + if self.get_toggle_option("lock-after-session-end") { + msg.lock_after_session_end = BoolOption::Yes.into(); + n += 1; + } + if self.get_toggle_option("privacy-mode") { + msg.privacy_mode = BoolOption::Yes.into(); + n += 1; + } + if self.get_toggle_option("disable-audio") { + msg.disable_audio = BoolOption::Yes.into(); + n += 1; + } + if self.get_toggle_option("disable-clipboard") { + msg.disable_clipboard = BoolOption::Yes.into(); + n += 1; + } + if n > 0 { + Some(msg) + } else { + None + } + } + + fn get_image_quality_enum(&self, q: &str, ignore_default: bool) -> Option { + if q == "low" { + Some(ImageQuality::Low) + } else if q == "best" { + Some(ImageQuality::Best) + } else if q == "balanced" { + if ignore_default { + None + } else { + Some(ImageQuality::Balanced) + } + } else { + None + } + } + + pub fn get_toggle_option(&self, name: &str) -> bool { + if name == "show-remote-cursor" { + self.config.show_remote_cursor + } else if name == "lock-after-session-end" { + self.config.lock_after_session_end + } else if name == "privacy-mode" { + self.config.privacy_mode + } else if name == "disable-audio" { + self.config.disable_audio + } else if name == "disable-clipboard" { + self.config.disable_clipboard + } else { + !self.get_option(name).is_empty() + } + } + + pub fn refresh() -> Message { + let mut misc = Misc::new(); + misc.set_refresh_video(true); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + + pub fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) -> Message { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_image_quality: bitrate << 8 | quantizer, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + let mut config = self.load_config(); + config.image_quality = "custom".to_owned(); + config.custom_image_quality = vec![bitrate, quantizer]; + self.save_config(config); + msg_out + } + + pub fn save_image_quality(&mut self, value: String) -> Option { + let mut res = None; + if let Some(q) = self.get_image_quality_enum(&value, false) { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + image_quality: q.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + res = Some(msg_out); + } + let mut config = self.load_config(); + config.image_quality = value; + self.save_config(config); + res + } + + pub fn get_option(&self, k: &str) -> String { + if let Some(v) = self.config.options.get(k) { + v.clone() + } else { + "".to_owned() + } + } + + pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { + if err == "Wrong Password" { + self.password = Default::default(); + interface.msgbox("re-input-password", err, "Do you want to enter again?"); + true + } else { + interface.msgbox("error", "Login Error", err); + false + } + } + + pub fn get_username(&self, pi: &PeerInfo) -> String { + return if pi.username.is_empty() { + self.info.username.clone() + } else { + pi.username.clone() + }; + } + + pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { + if !pi.version.is_empty() { + self.version = hbb_common::get_version_number(&pi.version); + } + let serde = PeerInfoSerde { + username, + hostname: pi.hostname.clone(), + platform: pi.platform.clone(), + }; + let mut config = self.load_config(); + config.info = serde; + let password = self.password.clone(); + let password0 = config.password.clone(); + let remember = self.remember; + if remember { + if !password.is_empty() && password != password0 { + config.password = password; + log::debug!("remember password of {}", self.id); + } + } else { + if !password0.is_empty() { + config.password = Default::default(); + log::debug!("remove password of {}", self.id); + } + } + // no matter if change, for update file time + self.save_config(config); + } + + fn create_login_msg(&self, password: Vec) -> Message { + #[cfg(any(target_os = "android", target_os = "ios"))] + let my_id = crate::common::MOBILE_INFO1.lock().unwrap().clone(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let my_id = Config::get_id(); + let mut lr = LoginRequest { + username: self.id.clone(), + password, + my_id, + my_name: crate::username(), + option: self.get_option_message(true).into(), + ..Default::default() + }; + if self.is_file_transfer { + lr.set_file_transfer(FileTransfer { + dir: self.get_option("remote_dir"), + show_hidden: !self.get_option("remote_show_hidden").is_empty(), + ..Default::default() + }); + } else if self.is_port_forward { + lr.set_port_forward(PortForward { + host: self.port_forward.0.clone(), + port: self.port_forward.1, + ..Default::default() + }); + } + let mut msg_out = Message::new(); + msg_out.set_login_request(lr); + msg_out + } +} + +pub enum MediaData { + VideoFrame(VideoFrame), + AudioFrame(AudioFrame), + AudioFormat(AudioFormat), + Reset, +} + +pub type MediaSender = mpsc::Sender; + +pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) +where + F: 'static + FnMut(&[u8]) + Send, +{ + let (video_sender, video_receiver) = mpsc::channel::(); + let (audio_sender, audio_receiver) = mpsc::channel::(); + let mut video_callback = video_callback; + + std::thread::spawn(move || { + let mut video_handler = VideoHandler::new(); + loop { + if let Ok(data) = video_receiver.recv() { + match data { + MediaData::VideoFrame(vf) => { + if let Some(video_frame::Union::vp9s(vp9s)) = &vf.union { + if let Ok(true) = video_handler.handle_vp9s(vp9s) { + video_callback(&video_handler.rgb); + } + } + } + MediaData::Reset => { + video_handler.reset(); + } + _ => {} + } + } else { + break; + } + } + log::info!("Video decoder loop exits"); + }); + std::thread::spawn(move || { + let mut audio_handler = AudioHandler::default(); + loop { + if let Ok(data) = audio_receiver.recv() { + match data { + MediaData::AudioFrame(af) => { + audio_handler.handle_frame(af); + } + MediaData::AudioFormat(f) => { + audio_handler.handle_format(f); + } + _ => {} + } + } else { + break; + } + } + log::info!("Audio decoder loop exits"); + }); + return (video_sender, audio_sender); +} + +pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { + if !t.from_client { + let mut msg_out = Message::new(); + msg_out.set_test_delay(t); + allow_err!(peer.send(&msg_out).await); + } +} + +pub async fn handle_hash( + lc: Arc>, + hash: Hash, + interface: &impl Interface, + peer: &mut Stream, +) { + let mut password = lc.read().unwrap().password.clone(); + if password.is_empty() { + password = lc.read().unwrap().config.password.clone(); + } + if password.is_empty() { + // login without password, the remote side can click accept + send_login(lc.clone(), Vec::new(), peer).await; + interface.msgbox("input-password", "Password Required", ""); + } else { + let mut hasher = Sha256::new(); + hasher.update(&password); + hasher.update(&hash.challenge); + send_login(lc.clone(), hasher.finalize()[..].into(), peer).await; + } + lc.write().unwrap().hash = hash; +} + +async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { + let msg_out = lc.read().unwrap().create_login_msg(password); + allow_err!(peer.send(&msg_out).await); +} + +pub async fn handle_login_from_ui( + lc: Arc>, + password: String, + remember: bool, + peer: &mut Stream, +) { + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&lc.read().unwrap().hash.salt); + let res = hasher.finalize(); + lc.write().unwrap().remember = remember; + lc.write().unwrap().password = res[..].into(); + let mut hasher2 = Sha256::new(); + hasher2.update(&res[..]); + hasher2.update(&lc.read().unwrap().hash.challenge); + send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; +} + +#[async_trait] +pub trait Interface: Send + Clone + 'static + Sized { + fn msgbox(&self, msgtype: &str, title: &str, text: &str); + fn handle_login_error(&mut self, err: &str) -> bool; + fn handle_peer_info(&mut self, pi: PeerInfo); + async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream); + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); +} + +#[derive(Clone)] +pub enum Data { + Close, + Login((String, bool)), + Message(Message), + SendFiles((i32, String, String, bool, bool)), + RemoveDirAll((i32, String, bool)), + ConfirmDeleteFiles((i32, i32)), + SetNoConfirm(i32), + RemoveDir((i32, String)), + RemoveFile((i32, String, i32, bool)), + CreateDir((i32, String, bool)), + CancelJob(i32), + RemovePortForward(i32), + AddPortForward((i32, String, i32)), + NewRDP, +} + +#[derive(Clone)] +pub enum Key { + ControlKey(ControlKey), + Chr(u32), + _Raw(u32), +} + +lazy_static::lazy_static! { + pub static ref KEY_MAP: HashMap<&'static str, Key> = + [ + ("VK_A", Key::Chr('a' as _)), + ("VK_B", Key::Chr('b' as _)), + ("VK_C", Key::Chr('c' as _)), + ("VK_D", Key::Chr('d' as _)), + ("VK_E", Key::Chr('e' as _)), + ("VK_F", Key::Chr('f' as _)), + ("VK_G", Key::Chr('g' as _)), + ("VK_H", Key::Chr('h' as _)), + ("VK_I", Key::Chr('i' as _)), + ("VK_J", Key::Chr('j' as _)), + ("VK_K", Key::Chr('k' as _)), + ("VK_L", Key::Chr('l' as _)), + ("VK_M", Key::Chr('m' as _)), + ("VK_N", Key::Chr('n' as _)), + ("VK_O", Key::Chr('o' as _)), + ("VK_P", Key::Chr('p' as _)), + ("VK_Q", Key::Chr('q' as _)), + ("VK_R", Key::Chr('r' as _)), + ("VK_S", Key::Chr('s' as _)), + ("VK_T", Key::Chr('t' as _)), + ("VK_U", Key::Chr('u' as _)), + ("VK_V", Key::Chr('v' as _)), + ("VK_W", Key::Chr('w' as _)), + ("VK_X", Key::Chr('x' as _)), + ("VK_Y", Key::Chr('y' as _)), + ("VK_Z", Key::Chr('z' as _)), + ("VK_0", Key::Chr('0' as _)), + ("VK_1", Key::Chr('1' as _)), + ("VK_2", Key::Chr('2' as _)), + ("VK_3", Key::Chr('3' as _)), + ("VK_4", Key::Chr('4' as _)), + ("VK_5", Key::Chr('5' as _)), + ("VK_6", Key::Chr('6' as _)), + ("VK_7", Key::Chr('7' as _)), + ("VK_8", Key::Chr('8' as _)), + ("VK_9", Key::Chr('9' as _)), + ("VK_COMMA", Key::Chr(',' as _)), + ("VK_SLASH", Key::Chr('/' as _)), + ("VK_SEMICOLON", Key::Chr(';' as _)), + ("VK_QUOTE", Key::Chr('\'' as _)), + ("VK_LBRACKET", Key::Chr('[' as _)), + ("VK_RBRACKET", Key::Chr(']' as _)), + ("VK_BACKSLASH", Key::Chr('\\' as _)), + ("VK_MINUS", Key::Chr('-' as _)), + ("VK_PLUS", Key::Chr('=' as _)), // it is =, but sciter return VK_PLUS + ("VK_DIVIDE", Key::ControlKey(ControlKey::Divide)), // numpad + ("VK_MULTIPLY", Key::ControlKey(ControlKey::Multiply)), // numpad + ("VK_SUBTRACT", Key::ControlKey(ControlKey::Subtract)), // numpad + ("VK_ADD", Key::ControlKey(ControlKey::Add)), // numpad + ("VK_DECIMAL", Key::ControlKey(ControlKey::Decimal)), // numpad + ("VK_F1", Key::ControlKey(ControlKey::F1)), + ("VK_F2", Key::ControlKey(ControlKey::F2)), + ("VK_F3", Key::ControlKey(ControlKey::F3)), + ("VK_F4", Key::ControlKey(ControlKey::F4)), + ("VK_F5", Key::ControlKey(ControlKey::F5)), + ("VK_F6", Key::ControlKey(ControlKey::F6)), + ("VK_F7", Key::ControlKey(ControlKey::F7)), + ("VK_F8", Key::ControlKey(ControlKey::F8)), + ("VK_F9", Key::ControlKey(ControlKey::F9)), + ("VK_F10", Key::ControlKey(ControlKey::F10)), + ("VK_F11", Key::ControlKey(ControlKey::F11)), + ("VK_F12", Key::ControlKey(ControlKey::F12)), + ("VK_ENTER", Key::ControlKey(ControlKey::Return)), + ("VK_CANCEL", Key::ControlKey(ControlKey::Cancel)), + ("VK_BACK", Key::ControlKey(ControlKey::Backspace)), + ("VK_TAB", Key::ControlKey(ControlKey::Tab)), + ("VK_CLEAR", Key::ControlKey(ControlKey::Clear)), + ("VK_RETURN", Key::ControlKey(ControlKey::Return)), + ("VK_SHIFT", Key::ControlKey(ControlKey::Shift)), + ("VK_CONTROL", Key::ControlKey(ControlKey::Control)), + ("VK_MENU", Key::ControlKey(ControlKey::Alt)), + ("VK_PAUSE", Key::ControlKey(ControlKey::Pause)), + ("VK_CAPITAL", Key::ControlKey(ControlKey::CapsLock)), + ("VK_KANA", Key::ControlKey(ControlKey::Kana)), + ("VK_HANGUL", Key::ControlKey(ControlKey::Hangul)), + ("VK_JUNJA", Key::ControlKey(ControlKey::Junja)), + ("VK_FINAL", Key::ControlKey(ControlKey::Final)), + ("VK_HANJA", Key::ControlKey(ControlKey::Hanja)), + ("VK_KANJI", Key::ControlKey(ControlKey::Kanji)), + ("VK_ESCAPE", Key::ControlKey(ControlKey::Escape)), + ("VK_CONVERT", Key::ControlKey(ControlKey::Convert)), + ("VK_SPACE", Key::ControlKey(ControlKey::Space)), + ("VK_PRIOR", Key::ControlKey(ControlKey::PageUp)), + ("VK_NEXT", Key::ControlKey(ControlKey::PageDown)), + ("VK_END", Key::ControlKey(ControlKey::End)), + ("VK_HOME", Key::ControlKey(ControlKey::Home)), + ("VK_LEFT", Key::ControlKey(ControlKey::LeftArrow)), + ("VK_UP", Key::ControlKey(ControlKey::UpArrow)), + ("VK_RIGHT", Key::ControlKey(ControlKey::RightArrow)), + ("VK_DOWN", Key::ControlKey(ControlKey::DownArrow)), + ("VK_SELECT", Key::ControlKey(ControlKey::Select)), + ("VK_PRINT", Key::ControlKey(ControlKey::Print)), + ("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)), + ("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)), + ("VK_INSERT", Key::ControlKey(ControlKey::Insert)), + ("VK_DELETE", Key::ControlKey(ControlKey::Delete)), + ("VK_HELP", Key::ControlKey(ControlKey::Help)), + ("VK_SLEEP", Key::ControlKey(ControlKey::Sleep)), + ("VK_SEPARATOR", Key::ControlKey(ControlKey::Separator)), + ("VK_NUMPAD0", Key::ControlKey(ControlKey::Numpad0)), + ("VK_NUMPAD1", Key::ControlKey(ControlKey::Numpad1)), + ("VK_NUMPAD2", Key::ControlKey(ControlKey::Numpad2)), + ("VK_NUMPAD3", Key::ControlKey(ControlKey::Numpad3)), + ("VK_NUMPAD4", Key::ControlKey(ControlKey::Numpad4)), + ("VK_NUMPAD5", Key::ControlKey(ControlKey::Numpad5)), + ("VK_NUMPAD6", Key::ControlKey(ControlKey::Numpad6)), + ("VK_NUMPAD7", Key::ControlKey(ControlKey::Numpad7)), + ("VK_NUMPAD8", Key::ControlKey(ControlKey::Numpad8)), + ("VK_NUMPAD9", Key::ControlKey(ControlKey::Numpad9)), + ("Apps", Key::ControlKey(ControlKey::Apps)), + ("Meta", Key::ControlKey(ControlKey::Meta)), + ("RAlt", Key::ControlKey(ControlKey::RAlt)), + ("RWin", Key::ControlKey(ControlKey::RWin)), + ("RControl", Key::ControlKey(ControlKey::RControl)), + ("RShift", Key::ControlKey(ControlKey::RShift)), + ("CTRL_ALT_DEL", Key::ControlKey(ControlKey::CtrlAltDel)), + ("LOCK_SCREEN", Key::ControlKey(ControlKey::LockScreen)), + ].iter().cloned().collect(); +} + +#[inline] +pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { + msgtype == "error" + && title == "Connection Error" + && !text.to_lowercase().contains("offline") + && !text.to_lowercase().contains("exist") + && !text.to_lowercase().contains("handshake") + && !text.to_lowercase().contains("failed") + && !text.to_lowercase().contains("resolve") + && !text.to_lowercase().contains("mismatch") + && !text.to_lowercase().contains("manually") +} + +#[inline] +fn get_rs_pk(str_base64: &str) -> Option { + if let Ok(pk) = base64::decode(str_base64) { + if pk.len() == sign::PUBLICKEYBYTES { + let mut tmp = [0u8; sign::PUBLICKEYBYTES]; + tmp[..].copy_from_slice(&pk); + return Some(sign::PublicKey(tmp)); + } + } + None +} diff --git a/rust-rdp/rust-desk/src/common.rs b/rust-rdp/rust-desk/src/common.rs new file mode 100644 index 0000000..16da93b --- /dev/null +++ b/rust-rdp/rust-desk/src/common.rs @@ -0,0 +1,462 @@ +pub use arboard::Clipboard as ClipboardContext; +use hbb_common::{ + allow_err, + anyhow::bail, + compress::{compress as compress_func, decompress}, + config::{Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, + get_version_number, log, + message_proto::*, + protobuf::Message as _, + protobuf::ProtobufEnum, + rendezvous_proto::*, + sleep, socket_client, tokio, ResultType, +}; +#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use std::sync::{Arc, Mutex}; + +pub const CLIPBOARD_NAME: &'static str = "clipboard"; +pub const CLIPBOARD_INTERVAL: u64 = 333; + +lazy_static::lazy_static! { + pub static ref CONTENT: Arc> = Default::default(); + pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref MOBILE_INFO1: Arc> = Default::default(); + pub static ref MOBILE_INFO2: Arc> = Default::default(); +} + +#[inline] +pub fn valid_for_numlock(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + let v = ck.value(); + (v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value()) + || v == ControlKey::Decimal.value() + } else { + false + } +} + +pub fn create_clipboard_msg(content: String) -> Message { + let bytes = content.into_bytes(); + let compressed = compress_func(&bytes, COMPRESS_LEVEL); + let compress = compressed.len() < bytes.len(); + let content = if compress { compressed } else { bytes }; + let mut msg = Message::new(); + msg.set_clipboard(Clipboard { + compress, + content, + ..Default::default() + }); + msg +} + +pub fn check_clipboard( + ctx: &mut ClipboardContext, + old: Option<&Arc>>, +) -> Option { + let side = if old.is_none() { "host" } else { "client" }; + let old = if let Some(old) = old { old } else { &CONTENT }; + if let Ok(content) = ctx.get_text() { + if content.len() < 2_000_000 && !content.is_empty() { + let changed = content != *old.lock().unwrap(); + if changed { + log::info!("{} update found on {}", CLIPBOARD_NAME, side); + *old.lock().unwrap() = content.clone(); + return Some(create_clipboard_msg(content)); + } + } + } + None +} + +pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { + let content = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content + }; + if let Ok(content) = String::from_utf8(content) { + match ClipboardContext::new() { + Ok(mut ctx) => { + let side = if old.is_none() { "host" } else { "client" }; + let old = if let Some(old) = old { old } else { &CONTENT }; + *old.lock().unwrap() = content.clone(); + if !content.is_empty() { + // empty content make ctx.set_text crash + allow_err!(ctx.set_text(content)); + } + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } + Err(err) => { + log::error!("Failed to create clipboard context: {}", err); + } + } + } +} + +#[cfg(feature = "use_rubato")] +pub fn resample_channels( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use rubato::{ + InterpolationParameters, InterpolationType, Resampler, SincFixedIn, WindowFunction, + }; + let params = InterpolationParameters { + sinc_len: 256, + f_cutoff: 0.95, + interpolation: InterpolationType::Nearest, + oversampling_factor: 160, + window: WindowFunction::BlackmanHarris2, + }; + let mut resampler = SincFixedIn::::new( + sample_rate as f64 / sample_rate0 as f64, + params, + data.len() / (channels as usize), + channels as _, + ); + let mut waves_in = Vec::new(); + if channels == 2 { + waves_in.push( + data.iter() + .step_by(2) + .map(|x| *x as f64) + .collect::>(), + ); + waves_in.push( + data.iter() + .skip(1) + .step_by(2) + .map(|x| *x as f64) + .collect::>(), + ); + } else { + waves_in.push(data.iter().map(|x| *x as f64).collect::>()); + } + if let Ok(x) = resampler.process(&waves_in) { + if x.is_empty() { + Vec::new() + } else if x.len() == 2 { + x[0].chunks(1) + .zip(x[1].chunks(1)) + .flat_map(|(a, b)| a.into_iter().chain(b)) + .map(|x| *x as f32) + .collect() + } else { + x[0].iter().map(|x| *x as f32).collect() + } + } else { + Vec::new() + } +} + +#[cfg(feature = "use_dasp")] +pub fn resample_channels( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use dasp::{interpolate::linear::Linear, signal, Signal}; + let n = data.len() / (channels as usize); + let n = n * sample_rate as usize / sample_rate0 as usize; + if channels == 2 { + let mut source = signal::from_interleaved_samples_iter::<_, [_; 2]>(data.iter().cloned()); + let a = source.next(); + let b = source.next(); + let interp = Linear::new(a, b); + let mut data = Vec::with_capacity(n << 1); + for x in source + .from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _) + .take(n) + { + data.push(x[0]); + data.push(x[1]); + } + data + } else { + let mut source = signal::from_iter(data.iter().cloned()); + let a = source.next(); + let b = source.next(); + let interp = Linear::new(a, b); + source + .from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _) + .take(n) + .collect() + } +} + +#[cfg(feature = "use_samplerate")] +pub fn resample_channels( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use samplerate::{convert, ConverterType}; + convert( + sample_rate0 as _, + sample_rate as _, + channels as _, + ConverterType::SincBestQuality, + data, + ) + .unwrap_or_default() +} + +pub fn test_nat_type() { + std::thread::spawn(move || loop { + match test_nat_type_() { + Ok(true) => break, + Err(err) => { + log::error!("test nat: {}", err); + } + _ => {} + } + if Config::get_nat_type() != 0 { + break; + } + std::thread::sleep(std::time::Duration::from_secs(12)); + }); +} + +#[tokio::main(flavor = "current_thread")] +async fn test_nat_type_() -> ResultType { + log::info!("Testing nat ..."); + let is_direct = crate::ipc::get_socks_async(1_000).await.is_none(); // sync socks BTW + if !is_direct { + Config::set_nat_type(NatType::SYMMETRIC as _); + return Ok(true); + } + let start = std::time::Instant::now(); + let rendezvous_server = get_rendezvous_server(1_000).await; + let server1 = rendezvous_server; + let tmp: Vec<&str> = server1.split(":").collect(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", server1); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", server1); + } + let server2 = format!("{}:{}", tmp[0], port - 1); + let mut msg_out = RendezvousMessage::new(); + let serial = Config::get_serial(); + msg_out.set_test_nat_request(TestNatRequest { + serial, + ..Default::default() + }); + let mut port1 = 0; + let mut port2 = 0; + let server1 = socket_client::get_target_addr(&server1)?; + let server2 = socket_client::get_target_addr(&server2)?; + let mut addr = Config::get_any_listen_addr(); + for i in 0..2 { + let mut socket = socket_client::connect_tcp( + if i == 0 { + server1.clone() + } else { + server2.clone() + }, + addr, + RENDEZVOUS_TIMEOUT, + ) + .await?; + addr = socket.local_addr(); + socket.send(&msg_out).await?; + if let Some(Ok(bytes)) = socket.next_timeout(3000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + if let Some(rendezvous_message::Union::test_nat_response(tnr)) = msg_in.union { + if i == 0 { + port1 = tnr.port; + } else { + port2 = tnr.port; + } + if let Some(cu) = tnr.cu.as_ref() { + Config::set_option( + "rendezvous-servers".to_owned(), + cu.rendezvous_servers.join(","), + ); + Config::set_serial(cu.serial); + } + } + } + } else { + break; + } + } + let ok = port1 > 0 && port2 > 0; + if ok { + let t = if port1 == port2 { + NatType::ASYMMETRIC + } else { + NatType::SYMMETRIC + }; + Config::set_nat_type(t as _); + log::info!("Tested nat type: {:?} in {:?}", t, start.elapsed()); + } + Ok(ok) +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub async fn get_rendezvous_server(_ms_timeout: u64) -> String { + Config::get_rendezvous_server() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub async fn get_rendezvous_server(ms_timeout: u64) -> String { + crate::ipc::get_rendezvous_server(ms_timeout).await +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub async fn get_nat_type(_ms_timeout: u64) -> i32 { + Config::get_nat_type() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub async fn get_nat_type(ms_timeout: u64) -> i32 { + crate::ipc::get_nat_type(ms_timeout).await +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +#[tokio::main(flavor = "current_thread")] +async fn test_rendezvous_server_() { + let servers = Config::get_rendezvous_servers(); + hbb_common::config::ONLINE.lock().unwrap().clear(); + let mut futs = Vec::new(); + for host in servers { + futs.push(tokio::spawn(async move { + let tm = std::time::Instant::now(); + if socket_client::connect_tcp( + crate::check_port(&host, RENDEZVOUS_PORT), + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await + .is_ok() + { + let elapsed = tm.elapsed().as_micros(); + Config::update_latency(&host, elapsed as _); + } else { + Config::update_latency(&host, -1); + } + })); + } + join_all(futs).await; +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +pub fn test_rendezvous_server() { + std::thread::spawn(test_rendezvous_server_); +} + +#[inline] +pub fn get_time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) as _ +} + +pub fn run_me>(args: Vec) -> std::io::Result { + let cmd = std::env::current_exe()?; + return std::process::Command::new(cmd).args(&args).spawn(); +} + +pub fn username() -> String { + // fix bug of whoami + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return whoami::username().trim_end_matches('\0').to_owned(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return MOBILE_INFO2.lock().unwrap().clone(); +} + +#[inline] +pub fn check_port(host: T, port: i32) -> String { + let host = host.to_string(); + if !host.contains(":") { + return format!("{}:{}", host, port); + } + return host; +} + +pub const POSTFIX_SERVICE: &'static str = "_service"; + +#[inline] +pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + ck.value() == key.value() + } else { + false + } +} + +#[inline] +pub fn is_modifier(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + let v = ck.value(); + v == ControlKey::Alt.value() + || v == ControlKey::Shift.value() + || v == ControlKey::Control.value() + || v == ControlKey::Meta.value() + || v == ControlKey::RAlt.value() + || v == ControlKey::RShift.value() + || v == ControlKey::RControl.value() + || v == ControlKey::RWin.value() + } else { + false + } +} + +pub fn check_software_update() { + std::thread::spawn(move || allow_err!(_check_software_update())); +} + +#[tokio::main(flavor = "current_thread")] +async fn _check_software_update() -> hbb_common::ResultType<()> { + sleep(3.).await; + + let rendezvous_server = socket_client::get_target_addr(&get_rendezvous_server(1_000).await)?; + let mut socket = + socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?; + + let mut msg_out = RendezvousMessage::new(); + msg_out.set_software_update(SoftwareUpdate { + url: crate::VERSION.to_owned(), + ..Default::default() + }); + socket.send(&msg_out, rendezvous_server).await?; + use hbb_common::protobuf::Message; + if let Some(Ok((bytes, _))) = socket.next_timeout(30_000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + if let Some(rendezvous_message::Union::software_update(su)) = msg_in.union { + let version = hbb_common::get_version_from_url(&su.url); + if get_version_number(&version) > get_version_number(crate::VERSION) { + *SOFTWARE_UPDATE_URL.lock().unwrap() = su.url; + } + } + } + } + Ok(()) +} + +#[cfg(target_os = "macos")] +pub fn get_full_name() -> String { + format!( + "{}.{}", + hbb_common::config::ORG, + hbb_common::config::APP_NAME, + ) +} + +pub fn is_ip(id: &str) -> bool { + hbb_common::regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+$") + .unwrap() + .is_match(id) +} diff --git a/rust-rdp/rust-desk/src/ipc.rs b/rust-rdp/rust-desk/src/ipc.rs new file mode 100644 index 0000000..341599d --- /dev/null +++ b/rust-rdp/rust-desk/src/ipc.rs @@ -0,0 +1,620 @@ +use crate::rendezvous_mediator::RendezvousMediator; +use hbb_common::{ + allow_err, bail, bytes, + bytes_codec::BytesCodec, + config::{self, Config}, + futures::StreamExt as _, + futures_util::sink::SinkExt, + log, timeout, tokio, + tokio::io::{AsyncRead, AsyncWrite}, + tokio_util::codec::Framed, + ResultType, +}; +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum FS { + ReadDir { + dir: String, + include_hidden: bool, + }, + RemoveDir { + path: String, + id: i32, + recursive: bool, + }, + RemoveFile { + path: String, + id: i32, + file_num: i32, + }, + CreateDir { + path: String, + id: i32, + }, + NewWrite { + path: String, + id: i32, + files: Vec<(String, u64)>, + }, + CancelWrite { + id: i32, + }, + WriteBlock { + id: i32, + file_num: i32, + data: Vec, + compressed: bool, + }, + WriteDone { + id: i32, + file_num: i32, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum Data { + Login { + id: i32, + is_file_transfer: bool, + peer_id: String, + name: String, + authorized: bool, + port_forward: String, + keyboard: bool, + clipboard: bool, + audio: bool, + }, + ChatMessage { + text: String, + }, + SwitchPermission { + name: String, + enabled: bool, + }, + SystemInfo(Option), + Authorize, + Close, + SAS, + OnlineStatus(Option<(i64, bool)>), + Config((String, Option)), + Options(Option>), + NatType(Option), + ConfirmedKey(Option<(Vec, Vec)>), + RawMessage(Vec), + Socks(Option), + FS(FS), + Test, + SyncConfigToRootReq { + from: String, + }, + SyncConfigToRootResp(bool), + SyncConfigToUserReq { + username: String, + to: String, + }, + SyncConfigToUserResp(bool), +} + +#[tokio::main(flavor = "current_thread")] +pub async fn start(postfix: &str) -> ResultType<()> { + let mut incoming = new_listener(postfix).await?; + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let postfix = postfix.to_owned(); + tokio::spawn(async move { + loop { + match stream.next().await { + Err(err) => { + log::trace!("ipc{} connection closed: {}", postfix, err); + break; + } + Ok(Some(data)) => { + handle(data, &mut stream).await; + } + _ => {} + } + } + }); + } + Err(err) => { + log::error!("Couldn't get client: {:?}", err); + } + } + } + } +} + +pub async fn new_listener(postfix: &str) -> ResultType { + let path = Config::ipc_path(postfix); + #[cfg(not(windows))] + check_pid(postfix).await; + let mut endpoint = Endpoint::new(path.clone()); + match SecurityAttributes::allow_everyone_create() { + Ok(attr) => endpoint.set_security_attributes(attr), + Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err), + }; + match endpoint.incoming() { + Ok(incoming) => { + log::info!("Started ipc{} server at path: {}", postfix, &path); + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + write_pid(postfix); + } + Ok(incoming) + } + Err(err) => { + log::error!( + "Failed to start ipc{} server at path {}: {}", + postfix, + path, + err + ); + Err(err.into()) + } + } +} + +async fn handle(data: Data, stream: &mut Connection) { + match data { + Data::SystemInfo(_) => { + let info = format!( + "log_path: {}, config: {}, username: {}", + Config::log_path().to_str().unwrap_or(""), + Config::file().to_str().unwrap_or(""), + crate::username(), + ); + allow_err!(stream.send(&Data::SystemInfo(Some(info))).await); + } + Data::Close => { + log::info!("Receive close message"); + crate::server::input_service::fix_key_down_timeout_at_exit(); + std::process::exit(0); + } + Data::OnlineStatus(_) => { + let x = config::ONLINE + .lock() + .unwrap() + .values() + .max() + .unwrap_or(&0) + .clone(); + let confirmed = Config::get_key_confirmed(); + allow_err!(stream.send(&Data::OnlineStatus(Some((x, confirmed)))).await); + } + Data::ConfirmedKey(None) => { + let out = if Config::get_key_confirmed() { + Some(Config::get_key_pair()) + } else { + None + }; + allow_err!(stream.send(&Data::ConfirmedKey(out)).await); + } + Data::Socks(s) => match s { + None => { + allow_err!(stream.send(&Data::Socks(Config::get_socks())).await); + } + Some(data) => { + if data.proxy.is_empty() { + Config::set_socks(None); + } else { + Config::set_socks(Some(data)); + } + crate::common::test_nat_type(); + RendezvousMediator::restart(); + log::info!("socks updated"); + } + }, + Data::Config((name, value)) => match value { + None => { + let value; + if name == "id" { + value = Some(Config::get_id()); + } else if name == "password" { + value = Some(Config::get_password()); + } else if name == "salt" { + value = Some(Config::get_salt()); + } else if name == "rendezvous_server" { + value = Some(Config::get_rendezvous_server()); + } else if name == "rendezvous_servers" { + value = Some(Config::get_rendezvous_servers().join(",")); + } else { + value = None; + } + allow_err!(stream.send(&Data::Config((name, value))).await); + } + Some(value) => { + if name == "id" { + Config::set_key_confirmed(false); + Config::set_id(&value); + } else if name == "password" { + Config::set_password(&value); + } else if name == "salt" { + Config::set_salt(&value); + } else { + return; + } + log::info!("{} updated", name); + } + }, + Data::Options(value) => match value { + None => { + let v = Config::get_options(); + allow_err!(stream.send(&Data::Options(Some(v))).await); + } + Some(value) => { + let v0 = Config::get_option("stop-service"); + let v1 = Config::get_rendezvous_servers(); + Config::set_options(value); + if v0 != Config::get_option("stop-service") + || v1 != Config::get_rendezvous_servers() + { + RendezvousMediator::restart(); + } + allow_err!(stream.send(&Data::Options(None)).await); + } + }, + Data::NatType(_) => { + let t = Config::get_nat_type(); + allow_err!(stream.send(&Data::NatType(Some(t))).await); + } + Data::SyncConfigToRootReq { from } => { + allow_err!( + stream + .send(&Data::SyncConfigToRootResp(Config::sync_config_to_root( + from + ))) + .await + ); + } + Data::SyncConfigToUserReq { username, to } => { + allow_err!( + stream + .send(&Data::SyncConfigToUserResp(Config::sync_config_to_user( + username, to + ))) + .await + ); + } + _ => {} + } +} + +pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { + let path = Config::ipc_path(postfix); + let client = timeout(ms_timeout, Endpoint::connect(&path)).await??; + Ok(ConnectionTmpl::new(client)) +} + +#[inline] +#[cfg(not(windows))] +fn get_pid_file(postfix: &str) -> String { + let path = Config::ipc_path(postfix); + format!("{}.pid", path) +} + +#[cfg(not(windows))] +async fn check_pid(postfix: &str) { + let pid_file = get_pid_file(postfix); + if let Ok(mut file) = File::open(&pid_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let pid = content.parse::().unwrap_or(0); + if pid > 0 { + use sysinfo::{ProcessExt, System, SystemExt}; + let mut sys = System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(pid.into()) { + if let Some(current) = sys.process((std::process::id() as i32).into()) { + if current.name() == p.name() { + // double check with connect + if connect(1000, postfix).await.is_ok() { + return; + } + } + } + } + } + } + hbb_common::allow_err!(std::fs::remove_file(&Config::ipc_path(postfix))); +} + +#[inline] +#[cfg(not(windows))] +fn write_pid(postfix: &str) { + let path = get_pid_file(postfix); + if let Ok(mut file) = File::create(&path) { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + file.write_all(&std::process::id().to_string().into_bytes()) + .ok(); + } +} + +pub struct ConnectionTmpl { + inner: Framed, +} + +pub type Connection = ConnectionTmpl; + +impl ConnectionTmpl +where + T: AsyncRead + AsyncWrite + std::marker::Unpin, +{ + pub fn new(conn: T) -> Self { + Self { + inner: Framed::new(conn, BytesCodec::new()), + } + } + + pub async fn send(&mut self, data: &Data) -> ResultType<()> { + let v = serde_json::to_vec(data)?; + self.inner.send(bytes::Bytes::from(v)).await?; + Ok(()) + } + + async fn send_config(&mut self, name: &str, value: String) -> ResultType<()> { + self.send(&Data::Config((name.to_owned(), Some(value)))) + .await + } + + pub async fn next_timeout(&mut self, ms_timeout: u64) -> ResultType> { + Ok(timeout(ms_timeout, self.next()).await??) + } + + pub async fn next_timeout2(&mut self, ms_timeout: u64) -> Option>> { + if let Ok(x) = timeout(ms_timeout, self.next()).await { + Some(x) + } else { + None + } + } + + pub async fn next(&mut self) -> ResultType> { + match self.inner.next().await { + Some(res) => { + let bytes = res?; + if let Ok(s) = std::str::from_utf8(&bytes) { + if let Ok(data) = serde_json::from_str::(s) { + return Ok(Some(data)); + } + } + return Ok(None); + } + _ => { + bail!("reset by the peer"); + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn get_config(name: &str) -> ResultType> { + get_config_async(name, 1_000).await +} + +async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Config((name.to_owned(), None))).await?; + if let Some(Data::Config((name2, value))) = c.next_timeout(ms_timeout).await? { + if name == name2 { + return Ok(value); + } + } + return Ok(None); +} + +#[tokio::main(flavor = "current_thread")] +async fn set_config(name: &str, value: String) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send_config(name, value).await?; + Ok(()) +} + +pub fn set_password(v: String) -> ResultType<()> { + Config::set_password(&v); + set_config("password", v) +} + +pub fn get_id() -> String { + if let Ok(Some(v)) = get_config("id") { + // update salt also, so that next time reinstallation not causing first-time auto-login failure + if let Ok(Some(v2)) = get_config("salt") { + Config::set_salt(&v2); + } + if v != Config::get_id() { + Config::set_key_confirmed(false); + Config::set_id(&v); + } + v + } else { + Config::get_id() + } +} + +pub fn get_password() -> String { + if let Ok(Some(v)) = get_config("password") { + Config::set_password(&v); + v + } else { + Config::get_password() + } +} + +pub async fn get_rendezvous_server(ms_timeout: u64) -> String { + if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await { + v + } else { + Config::get_rendezvous_server() + } +} + +async fn get_options_(ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Options(None)).await?; + if let Some(Data::Options(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_options(value.clone()); + Ok(value) + } else { + Ok(Config::get_options()) + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_options() -> HashMap { + get_options_(1000).await.unwrap_or(Config::get_options()) +} + +pub fn get_option(key: &str) -> String { + if let Some(v) = get_options().get(key) { + v.clone() + } else { + "".to_owned() + } +} + +pub fn set_option(key: &str, value: &str) { + let mut options = get_options(); + if value.is_empty() { + options.remove(key); + } else { + options.insert(key.to_owned(), value.to_owned()); + } + set_options(options).ok(); +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_options(value: HashMap) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Options(Some(value.clone()))).await?; + // do not put below before connect, because we need to check should_exit + c.next_timeout(1000).await.ok(); + Config::set_options(value); + Ok(()) +} + +#[inline] +async fn get_nat_type_(ms_timeout: u64) -> ResultType { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::NatType(None)).await?; + if let Some(Data::NatType(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_nat_type(value); + Ok(value) + } else { + Ok(Config::get_nat_type()) + } +} + +pub async fn get_nat_type(ms_timeout: u64) -> i32 { + get_nat_type_(ms_timeout) + .await + .unwrap_or(Config::get_nat_type()) +} + +#[inline] +async fn get_socks_(ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Socks(None)).await?; + if let Some(Data::Socks(value)) = c.next_timeout(ms_timeout).await? { + Config::set_socks(value.clone()); + Ok(value) + } else { + Ok(Config::get_socks()) + } +} + +pub async fn get_socks_async(ms_timeout: u64) -> Option { + get_socks_(ms_timeout).await.unwrap_or(Config::get_socks()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_socks() -> Option { + get_socks_async(1_000).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { + Config::set_socks(if value.proxy.is_empty() { + None + } else { + Some(value.clone()) + }); + connect(1_000, "") + .await? + .send(&Data::Socks(Some(value))) + .await?; + Ok(()) +} + +/* +static mut SHARED_MEMORY: *mut i64 = std::ptr::null_mut(); + +pub fn initialize_shared_memory(create: bool) { + let mut shmem_flink = "shared-memory".to_owned(); + if cfg!(windows) { + let df = "C:\\ProgramData"; + let df = if std::path::Path::new(df).exists() { + df.to_owned() + } else { + std::env::var("TEMP").unwrap_or("C:\\Windows\\TEMP".to_owned()) + }; + let df = format!("{}\\{}", df, *hbb_common::config::APP_NAME.read().unwrap()); + std::fs::create_dir(&df).ok(); + shmem_flink = format!("{}\\{}", df, shmem_flink); + } else { + shmem_flink = Config::ipc_path("").replace("ipc", "") + &shmem_flink; + } + use shared_memory::*; + let shmem = if create { + match ShmemConf::new() + .force_create_flink() + .size(16) + .flink(&shmem_flink) + .create() + { + Err(ShmemError::LinkExists) => ShmemConf::new().flink(&shmem_flink).open(), + Ok(m) => Ok(m), + Err(e) => Err(e), + } + } else { + ShmemConf::new().flink(&shmem_flink).open() + }; + if create { + set_all_perm(&shmem_flink); + } + match shmem { + Ok(shmem) => unsafe { + SHARED_MEMORY = shmem.as_ptr() as *mut i64; + std::mem::forget(shmem); + }, + Err(err) => { + log::error!( + "Unable to create or open shmem flink {} : {}", + shmem_flink, + err + ); + } + } +} + +fn set_all_perm(p: &str) { + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o0777)).ok(); + } +} +*/ diff --git a/rust-rdp/rust-desk/src/lang.rs b/rust-rdp/rust-desk/src/lang.rs new file mode 100644 index 0000000..7d9c70b --- /dev/null +++ b/rust-rdp/rust-desk/src/lang.rs @@ -0,0 +1,43 @@ +use hbb_common::{config::Config, log}; +use std::ops::Deref; + +mod cn; +mod en; +mod fr; +mod it; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn translate(name: String) -> String { + let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase(); + log::debug!("The current locale is {}", locale); + translate_locale(name, &locale) +} + +pub fn translate_locale(name: String, locale: &str) -> String { + let mut lang = Config::get_option("lang"); + if lang.is_empty() { + lang = locale + .split("-") + .last() + .map(|x| x.split("_").last().unwrap_or_default()) + .unwrap_or_default() + .to_owned(); + } + let lang = lang.to_lowercase(); + let m = match lang.as_str() { + "fr" => fr::T.deref(), + "cn" => cn::T.deref(), + "it" => it::T.deref(), + _ => en::T.deref(), + }; + if let Some(v) = m.get(&name as &str) { + v.to_string() + } else { + if lang != "en" { + if let Some(v) = en::T.get(&name as &str) { + return v.to_string(); + } + } + name + } +} diff --git a/rust-rdp/rust-desk/src/lang/cn.rs b/rust-rdp/rust-desk/src/lang/cn.rs new file mode 100644 index 0000000..bc4f569 --- /dev/null +++ b/rust-rdp/rust-desk/src/lang/cn.rs @@ -0,0 +1,210 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "状态"), + ("Your Desktop", "你的桌面"), + ("desk_tip", "你的桌面可以通过下面的ID和密码访问。"), + ("Password", "密码"), + ("Ready", "就绪"), + ("connecting_status", "正在接入RustDesk网络..."), + ("Enable Service", "允许服务"), + ("Start Service", "启动服务"), + ("Service is not running", "服务没有启动"), + ("not_ready_status", "未就绪,请检查网络连接"), + ("Control Remote Desktop", "控制远程桌面"), + ("Transfer File", "传输文件"), + ("Connect", "连接"), + ("Recent Sessions", "最近访问过"), + ("Address Book", "地址簿"), + ("Confirmation", "确认"), + ("TCP Tunneling", "TCP隧道"), + ("Remove", "删除"), + ("Refresh random password", "刷新随机密码"), + ("Set your own password", "设置密码"), + ("Enable Keyboard/Mouse", "允许控制键盘/鼠标"), + ("Enable Clipboard", "允许同步剪贴板"), + ("Enable File Transfer", "允许传输文件"), + ("Enable TCP Tunneling", "允许建立TCP隧道"), + ("IP Whitelisting", "IP白名单"), + ("ID/Relay Server", "ID/中继服务器"), + ("Stop service", "停止服务"), + ("Change ID", "改变ID"), + ("Website", "网站"), + ("About", "关于"), + ("Mute", "静音"), + ("Audio Input", "音频输入"), + ("ID Server", "ID服务器"), + ("Relay Server", "中继服务器"), + ("API Server", "API服务器"), + ("invalid_http", "必须以http://或者https://开头"), + ("Invalid IP", "无效IP"), + ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), + ("Invalid format", "无效格式"), + ("This function is turned off by the server", "服务器关闭了此功能"), + ("Not available", "已被占用"), + ("Too frequent", "修改太频繁,请稍后再试"), + ("Cancel", "取消"), + ("Skip", "跳过"), + ("Close", "关闭"), + ("Retry", "再试"), + ("OK", "确认"), + ("Password Required", "需要密码"), + ("Please enter your password", "请输入密码"), + ("Remember password", "记住密码"), + ("Wrong Password", "密码错误"), + ("Do you want to enter again?", "还想输入一次吗?"), + ("Connection Error", "连接错误"), + ("Error", "错误"), + ("Reset by the peer", "连接被对方关闭"), + ("Connecting...", "正在连接..."), + ("Connection in progress. Please wait.", "连接进行中,请稍等。"), + ("Please try 1 minute later", "一分钟后再试"), + ("Login Error", "登录错误"), + ("Successful", "成功"), + ("Connected, waiting for image...", "已连接,等待画面传输..."), + ("Name", "文件名"), + ("Modified", "修改时间"), + ("Size", "大小"), + ("Show Hidden Files", "显示隐藏文件"), + ("Receive", "接受"), + ("Send", "发送"), + ("Remote Computer", "远程电脑"), + ("Local Computer", "本地电脑"), + ("Confirm Delete", "确认删除"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Do this for all conflicts", "应用于其它冲突"), + ("Deleting", "正在删除"), + ("files", "文件"), + ("Waiting", "等待..."), + ("Finished", "完成"), + ("Custom Image Quality", "设置画面质量"), + ("Privacy mode", "隐私模式"), + ("Block user input", "阻止用户输入"), + ("Unblock user input", "取消阻止用户输入"), + ("Adjust Window", "调节窗口"), + ("Original", "原始比例"), + ("Shrink", "收缩"), + ("Stretch", "伸展"), + ("Good image quality", "好画质"), + ("Balanced", "一般画质"), + ("Optimize reaction time", "优化反应时间"), + ("Custom", "自定义画质"), + ("Show remote cursor", "显示远程光标"), + ("Disable clipboard", "禁止剪贴板"), + ("Lock after session end", "断开后锁定远程电脑"), + ("Insert", "插入"), + ("Insert Lock", "锁定远程电脑"), + ("Refresh", "刷新画面"), + ("ID does not exist", "ID不存在"), + ("Failed to connect to rendezvous server", "连接注册服务器失败"), + ("Please try later", "请稍后再试"), + ("Remote desktop is offline", "远程电脑不在线"), + ("Key mismatch", "Key不匹配"), + ("Timeout", "连接超时"), + ("Failed to connect to relay server", "无法连接到中继服务器"), + ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), + ("Failed to connect via relay server", "无法通过中继服务器建立连接"), + ("Failed to make direct connection to remote desktop", "无法建立直接连接"), + ("Set Password", "设置密码"), + ("OS Password", "操作系统密码"), + ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将RustDesk安装到系统,从而规避上述问题。"), + ("Click to upgrade", "点击这里升级"), + ("Click to download", "点击这里下载"), + ("Click to update", "点击这里更新"), + ("Configuration Permissions", "配置权限"), + ("Configure", "配置"), + ("config_acc", "为了能够远程控制你的桌面, 请给予RustDesk\"辅助功能\" 权限。"), + ("config_screen", "为了能够远程访问你的桌面, 请给予RustDesk\"屏幕录制\" 权限。"), + ("Installing ...", "安装 ..."), + ("Install", "安装"), + ("Installation", "安装"), + ("Installation Path", "安装路径"), + ("Create start menu shortcuts", "创建启动菜单快捷方式"), + ("Create desktop icon", "创建桌面图标"), + ("agreement_tip", "开始安装即表示接受许可协议。"), + ("Accept and Install", "同意并安装"), + ("End-user license agreement", "用户协议"), + ("Generating ...", "正在产生 ..."), + ("Your installation is lower version.", "你安装的版本比当前运行的低。"), + ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), + ("Listening ...", "正在等待隧道连接 ..."), + ("Remote Host", "远程主机"), + ("Remote Port", "远程端口"), + ("Action", "动作"), + ("Add", "添加"), + ("Local Port", "本地端口"), + ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), + ("Too short, at least 6 characters.", "太短了,至少6个字符"), + ("The confirmation is not identical.", "两次输入不匹配"), + ("Permissions", "权限"), + ("Accept", "接受"), + ("Dismiss", "拒绝"), + ("Disconnect", "断开连接"), + ("Allow using keyboard and mouse", "允许使用键盘鼠标"), + ("Allow using clipboard", "允许使用剪贴板"), + ("Allow hearing sound", "允许听到声音"), + ("Connected", "已经连接"), + ("Direct and encrypted connection", "加密直连"), + ("Relayed and encrypted connection", "加密中继连接"), + ("Direct and unencrypted connection", "非加密直连"), + ("Relayed and unencrypted connection", "非加密中继连接"), + ("Enter Remote ID", "输入对方ID"), + ("Enter your password", "输入密码"), + ("Logging in...", "正在登录..."), + ("Enable RDP session sharing", "允许RDP会话共享"), + ("Auto Login", "自动登录(设置断开后锁定才有效)"), + ("Enable Direct IP Access", "允许IP直接访问"), + ("Rename", "改名"), + ("Space", "空格"), + ("Create Desktop Shortcut", "创建桌面快捷方式"), + ("Change Path", "改变路径"), + ("Create Folder", "创建文件夹"), + ("Please enter the folder name", "请输入文件夹名称"), + ("Fix it", "修复"), + ("Warning", "警告"), + ("Login screen using Wayland is not supported", "不支持使用 Wayland 登录界面"), + ("Reboot required", "重启后才能生效"), + ("Unsupported display server ", "不支持当前显示服务器"), + ("x11 expected", "请切换到 x11"), + ("Port", "端口"), + ("Settings", "设置"), + ("Username", " 用户名"), + ("Invalid port", "无效端口"), + ("Closed manually by the peer", "被对方手动关闭"), + ("Enable remote configuration modification", "允许远程修改配置"), + ("Run without install", "无安装运行"), + ("Always connected via relay", "强制走中继连接"), + ("Always connect via relay", "强制走中继连接"), + ("whitelist_tip", "只有白名单里的ip才能访问我"), + ("Login", "登录"), + ("Logout", "登出"), + ("Tags", "标签"), + ("Search ID", "查找ID"), + ("Current Wayland display server is not supported", "不支持 Wayland 显示服务器"), + ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), + ("Add ID", "增加ID"), + ("Add Tag", "增加标签"), + ("Unselect all tags", "取消选择所有标签"), + ("Network error", "网络错误"), + ("Username missed", "用户名没有填写"), + ("Password missed", "密码没有填写"), + ("Wrong credentials", "用户名或者密码错误"), + ("Edit Tag", "修改标签"), + ("Unremember Password", "忘掉密码"), + ("Favorites", "收藏"), + ("Add to Favorites", "加入到收藏"), + ("Remove from Favorites", "从收藏中删除"), + ("Empty", "空空如也"), + ("Invalid folder name", "无效文件夹名称"), + ("Socks5 Proxy", "Socks5 代理"), + ("Hostname", "主机名"), + ("Discovered", "已发现"), + ("install_daemon_tip", "为了开机启动,请安装系统服务。"), + ("Remote ID", "远程ID"), + ("Paste", "粘贴"), + ("Are you sure to close the connection?", "是否确认关闭连接?"), + ("Download new version", "下载新版本"), + ("Touch mode", "触屏模式"), + ("Reset canvas", "重置画布"), + ].iter().cloned().collect(); +} \ No newline at end of file diff --git a/rust-rdp/rust-desk/src/lang/en.rs b/rust-rdp/rust-desk/src/lang/en.rs new file mode 100644 index 0000000..0d83ed0 --- /dev/null +++ b/rust-rdp/rust-desk/src/lang/en.rs @@ -0,0 +1,21 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("desk_tip", "Your desktop can be accessed with this ID and password."), + ("connecting_status", "Connecting to the RustDesk network..."), + ("not_ready_status", "Not ready. Please check your connection"), + ("id_change_tip", "Only a-z, A-Z, 0-9 and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."), + ("install_tip", "Due to UAC, RustDesk can not work properly as the remote side in some cases. To avoid UAC, please click the button below to install RustDesk to the system."), + ("config_acc", "In order to control your Desktop remotely, you need to grant RustDesk \"Accessibility\" permissions."), + ("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."), + ("agreement_tip", "By starting the installation, you accept the license agreement."), + ("not_close_tcp_tip", "Don't close this window while you are using the tunnel"), + ("setup_server_tip", "For faster connection, please set up your own server"), + ("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"), + ("whitelist_tip", "Only whitelisted IP can access me"), + ("whitelist_sep", "Seperated by comma, semicolon, spaces or new line"), + ("Wrong credentials", "Wrong username or password"), + ("invalid_http", "must start with http:// or https://"), + ("install_daemon_tip", "For starting on boot, you need to install system service."), + ].iter().cloned().collect(); +} diff --git a/rust-rdp/rust-desk/src/lang/fr.rs b/rust-rdp/rust-desk/src/lang/fr.rs new file mode 100644 index 0000000..50892df --- /dev/null +++ b/rust-rdp/rust-desk/src/lang/fr.rs @@ -0,0 +1,203 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Statut"), + ("Your Desktop", "Votre bureau"), + ("desk_tip", "Votre bureau est accessible via l'identifiant et le mot de passe ci-dessous."), + ("Password", "Mot de passe"), + ("Ready", "Prêt"), + ("connecting_status", "Connexion au réseau RustDesk..."), + ("Enable Service", "Autoriser le service"), + ("Start Service", "Démarrer le service"), + ("Service is not running", "Le service ne fonctionne pas"), + ("not_ready_status", "Pas prêt, veuillez vérifier la connexion réseau"), + ("Control Remote Desktop", "Contrôler le bureau à distance"), + ("Transfer File", "Transférer le fichier"), + ("Connect", "Connecter"), + ("Recent Sessions", "Sessions récentes"), + ("Address Book", "Carnet d'adresses"), + ("Confirmation", "Confirmation"), + ("TCP Tunneling", "Tunneling TCP"), + ("Remove", "Supprimer"), + ("Refresh random password", "Actualiser le mot de passe aléatoire"), + ("Set your own password", "Définir votre propre mot de passe"), + ("Enable Keyboard/Mouse", "Activer le contrôle clavier/souris"), + ("Enable Clipboard", "Activer la synchronisation du presse-papiers"), + ("Enable File Transfer", "Activer le transfert de fichiers"), + ("Enable TCP Tunneling", "Activer le tunneling TCP"), + ("IP Whitelisting", "Liste blanche IP"), + ("ID/Relay Server", "ID/Serveur Relais"), + ("Stop service", "Arrêter service"), + ("Change ID", "Changer d'ID"), + ("Website", "Site Web"), + ("About", "Sur"), + ("Mute", "Muet"), + ("Audio Input", "Entrée audio"), + ("ID Server", "Serveur ID"), + ("Relay Server", "Serveur Relais"), + ("API Server", "Serveur API"), + ("invalid_http", "Doit commencer par http:// ou https://"), + ("Invalid IP", "IP invalide"), + ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur est comprise entre 6 et 16."), + ("Invalid format", "Format invalide"), + ("This function is turned off by the server", "Cette fonction est désactivée par le serveur"), + ("Not available", "Indisponible"), + ("Too frequent", "Modifier trop fréquemment, veuillez réessayer plus tard"), + ("Cancel", "Annuler"), + ("Skip", "Ignorer"), + ("Close", "Fermer"), + ("Retry", "Réessayer"), + ("OK", "Confirmer"), + ("Password Required", "Mot de passe requis"), + ("Please enter your password", "Veuillez saisir votre mot de passe"), + ("Remember password", "Mémoriser le mot de passe"), + ("Wrong Password", "Mauvais mot de passe"), + ("Do you want to enter again?", "Voulez-vous participer à nouveau?"), + ("Connection Error", "Erreur de connexion"), + ("Error", "Erreur"), + ("Reset by the peer", "La connexion a été fermée par le pair"), + ("Connecting...", "Connexion..."), + ("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."), + ("Please try 1 minute later", "Réessayez dans une minute"), + ("Login Error", "Erreur de connexion"), + ("Successful", "Succès"), + ("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."), + ("Name", "Nom du fichier"), + ("Modified", "Modifié"), + ("Size", "Taille"), + ("Show Hidden Files", "Afficher les fichiers cachés"), + ("Receive", "Accepter"), + ("Send", "Envoyer"), + ("Remote Computer", "Ordinateur distant"), + ("Local Computer", "Ordinateur local"), + ("Confirm Delete", "Confirmer la suppression"), + ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier?"), + ("Do this for all conflicts", "Appliquer à d'autres conflits"), + ("Deleting", "Suppression"), + ("files", "fichier"), + ("Waiting", "En attente en attente..."), + ("Finished", "Terminé"), + ("Custom Image Quality", "Définir la qualité d'image"), + ("Privacy mode", "Mode privé"), + ("Block user input", "Bloquer la saisie de l'utilisateur"), + ("Unblock user input", "Débloquer l'entrée de l'utilisateur"), + ("Adjust Window", "Ajuster la fenêtre"), + ("Original", "Ratio d'origine"), + ("Shrink", "Rétréci"), + ("Stretch", "Étirer"), + ("Good image quality", "Bonne qualité d'image"), + ("Balanced", "Qualité d'image normale"), + ("Optimize reaction time", "Optimiser le temps de réaction"), + ("Custom", "Qualité d'image personnalisée"), + ("Show remote cursor", "Afficher le curseur distant"), + ("Disable clipboard", "Désactiver le presse-papiers"), + ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), + ("Insert", "Insérer"), + ("Insert Lock", "Verrouiller l'ordinateur distant"), + ("Refresh", "Rafraîchir l'écran"), + ("ID does not exist", "L'ID n'existe pas"), + ("Failed to connect to rendezvous server", "Échec de la connexion au serveur de rendez-vous"), + ("Please try later", "Veuillez essayer plus tard"), + ("Remote desktop is offline", "Le bureau à distance est hors ligne"), + ("Key mismatch", "Discordance de clé"), + ("Timeout", "Connexion expirée"), + ("Failed to connect to relay server", "Échec de la connexion au serveur relais"), + ("Failed to connect via rendezvous server", "Échec de l'établissement d'une connexion via le serveur de rendez-vous"), + ("Failed to connect via relay server", "Impossible d'établir une connexion via le serveur relais"), + ("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"), + ("Set Password", "Définir le mot de passe"), + ("OS Password", "Mot de passe du système d'exploitation"), + ("install_tip", "Vous utilisez une version désinstallée. En raison des restrictions UAC, en tant que terminal contrôlé, dans certains cas, il ne sera pas en mesure de contrôler la souris et le clavier ou d'enregistrer l'écran. Veuillez cliquer sur le bouton ci-dessous pour installer RustDesk au système pour éviter la question ci-dessus."), + ("Click to upgrade", "Cliquez pour mettre à niveau"), + ("Click to download", "Cliquez pour télécharger"), + ("Click to update", "Cliquez pour mettre à jour"), + ("Configuration Permissions", "Autorisations de configuration"), + ("Configure", "Configurer"), + ("config_acc", "Afin de pouvoir contrôler votre bureau à distance, veuillez donner l'autorisation\"accessibilité\" à RustDesk."), + ("config_screen", "Afin de pouvoir accéder à votre bureau à distance, veuillez donner l'autorisation à RustDesk\"enregistrement d'écran\"."), + ("Installing ...", "Installation ..."), + ("Install", "Installer"), + ("Installation", "Installation"), + ("Installation Path", "Chemin d'installation"), + ("Create start menu shortcuts", "Créer des raccourcis dans le menu démarrer"), + ("Create desktop icon", "Créer une icône sur le bureau"), + ("agreement_tip", "Démarrer l'installation signifie accepter le contrat de licence."), + ("Accept and Install", "Accepter et installer"), + ("End-user license agreement", "Contrat d'utilisateur"), + ("Generating ...", "Génération ..."), + ("Your installation is lower version.", "La version que vous avez installée est inférieure à la version en cours d'exécution."), + ("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l'utilisation du tunnel"), + ("Listening ...", "En attente de connexion tunnel..."), + ("Remote Host", "Hôte distant"), + ("Remote Port", "Port distant"), + ("Action", "Action"), + ("Add", "Ajouter"), + ("Local Port", "Port local"), + ("setup_server_tip", "Si vous avez besoin d'une vitesse de connexion plus rapide, vous pouvez choisir de créer votre propre serveur"), + ("Too short, at least 6 characters.", "Trop court, au moins 6 caractères."), + ("The confirmation is not identical.", "Les deux entrées ne correspondent pas"), + ("Permissions", "Autorisations"), + ("Accept", "Accepter"), + ("Dismiss", "Rejeter"), + ("Disconnect", "Déconnecter"), + ("Allow using keyboard and mouse", "Autoriser l'utilisation du clavier et de la souris"), + ("Allow using clipboard", "Autoriser l'utilisation du presse-papiers"), + ("Allow hearing sound", "Autoriser l'audition du son"), + ("Connected", "Connecté"), + ("Direct and encrypted connection", "Connexion directe cryptée"), + ("Relayed and encrypted connection", "Connexion relais cryptée"), + ("Direct and unencrypted connection", "Connexion directe non cryptée"), + ("Relayed and unencrypted connection", "Connexion relais non cryptée"), + ("Enter Remote ID", "Entrez l'ID à distance"), + ("Enter your password", "Entrez votre mot de passe"), + ("Logging in...", "Se connecter..."), + ("Enable RDP session sharing", "Activer le partage de session RDP"), + ("Auto Login", "Connexion automatique (le verrouillage ne sera effectif qu'après la déconnexion du paramètre)"), + ("Enable Direct IP Access", "Autoriser l'accès direct IP"), + ("Rename", "Renommer"), + ("Space", "Espace"), + ("Create Desktop Shortcut", "Créer un raccourci sur le bureau"), + ("Change Path", "Changer de chemin"), + ("Create Folder", "Créer un dossier"), + ("Please enter the folder name", "Veuillez saisir le nom du dossier"), + ("Fix it", "Réparez-le"), + ("Warning", "Avertissement"), + ("Login screen using Wayland is not supported", "L'écran de connexion utilisant Wayland n'est pas pris en charge"), + ("Reboot required", "Redémarrage pour prendre effet"), + ("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"), + ("x11 expected", "Veuillez passer à x11"), + ("Port", "Port"), + ("Settings", "Paramètres"), + ("Username", " Nom d'utilisateur"), + ("Invalid port", "Port invalide"), + ("Closed manually by the peer", "Fermé manuellement par le pair"), + ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), + ("Run without install", "Exécuter sans installer"), + ("Always connected via relay", "Forcer la connexion relais"), + ("Always connect via relay", "Forcer la connexion relais"), + ("whitelist_tip", "Seul l'ip dans la liste blanche peut m'accéder"), + ("Login", "Connexion"), + ("Logout", "Déconnexion"), + ("Tags", "Étiqueter"), + ("Search ID", "Identifiant de recherche"), + ("Current Wayland display server is not supported", "Le serveur d'affichage Wayland n'est pas pris en charge"), + ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), + ("Add ID", "Ajouter ID"), + ("Add Tag", "Ajouter une balise"), + ("Unselect all tags", "Désélectionner toutes les balises"), + ("Network error", "Erreur réseau"), + ("Username missed", "Nom d'utilisateur manqué"), + ("Password missed", "Mot de passe manqué"), + ("Wrong credentials", "Identifiant ou mot de passe erroné"), + ("Edit Tag", "Modifier la balise"), + ("Invalid folder name", "Nom de dossier invalide"), + ("Hostname", "nom d'hôte"), + ("Discovered", "Découvert"), + ("Remote ID", "ID à distance"), + ("Paste", "Pâte"), + ("Are you sure to close the connection?", "Êtes-vous sûr de fermer la connexion?"), + ("Download new version", "Télécharger la nouvelle version"), + ("Touch mode", "Mode tactile"), + ("Reset canvas", "Réinitialiser le canevas"), + ].iter().cloned().collect(); +} diff --git a/rust-rdp/rust-desk/src/lang/it.rs b/rust-rdp/rust-desk/src/lang/it.rs new file mode 100644 index 0000000..8e13bfe --- /dev/null +++ b/rust-rdp/rust-desk/src/lang/it.rs @@ -0,0 +1,203 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stato"), + ("Your Desktop", "Il tuo desktop"), + ("desk_tip", "Puoi accedere al tuo desktop usando l'ID e la password riportati qui."), + ("Password", "Password"), + ("Ready", "Pronto"), + ("connecting_status", "Connessione alla rete RustDesk in corso..."), + ("Enable Service", "Abilita servizio"), + ("Start Service", "Avvia servizio"), + ("Service is not running", "Il servizio non è in esecuzione"), + ("not_ready_status", "Non pronto. Verifica la tua connessione"), + ("Control Remote Desktop", "Controlla una scrivania remota"), + ("Transfer File", "Trasferisci file"), + ("Connect", "Connetti"), + ("Recent Sessions", "Sessioni recenti"), + ("Address Book", "Rubrica"), + ("Confirmation", "Conferma"), + ("TCP Tunneling", "Tunnel TCP"), + ("Remove", "Rimuovi"), + ("Refresh random password", "Nuova password casuale"), + ("Set your own password", "Imposta la tua password"), + ("Enable Keyboard/Mouse", "Abilita tastiera/mouse"), + ("Enable Clipboard", "Abilita appunti"), + ("Enable File Transfer", "Abilita trasferimento file"), + ("Enable TCP Tunneling", "Abilita tunnel TCP"), + ("IP Whitelisting", "IP autorizzati"), + ("ID/Relay Server", "Server ID/Relay"), + ("Stop service", "Arresta servizio"), + ("Change ID", "Cambia ID"), + ("Website", "Sito web"), + ("About", "Informazioni"), + ("Mute", "Silenzia"), + ("Audio Input", "Input audio"), + ("ID Server", "ID server"), + ("Relay Server", "Server relay"), + ("API Server", "Server API"), + ("invalid_http", "deve iniziare con http:// o https://"), + ("Invalid IP", "Indirizzo IP non valido"), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), + ("Invalid format", "Formato non valido"), + ("This function is turned off by the server", "Questa funzione è disabilitata sul server"), + ("Not available", "Non disponibile"), + ("Too frequent", "Troppo frequente"), + ("Cancel", "Annulla"), + ("Skip", "Ignora"), + ("Close", "Chiudi"), + ("Retry", "Riprova"), + ("OK", "OK"), + ("Password Required", "Password richiesta"), + ("Please enter your password", "Inserisci la tua password"), + ("Remember password", "Ricorda password"), + ("Wrong Password", "Password errata"), + ("Do you want to enter again?", "Vuoi riprovare?"), + ("Connection Error", "Errore di connessione"), + ("Error", "Errore"), + ("Reset by the peer", "Reimpostata dal peer"), + ("Connecting...", "Connessione..."), + ("Connection in progress. Please wait.", "Connessione in corso. Attendi."), + ("Please try 1 minute later", "Per favore riprova fra 1 minuto"), + ("Login Error", "Errore di login"), + ("Successful", "Successo"), + ("Connected, waiting for image...", "Connesso, in attesa dell'immagine..."), + ("Name", "Nome"), + ("Modified", "Modificato"), + ("Size", "Dimensione"), + ("Show Hidden Files", "Mostra file nascosti"), + ("Receive", "Ricevi"), + ("Send", "Invia"), + ("Remote Computer", "Computer remoto"), + ("Local Computer", "Computer locale"), + ("Confirm Delete", "Conferma cancellazione"), + ("Are you sure you want to delete this file?", "Vuoi davvero eliminare questo file?"), + ("Do this for all conflicts", "Ricorca questa scelta per tutti i conflitti"), + ("Deleting", "Cancellazione di"), + ("files", "file"), + ("Waiting", "In attesa"), + ("Finished", "Terminato"), + ("Custom Image Quality", "Qualità immagine personalizzata"), + ("Privacy mode", "Modalità privacy"), + ("Block user input", "Blocca l'input dell'utente"), + ("Unblock user input", "Sbloccare l'input dell'utente"), + ("Adjust Window", "Adatta la finestra"), + ("Original", "Originale"), + ("Shrink", "Restringi"), + ("Stretch", "Allarga"), + ("Good image quality", "Buona qualità immagine"), + ("Balanced", "Bilanciato"), + ("Optimize reaction time", "Ottimizza il tempo di reazione"), + ("Custom", "Personalizzato"), + ("Show remote cursor", "Mostra il cursore remoto"), + ("Disable clipboard", "Disabilita appunti"), + ("Lock after session end", "Blocca al termine della sessione"), + ("Insert", "Inserisci"), + ("Insert Lock", "Blocco inserimento"), + ("Refresh", "Aggiorna"), + ("ID does not exist", "L'ID non esiste"), + ("Failed to connect to rendezvous server", "Errore di connessione al server rendezvous"), + ("Please try later", "Riprova più tardi"), + ("Remote desktop is offline", "Il desktop remoto è offline"), + ("Key mismatch", "La chiave non corrisponde"), + ("Timeout", "Timeout"), + ("Failed to connect to relay server", "Errore di connessione al server relay"), + ("Failed to connect via rendezvous server", "Errore di connessione tramite il server rendezvous"), + ("Failed to connect via relay server", "Errore di connessione tramite il server relay"), + ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), + ("Set Password", "Imposta password"), + ("OS Password", "Password del sistema operativo"), + ("install_tip", "A causa del Controllo Account Utente, RustDesk potrebbe non funzionare correttamente come desktop remoto. Per evitare questo problema, fai click sul tasto qui sotto per installare RustDesk a livello di sistema."), + ("Click to upgrade", "Fai click per aggiornare"), + ("Click to download", "Cliquez per scaricare"), + ("Click to update", "Fare clic per aggiornare"), + ("Configuration Permissions", "Permessi di configurazione"), + ("Configure", "Configura"), + ("config_acc", "Per controllare il tuo desktop dall'esterno, devi fornire a RustDesk il permesso \"Accessibilità\"."), + ("config_screen", "Per controllare il tuo desktop dall'esterno, devi fornire a RustDesk il permesso \"Registrazione schermo\"."), + ("Installing ...", "Installazione ..."), + ("Install", "Installa"), + ("Installation", "Installazione"), + ("Installation Path", "Percorso di installazione"), + ("Create start menu shortcuts", "Crea i collegamenti nel menu di avvio"), + ("Create desktop icon", "Crea un'icona sul desktop"), + ("agreement_tip", "Avviando l'installazione, accetti i termini del contratto di licenza."), + ("Accept and Install", "Accetta e installa"), + ("End-user license agreement", "Contratto di licenza con l'utente finale"), + ("Generating ...", "Generazione ..."), + ("Your installation is lower version.", "La tua installazione non è aggiornata."), + ("not_close_tcp_tip", "Non chiudere questa finestra mentre stai usando il tunnel"), + ("Listening ...", "In ascolto ..."), + ("Remote Host", "Host remoto"), + ("Remote Port", "Porta remota"), + ("Action", "Azione"), + ("Add", "Aggiungi"), + ("Local Port", "Porta locale"), + ("setup_server_tip", "Per una connessione più veloce, configura un tuo server"), + ("Too short, at least 6 characters.", "Troppo breve, almeno 6 caratteri"), + ("The confirmation is not identical.", "La conferma non corrisponde"), + ("Permissions", "Permessi"), + ("Accept", "Accetta"), + ("Dismiss", "Rifiuta"), + ("Disconnect", "Disconnetti"), + ("Allow using keyboard and mouse", "Consenti l'uso di tastiera e mouse"), + ("Allow using clipboard", "Consenti l'uso degli appunti"), + ("Allow hearing sound", "Consenti la riproduzione dell'audio"), + ("Connected", "Connesso"), + ("Direct and encrypted connection", "Connessione diretta e cifrata"), + ("Relayed and encrypted connection", "Connessione tramite relay e cifrata"), + ("Direct and unencrypted connection", "Connessione diretta e non cifrata"), + ("Relayed and unencrypted connection", "Connessione tramite relay e non cifrata"), + ("Enter Remote ID", "Inserisci l'ID remoto"), + ("Enter your password", "Inserisci la tua password"), + ("Logging in...", "Autenticazione..."), + ("Enable RDP session sharing", "Abilita la condivisione della sessione RDP"), + ("Auto Login", "Login automatico"), + ("Enable Direct IP Access", "Abilita l'accesso diretto tramite IP"), + ("Rename", "Rinomina"), + ("Space", "Spazio"), + ("Create Desktop Shortcut", "Crea collegamento sul desktop"), + ("Change Path", "Cambia percorso"), + ("Create Folder", "Crea cartella"), + ("Please enter the folder name", "Inserisci il nome della cartella"), + ("Fix it", "Risolvi"), + ("Warning", "Avviso"), + ("Login screen using Wayland is not supported", "La schermata di login non è supportata utilizzando Wayland"), + ("Reboot required", "Riavvio necessario"), + ("Unsupported display server ", "Display server non supportato"), + ("x11 expected", "x11 necessario"), + ("Port", "Porta"), + ("Settings", "Impostazioni"), + ("Username", " Nome utente"), + ("Invalid port", "Porta non valida"), + ("Closed manually by the peer", "Chiuso manualmente dal peer"), + ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), + ("Run without install", "Avvia senza installare"), + ("Always connected via relay", "Connesso sempre tramite relay"), + ("Always connect via relay", "Connetti sempre tramite relay"), + ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), + ("Login", "Accedi"), + ("Logout", "Esci"), + ("Tags", "Tag"), + ("Search ID", "Cerca ID"), + ("Current Wayland display server is not supported", "Questo display server Wayland non è supportato"), + ("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"), + ("Add ID", "Aggiungi ID"), + ("Add Tag", "Aggiungi tag"), + ("Unselect all tags", "Deseleziona tutti i tag"), + ("Network error", "Errore di rete"), + ("Username missed", "Nome utente dimenticato"), + ("Password missed", "Password dimenticata"), + ("Wrong credentials", "Credenziali errate"), + ("Edit Tag", "Modifica tag"), + ("Invalid folder name", "Nome della cartella non valido"), + ("Hostname", "Nome host"), + ("Discovered", "Scoperto"), + ("Remote ID", "ID remoto"), + ("Paste", "Impasto"), + ("Are you sure to close the connection?", "Sei sicuro di chiudere la connessione?"), + ("Download new version", "Scarica nuova versione"), + ("Touch mode", "Modalità tocco"), + ("Reset canvas", "Ripristina tela"), + ].iter().cloned().collect(); +} \ No newline at end of file diff --git a/rust-rdp/rust-desk/src/lib.rs b/rust-rdp/rust-desk/src/lib.rs new file mode 100644 index 0000000..0718b9a --- /dev/null +++ b/rust-rdp/rust-desk/src/lib.rs @@ -0,0 +1,30 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod platform; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod server; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use self::server::*; +mod client; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod rendezvous_mediator; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use self::rendezvous_mediator::*; +pub mod common; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod ipc; +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub mod ui; +mod version; +pub use version::*; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod mobile; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod mobile_ffi; +use common::*; +#[cfg(feature = "cli")] +pub mod cli; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod port_forward; +mod lang; diff --git a/rust-rdp/rust-desk/src/main.rs b/rust-rdp/rust-desk/src/main.rs new file mode 100644 index 0000000..e2d00d0 --- /dev/null +++ b/rust-rdp/rust-desk/src/main.rs @@ -0,0 +1,150 @@ +// Specify the Windows subsystem to eliminate console window. +// Requires Rust 1.18. +//#![windows_subsystem = "windows"] + +use hbb_common::log; +use rustdesk::*; + +#[cfg(any(target_os = "android", target_os = "ios"))] +fn main() { + common::test_rendezvous_server(); + common::test_nat_type(); + #[cfg(target_os = "android")] + crate::common::check_software_update(); + mobile::Session::start(""); +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +fn main() { + let mut args = Vec::new(); + let mut i = 0; + for arg in std::env::args() { + if i > 0 { + args.push(arg); + } + i += 1; + } + if args.len() > 0 && args[0] == "--version" { + println!("{}", crate::VERSION); + return; + } + #[cfg(not(feature = "inline"))] + { + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + } + #[cfg(feature = "inline")] + { + let mut path = hbb_common::config::Config::log_path(); + if args.len() > 0 && args[0].starts_with("--") { + let name = args[0].replace("--", ""); + if !name.is_empty() { + path.push(name); + } + } + use flexi_logger::*; + Logger::try_with_env_or_str("debug") + .map(|x| { + x.log_to_file(FileSpec::default().directory(path)) + .format(opt_format) + .rotate( + Criterion::Age(Age::Day), + Naming::Timestamps, + Cleanup::KeepLogFiles(6), + ) + .start() + .ok(); + }) + .ok(); + } + if args.is_empty() { + std::thread::spawn(move || start_server(false, false)); + } else { + #[cfg(windows)] + { + if args[0] == "--uninstall" { + if let Err(err) = platform::uninstall_me() { + log::error!("Failed to uninstall: {}", err); + } + return; + } else if args[0] == "--update" { + hbb_common::allow_err!(platform::update_me()); + return; + } else if args[0] == "--reinstall" { + hbb_common::allow_err!(platform::uninstall_me()); + hbb_common::allow_err!(platform::install_me("desktopicon startmenu",)); + return; + } + } + if args[0] == "--remove" { + if args.len() == 2 { + // sleep a while so that process of removed exe exit + std::thread::sleep(std::time::Duration::from_secs(1)); + std::fs::remove_file(&args[1]).ok(); + return; + } + } else if args[0] == "--service" { + log::info!("start --service"); + start_os_service(); + return; + } else if args[0] == "--server" { + log::info!("start --server"); + start_server(true, true); + return; + } else if args[0] == "--import-config" { + if args.len() == 2 { + hbb_common::config::Config::import(&args[1]); + } + return; + } else if args[0] == "--password" { + if args.len() == 2 { + ipc::set_password(args[1].to_owned()).unwrap(); + } + return; + } + } + ui::start(&mut args[..]); +} + +#[cfg(feature = "cli")] +fn main() { + use clap::App; + let args = format!( + "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -s, --server... 'Start server'", + ); + let matches = App::new("rustdesk") + .version(crate::VERSION) + .author("CarrieZ Studio") + .about("RustDesk command line tool") + .args_from_usage(&args) + .get_matches(); + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + if let Some(p) = matches.value_of("port-forward") { + let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); + if options.len() < 3 { + log::error!("Wrong port-forward options"); + return; + } + let mut port = 0; + if let Ok(v) = options[1].parse::() { + port = v; + } else { + log::error!("Wrong local-port"); + return; + } + let mut remote_port = 0; + if let Ok(v) = options[2].parse::() { + remote_port = v; + } else { + log::error!("Wrong remote-port"); + return; + } + let mut remote_host = "localhost".to_owned(); + if options.len() > 3 { + remote_host = options[3].clone(); + } + cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port); + } +} diff --git a/rust-rdp/rust-desk/src/platform/linux.rs b/rust-rdp/rust-desk/src/platform/linux.rs new file mode 100644 index 0000000..a376c9d --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/linux.rs @@ -0,0 +1,654 @@ +use super::{CursorData, ResultType}; +use hbb_common::{allow_err, bail, log}; +use libc::{c_char, c_int, c_void}; +use std::io::prelude::*; +use std::{ + cell::RefCell, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; +type Xdo = *const c_void; + +pub const PA_SAMPLE_RATE: u32 = 48000; +static mut UNMODIFIED: bool = true; + +thread_local! { + static XDO: RefCell = RefCell::new(unsafe { xdo_new(std::ptr::null()) }); + static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); +} + +extern "C" { + fn xdo_get_mouse_location( + xdo: Xdo, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, + ) -> c_int; + fn xdo_new(display: *const c_char) -> Xdo; +} + +#[link(name = "X11")] +extern "C" { + fn XOpenDisplay(display_name: *const c_char) -> *mut c_void; + // fn XCloseDisplay(d: *mut c_void) -> c_int; +} + +#[link(name = "Xfixes")] +extern "C" { + // fn XFixesQueryExtension(dpy: *mut c_void, event: *mut c_int, error: *mut c_int) -> c_int; + fn XFixesGetCursorImage(dpy: *mut c_void) -> *const xcb_xfixes_get_cursor_image; + fn XFree(data: *mut c_void); +} + +// /usr/include/X11/extensions/Xfixes.h +#[repr(C)] +pub struct xcb_xfixes_get_cursor_image { + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub xhot: u16, + pub yhot: u16, + pub cursor_serial: libc::c_long, + pub pixels: *const libc::c_long, +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + let mut res = None; + XDO.with(|xdo| { + if let Ok(xdo) = xdo.try_borrow_mut() { + if xdo.is_null() { + return; + } + let mut x: c_int = 0; + let mut y: c_int = 0; + unsafe { + xdo_get_mouse_location(*xdo, &mut x as _, &mut y as _, std::ptr::null_mut()); + } + res = Some((x, y)); + } + }); + res +} + +pub fn reset_input_cache() {} + +pub fn get_cursor() -> ResultType> { + let mut res = None; + DISPLAY.with(|conn| { + if let Ok(d) = conn.try_borrow_mut() { + if !d.is_null() { + unsafe { + let img = XFixesGetCursorImage(*d); + if !img.is_null() { + res = Some((*img).cursor_serial as u64); + XFree(img as _); + } + } + } + } + }); + Ok(res) +} + +pub fn get_cursor_data(hcursor: u64) -> ResultType { + let mut res = None; + DISPLAY.with(|conn| { + if let Ok(ref mut d) = conn.try_borrow_mut() { + if !d.is_null() { + unsafe { + let img = XFixesGetCursorImage(**d); + if !img.is_null() && hcursor == (*img).cursor_serial as u64 { + let mut cd: CursorData = Default::default(); + cd.hotx = (*img).xhot as _; + cd.hoty = (*img).yhot as _; + cd.width = (*img).width as _; + cd.height = (*img).height as _; + // to-do: how about if it is 0 + cd.id = (*img).cursor_serial as _; + let pixels = + std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _); + cd.colors.resize(pixels.len() * 4, 0); + for y in 0..cd.height { + for x in 0..cd.width { + let pos = (y * cd.width + x) as usize; + let p = pixels[pos]; + let a = (p >> 24) & 0xff; + let r = (p >> 16) & 0xff; + let g = (p >> 8) & 0xff; + let b = (p >> 0) & 0xff; + if a == 0 { + continue; + } + let pos = pos * 4; + cd.colors[pos] = r as _; + cd.colors[pos + 1] = g as _; + cd.colors[pos + 2] = b as _; + cd.colors[pos + 3] = a as _; + } + } + res = Some(cd); + } + if !img.is_null() { + XFree(img as _); + } + } + } + } + }); + match res { + Some(x) => Ok(x), + _ => bail!("Failed to get cursor image of {}", hcursor), + } +} + +pub fn start_os_service() { + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + let mut uid = "".to_owned(); + let mut server: Option = None; + if let Err(err) = ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) { + println!("Failed to set Ctrl-C handler: {}", err); + } + + let mut cm0 = false; + let mut last_restart = std::time::Instant::now(); + while running.load(Ordering::SeqCst) { + let cm = get_cm(); + let tmp = get_active_userid(); + let mut start_new = false; + if tmp != uid && !tmp.is_empty() { + uid = tmp; + log::info!("uid of seat0: {}", uid); + let gdm = format!("/run/user/{}/gdm/Xauthority", uid); + let mut auth = get_env_tries("XAUTHORITY", &uid, 10); + if auth.is_empty() { + auth = if std::path::Path::new(&gdm).exists() { + gdm + } else { + let username = get_active_username(); + if username == "root" { + format!("/{}/.Xauthority", username) + } else { + let tmp = format!("/home/{}/.Xauthority", username); + if std::path::Path::new(&tmp).exists() { + tmp + } else { + format!("/var/lib/{}/.Xauthority", username) + } + } + }; + } + let mut d = get_env("DISPLAY", &uid); + if d.is_empty() { + d = get_display(); + } + if d.is_empty() { + d = ":0".to_owned(); + } + d = d.replace(&whoami::hostname(), "").replace("localhost", ""); + log::info!("DISPLAY: {}", d); + log::info!("XAUTHORITY: {}", auth); + std::env::set_var("XAUTHORITY", auth); + std::env::set_var("DISPLAY", d); + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + last_restart = std::time::Instant::now(); + } + } else if !cm + && ((cm0 && last_restart.elapsed().as_secs() > 60) + || last_restart.elapsed().as_secs() > 3600) + { + // restart server if new connections all closed, or every one hour, + // as a workaround to resolve "SpotUdp" (dns resolve) + // and x server get displays failure issue + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + last_restart = std::time::Instant::now(); + log::info!("restart server"); + } + } + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + if start_new { + match crate::run_me(vec!["--server"]) { + Ok(ps) => server = Some(ps), + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } + } + cm0 = cm; + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + } + + if let Some(ps) = server.take().as_mut() { + allow_err!(ps.kill()); + } + log::info!("Exit"); +} + +fn get_active_userid() -> String { + get_value_of_seat0(1) +} + +fn is_active(sid: &str) -> bool { + if let Ok(output) = std::process::Command::new("loginctl") + .args(vec!["show-session", "-p", "State", sid]) + .output() + { + String::from_utf8_lossy(&output.stdout).contains("active") + } else { + false + } +} + +fn get_cm() -> bool { + if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains(&format!( + "{} --cm", + std::env::current_exe() + .unwrap_or("".into()) + .to_string_lossy() + )) { + return true; + } + } + } + false +} + +fn get_display() -> String { + let user = get_active_username(); + log::debug!("w {}", &user); + if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + log::debug!(" {}", line); + let mut iter = line.split_whitespace(); + let b = iter.nth(2); + if let Some(b) = b { + if b.starts_with(":") { + return b.to_owned(); + } + } + } + } + // above not work for gdm user + log::debug!("ls -l /tmp/.X11-unix/"); + let mut last = "".to_owned(); + if let Ok(output) = std::process::Command::new("ls") + .args(vec!["-l", "/tmp/.X11-unix/"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + log::debug!(" {}", line); + let mut iter = line.split_whitespace(); + let user_field = iter.nth(2); + if let Some(x) = iter.last() { + if x.starts_with("X") { + last = x.replace("X", ":").to_owned(); + if user_field == Some(&user) { + return last; + } + } + } + } + } + last +} + +fn get_value_of_seat0(i: usize) -> String { + if let Ok(output) = std::process::Command::new("loginctl").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("seat0") { + if let Some(sid) = line.split_whitespace().nth(0) { + if is_active(sid) { + if let Some(uid) = line.split_whitespace().nth(i) { + return uid.to_owned(); + } + } + } + } + } + } + + // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 + if let Ok(output) = std::process::Command::new("loginctl").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(sid) = line.split_whitespace().nth(0) { + let d = get_display_server_of_session(sid); + if is_active(sid) && d != "tty" { + if let Some(uid) = line.split_whitespace().nth(i) { + return uid.to_owned(); + } + } + } + } + } + + return "".to_owned(); +} + +pub fn get_display_server() -> String { + let session = get_value_of_seat0(0); + get_display_server_of_session(&session) +} + +fn get_display_server_of_session(session: &str) -> String { + if let Ok(output) = std::process::Command::new("loginctl") + .args(vec!["show-session", "-p", "Type", session]) + .output() + // Check session type of the session + { + let display_server = String::from_utf8_lossy(&output.stdout) + .replace("Type=", "") + .trim_end() + .into(); + if display_server == "tty" { + // If the type is tty... + if let Ok(output) = std::process::Command::new("loginctl") + .args(vec!["show-session", "-p", "TTY", session]) + .output() + // Get the tty number + { + let tty: String = String::from_utf8_lossy(&output.stdout) + .replace("TTY=", "") + .trim_end() + .into(); + if let Ok(Some(xorg_results)) = + run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) + // And check if Xorg is running on that tty + { + if xorg_results.trim_end().to_string() != "" { + // If it is, manually return "x11", otherwise return tty + "x11".to_owned() + } else { + display_server + } + } else { + // If any of these commands fail just fall back to the display server + display_server + } + } else { + display_server + } + } else { + // If the session is not a tty, then just return the type as usual + display_server + } + } else { + "".to_owned() + } +} + +pub fn is_login_wayland() -> bool { + if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { + contents.contains("#WaylandEnable=false") + } else if let Ok(contents) = std::fs::read_to_string("/etc/gdm/custom.conf") { + contents.contains("#WaylandEnable=false") + } else { + false + } +} + +pub fn fix_login_wayland() { + let mut file = "/etc/gdm3/custom.conf".to_owned(); + if !std::path::Path::new(&file).exists() { + file = "/etc/gdm/custom.conf".to_owned(); + } + match std::process::Command::new("pkexec") + .args(vec![ + "sed", + "-i", + "s/#WaylandEnable=false/WaylandEnable=false/g", + &file, + ]) + .output() + { + Ok(x) => { + let x = String::from_utf8_lossy(&x.stderr); + if !x.is_empty() { + log::error!("fix_login_wayland failed: {}", x); + } + } + Err(err) => { + log::error!("fix_login_wayland failed: {}", err); + } + } +} + +pub fn current_is_wayland() -> bool { + let dtype = get_display_server(); + return "wayland" == dtype && unsafe { UNMODIFIED }; +} + +pub fn modify_default_login() -> String { + let dsession = std::env::var("DESKTOP_SESSION").unwrap(); + let user_name = std::env::var("USERNAME").unwrap(); + if let Ok(Some(x)) = + run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION}-xorg.desktop".to_owned()) + { + if x.trim_end().to_string() != "" { + match std::process::Command::new("pkexec") + .args(vec![ + "sed", + "-i", + &format!("s/={0}$/={0}-xorg/g", &dsession), + &format!("/var/lib/AccountsService/users/{}", &user_name), + ]) + .output() + { + Ok(x) => { + let x = String::from_utf8_lossy(&x.stderr); + if !x.is_empty() { + log::error!("modify_default_login failed: {}", x); + return "Fix failed! Please re-login with X server manually".to_owned(); + } else { + unsafe { + UNMODIFIED = false; + } + return "".to_owned(); + } + } + Err(err) => { + log::error!("modify_default_login failed: {}", err); + return "Fix failed! Please re-login with X server manually".to_owned(); + } + } + } else if let Ok(Some(z)) = + run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION:0:-8}.desktop".to_owned()) + { + if z.trim_end().to_string() != "" { + match std::process::Command::new("pkexec") + .args(vec![ + "sed", + "-i", + &format!("s/={}$/={}/g", &dsession, &dsession[..dsession.len() - 8]), + &format!("/var/lib/AccountsService/users/{}", &user_name), + ]) + .output() + { + Ok(x) => { + let x = String::from_utf8_lossy(&x.stderr); + if !x.is_empty() { + log::error!("modify_default_login failed: {}", x); + return "Fix failed! Please re-login with X server manually".to_owned(); + } else { + unsafe { + UNMODIFIED = false; + } + return "".to_owned(); + } + } + Err(err) => { + log::error!("modify_default_login failed: {}", err); + return "Fix failed! Please re-login with X server manually".to_owned(); + } + } + } + } + } + return "Fix failed! Please re-login with X server manually".to_owned(); +} + +// to-do: test the other display manager +fn _get_display_manager() -> String { + if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") { + if let Some(x) = x.split("/").last() { + return x.to_owned(); + } + } + "gdm3".to_owned() +} + +pub fn get_active_username() -> String { + get_value_of_seat0(2) +} + +pub fn is_prelogin() -> bool { + let n = get_active_userid().len(); + n < 4 && n > 1 +} + +pub fn is_root() -> bool { + crate::username() == "root" +} + +pub fn run_as_user(arg: &str) -> ResultType> { + let uid = get_active_userid(); + let cmd = std::env::current_exe()?; + let task = std::process::Command::new("sudo") + .args(vec![ + &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str, + "-u", + &get_active_username(), + cmd.to_str().unwrap_or(""), + arg, + ]) + .spawn()?; + Ok(Some(task)) +} + +pub fn get_pa_monitor() -> String { + get_pa_sources() + .drain(..) + .map(|x| x.0) + .filter(|x| x.contains("monitor")) + .next() + .unwrap_or("".to_owned()) +} + +pub fn get_pa_source_name(desc: &str) -> String { + get_pa_sources() + .drain(..) + .filter(|x| x.1 == desc) + .map(|x| x.0) + .next() + .unwrap_or("".to_owned()) +} + +pub fn get_pa_sources() -> Vec<(String, String)> { + use pulsectl::controllers::*; + let mut out = Vec::new(); + match SourceController::create() { + Ok(mut handler) => { + if let Ok(devices) = handler.list_devices() { + for dev in devices.clone() { + out.push(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + } + Err(err) => { + log::error!("Failed to get_pa_sources: {:?}", err); + } + } + out +} + +pub fn lock_screen() { + std::thread::spawn(move || { + use crate::server::input_service::handle_key; + use hbb_common::message_proto::*; + let mut evt = KeyEvent { + down: true, + modifiers: vec![ControlKey::Meta.into()], + ..Default::default() + }; + evt.set_chr('l' as _); + handle_key(&evt); + evt.down = false; + handle_key(&evt); + }); +} + +pub fn toggle_blank_screen(_v: bool) { + // https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x +} + +pub fn block_input(_v: bool) -> bool { + true +} + +pub fn is_installed() -> bool { + true +} + +fn run_cmds(cmds: String) -> ResultType> { + let mut tmp = std::env::temp_dir(); + tmp.push(format!( + "{}_{}", + hbb_common::config::APP_NAME, + crate::get_time() + )); + let mut file = std::fs::File::create(&tmp)?; + file.write_all(cmds.as_bytes())?; + file.sync_all()?; + if let Ok(output) = std::process::Command::new("bash") + .arg(tmp.to_str().unwrap_or("")) + .output() + { + Ok(Some(String::from_utf8_lossy(&output.stdout).to_string())) + } else { + Ok(None) + } +} + +fn get_env_tries(name: &str, uid: &str, n: usize) -> String { + for _ in 0..n { + let x = get_env(name, uid); + if !x.is_empty() { + return x; + } + std::thread::sleep(std::time::Duration::from_millis(300)); + } + "".to_owned() +} + +fn get_env(name: &str, uid: &str) -> String { + let cmd = format!("ps -u {} -o pid= | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, name, name); + log::debug!("Run: {}", &cmd); + if let Ok(Some(x)) = run_cmds(cmd) { + x.trim_end().to_string() + } else { + "".to_owned() + } +} diff --git a/rust-rdp/rust-desk/src/platform/macos.rs b/rust-rdp/rust-desk/src/platform/macos.rs new file mode 100644 index 0000000..4ef995a --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/macos.rs @@ -0,0 +1,432 @@ +// https://developer.apple.com/documentation/appkit/nscursor +// https://github.com/servo/core-foundation-rs +// https://github.com/rust-windowing/winit + +use super::{CursorData, ResultType}; +use cocoa::{ + base::{id, nil, BOOL, NO, YES}, + foundation::{NSDictionary, NSPoint, NSSize, NSString}, +}; +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + dictionary::CFDictionaryRef, + string::CFStringRef, +}; +use core_graphics::{ + display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, + window::{kCGWindowName, kCGWindowOwnerPID}, +}; +use hbb_common::{bail, log}; +use include_dir::{include_dir, Dir}; +use objc::{class, msg_send, sel, sel_impl}; +use scrap::{libc::c_void, quartz::ffi::*}; + +static PRIVILEGES_SCRIPTS_DIR: Dir = + include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); +static mut LATEST_SEED: i32 = 0; + +extern "C" { + fn CGSCurrentCursorSeed() -> i32; + fn CGEventCreate(r: *const c_void) -> *const c_void; + fn CGEventGetLocation(e: *const c_void) -> CGPoint; + static kAXTrustedCheckOptionPrompt: CFStringRef; + fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; +} + +pub fn is_process_trusted(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + let value: id = msg_send![class!(NSNumber), numberWithBool: value]; + let options = NSDictionary::dictionaryWithObject_forKey_( + nil, + value, + kAXTrustedCheckOptionPrompt as _, + ); + AXIsProcessTrustedWithOptions(options as _) == YES + } +} + +// macOS >= 10.15 +// https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ +// remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk +pub fn is_can_screen_recording(prompt: bool) -> bool { + let mut can_record_screen: bool = false; + unsafe { + let our_pid: i32 = std::process::id() as _; + let our_pid: id = msg_send![class!(NSNumber), numberWithInteger: our_pid]; + let window_list = + CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); + let n = CFArrayGetCount(window_list); + let dock = NSString::alloc(nil).init_str("Dock"); + for i in 0..n { + let w: id = CFArrayGetValueAtIndex(window_list, i) as _; + let name: id = msg_send![w, valueForKey: kCGWindowName as id]; + if name.is_null() { + continue; + } + let pid: id = msg_send![w, valueForKey: kCGWindowOwnerPID as id]; + let is_me: BOOL = msg_send![pid, isEqual: our_pid]; + if is_me == YES { + continue; + } + let pid: i32 = msg_send![pid, intValue]; + let p: id = msg_send![ + class!(NSRunningApplication), + runningApplicationWithProcessIdentifier: pid + ]; + if p.is_null() { + // ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar" + continue; + } + let url: id = msg_send![p, executableURL]; + let exe_name: id = msg_send![url, lastPathComponent]; + if exe_name.is_null() { + continue; + } + let is_dock: BOOL = msg_send![exe_name, isEqual: dock]; + if is_dock == YES { + // ignore the Dock, which provides the desktop picture + continue; + } + can_record_screen = true; + break; + } + } + if !can_record_screen && prompt { + use scrap::{Capturer, Display}; + if let Ok(d) = Display::primary() { + Capturer::new(d, true).ok(); + } + } + can_record_screen +} + +pub fn is_installed_daemon(prompt: bool) -> bool { + let daemon = format!("{}_service.plist", crate::get_full_name()); + let agent = format!("{}_server.plist", crate::get_full_name()); + if !prompt { + if !std::path::Path::new(&format!("/Library/LaunchDaemons/{}", daemon)).exists() { + return false; + } + if !std::path::Path::new(&format!("/Library/LaunchAgents/{}", agent)).exists() { + return false; + } + return true; + } + + let install_script = PRIVILEGES_SCRIPTS_DIR.get_file("install.scpt").unwrap(); + let install_script_body = install_script.contents_utf8().unwrap(); + + let daemon_plist = PRIVILEGES_SCRIPTS_DIR.get_file(&daemon).unwrap(); + let daemon_plist_body = daemon_plist.contents_utf8().unwrap(); + + let agent_plist = PRIVILEGES_SCRIPTS_DIR.get_file(&agent).unwrap(); + let agent_plist_body = agent_plist.contents_utf8().unwrap(); + + match std::process::Command::new("osascript") + .arg("-e") + .arg(install_script_body) + .arg(daemon_plist_body) + .arg(agent_plist_body) + .arg(&get_active_username()) + .spawn() + { + Ok(_) => { + std::process::exit(0); + } + Err(e) => { + log::error!("run osascript failed: {}", e); + false + } + } +} + +pub fn launch(load: bool) { + // to-do: do together with win/linux about refactory start/stop service + if !is_installed() || !is_installed_daemon(false) { + return; + } + let mut script_filename = "load.scpt"; + if !load { + script_filename = "unload.scpt"; + } + + let script_file = PRIVILEGES_SCRIPTS_DIR.get_file(script_filename).unwrap(); + let script_body = script_file.contents_utf8().unwrap(); + + std::process::Command::new("osascript") + .arg("-e") + .arg(script_body) + .arg(&get_active_username()) + .spawn() + .ok(); +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + unsafe { + let e = CGEventCreate(0 as _); + let point = CGEventGetLocation(e); + CFRelease(e); + Some((point.x as _, point.y as _)) + } + /* + let mut pt: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] }; + let screen: id = unsafe { msg_send![class!(NSScreen), currentScreenForMouseLocation] }; + let frame: NSRect = unsafe { msg_send![screen, frame] }; + pt.x -= frame.origin.x; + pt.y -= frame.origin.y; + Some((pt.x as _, pt.y as _)) + */ +} + +pub fn get_cursor() -> ResultType> { + unsafe { + let seed = CGSCurrentCursorSeed(); + if seed == LATEST_SEED { + return Ok(None); + } + LATEST_SEED = seed; + } + let c = get_cursor_id()?; + Ok(Some(c.1)) +} + +pub fn reset_input_cache() { + unsafe { + LATEST_SEED = 0; + } +} + +fn get_cursor_id() -> ResultType<(id, u64)> { + unsafe { + let c: id = msg_send![class!(NSCursor), currentSystemCursor]; + if c == nil { + bail!("Failed to call [NSCursor currentSystemCursor]"); + } + let hotspot: NSPoint = msg_send![c, hotSpot]; + let img: id = msg_send![c, image]; + if img == nil { + bail!("Failed to call [NSCursor image]"); + } + let size: NSSize = msg_send![img, size]; + let tif: id = msg_send![img, TIFFRepresentation]; + if tif == nil { + bail!("Failed to call [NSImage TIFFRepresentation]"); + } + let rep: id = msg_send![class!(NSBitmapImageRep), imageRepWithData: tif]; + if rep == nil { + bail!("Failed to call [NSBitmapImageRep imageRepWithData]"); + } + let rep_size: NSSize = msg_send![rep, size]; + let mut hcursor = + size.width + size.height + hotspot.x + hotspot.y + rep_size.width + rep_size.height; + let x = (rep_size.width * hotspot.x / size.width) as usize; + let y = (rep_size.height * hotspot.y / size.height) as usize; + for i in 0..2 { + let mut x2 = x + i; + if x2 >= rep_size.width as usize { + x2 = rep_size.width as usize - 1; + } + let mut y2 = y + i; + if y2 >= rep_size.height as usize { + y2 = rep_size.height as usize - 1; + } + let color: id = msg_send![rep, colorAtX:x2 y:y2]; + if color != nil { + let r: f64 = msg_send![color, redComponent]; + let g: f64 = msg_send![color, greenComponent]; + let b: f64 = msg_send![color, blueComponent]; + let a: f64 = msg_send![color, alphaComponent]; + hcursor += (r + g + b + a) * (255 << i) as f64; + } + } + Ok((c, hcursor as _)) + } +} + +// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c +pub fn get_cursor_data(hcursor: u64) -> ResultType { + unsafe { + let (c, hcursor2) = get_cursor_id()?; + if hcursor != hcursor2 { + bail!("cursor changed"); + } + let hotspot: NSPoint = msg_send![c, hotSpot]; + let img: id = msg_send![c, image]; + let size: NSSize = msg_send![img, size]; + let reps: id = msg_send![img, representations]; + if reps == nil { + bail!("Failed to call [NSImage representations]"); + } + let nreps: usize = msg_send![reps, count]; + if nreps == 0 { + bail!("Get empty [NSImage representations]"); + } + let rep: id = msg_send![reps, objectAtIndex: 0]; + /* + let n: id = msg_send![class!(NSNumber), numberWithFloat:1.0]; + let props: id = msg_send![class!(NSDictionary), dictionaryWithObject:n forKey:NSString::alloc(nil).init_str("NSImageCompressionFactor")]; + let image_data: id = msg_send![rep, representationUsingType:2 properties:props]; + let () = msg_send![image_data, writeToFile:NSString::alloc(nil).init_str("cursor.jpg") atomically:0]; + */ + let mut colors: Vec = Vec::new(); + colors.reserve((size.height * size.width) as usize * 4); + // TIFF is rgb colrspace, no need to convert + // let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace]; + for y in 0..(size.height as _) { + for x in 0..(size.width as _) { + let color: id = msg_send![rep, colorAtX:x y:y]; + // let color: id = msg_send![color, colorUsingColorSpace: cs]; + if color == nil { + continue; + } + let r: f64 = msg_send![color, redComponent]; + let g: f64 = msg_send![color, greenComponent]; + let b: f64 = msg_send![color, blueComponent]; + let a: f64 = msg_send![color, alphaComponent]; + colors.push((r * 255.) as _); + colors.push((g * 255.) as _); + colors.push((b * 255.) as _); + colors.push((a * 255.) as _); + } + } + Ok(CursorData { + id: hcursor, + colors, + hotx: hotspot.x as _, + hoty: hotspot.y as _, + width: size.width as _, + height: size.height as _, + ..Default::default() + }) + } +} + +fn get_active_user(t: &str) -> String { + if let Ok(output) = std::process::Command::new("ls") + .args(vec![t, "/dev/console"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(n) = line.split_whitespace().nth(2) { + return n.to_owned(); + } + } + } + "".to_owned() +} + +pub fn get_active_username() -> String { + get_active_user("-l") +} + +pub fn get_active_userid() -> String { + get_active_user("-n") +} + +pub fn is_prelogin() -> bool { + get_active_userid() == "0" +} + +pub fn is_root() -> bool { + crate::username() == "root" +} + +pub fn run_as_user(arg: &str) -> ResultType> { + let uid = get_active_userid(); + let cmd = std::env::current_exe()?; + let task = std::process::Command::new("launchctl") + .args(vec!["asuser", &uid, cmd.to_str().unwrap_or(""), arg]) + .spawn()?; + Ok(Some(task)) +} + +pub fn lock_screen() { + std::process::Command::new( + "/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession", + ) + .arg("-suspend") + .output() + .ok(); +} + +pub fn start_os_service() { + log::info!("{}", crate::username()); + if let Err(err) = crate::ipc::start("_service") { + log::error!("Failed to start ipc_service: {}", err); + } + + /* // somehow, below works fine if user logged in, but mouse/keyboard not work under prelogin. + // one solution to run --server as agent in prelogin, + // and run --server under --service via run_as_user if not in prelogin + // so that no multiple --server if multiple logged-in users + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + let mut uid = "".to_owned(); + let mut server: Option = None; + if let Err(err) = ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) { + println!("Failed to set Ctrl-C handler: {}", err); + } + while running.load(Ordering::SeqCst) { + let tmp = get_active_userid(); + let mut start_new = false; + if tmp != uid && !tmp.is_empty() { + uid = tmp; + log::info!("active uid: {}", uid); + if let Some(ps) = server.as_mut() { + hbb_common::allow_err!(ps.kill()); + } + } + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + if start_new { + match run_as_user("--server") { + Ok(Some(ps)) => server = Some(ps), + Err(err) => { + log::error!("Failed to start server: {}", err); + } + _ => { /*no hapen*/ } + } + } + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + } + + if let Some(ps) = server.take().as_mut() { + hbb_common::allow_err!(ps.kill()); + } + log::info!("Exit"); + */ +} + +pub fn toggle_blank_screen(_v: bool) { + // https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily +} + +pub fn block_input(_v: bool) -> bool { + true +} + +pub fn is_installed() -> bool { + if let Ok(p) = std::env::current_exe() { + return p.to_str().unwrap_or_default().contains(&format!( + "/Applications/{}.app", + hbb_common::config::APP_NAME + )); + } + false +} diff --git a/rust-rdp/rust-desk/src/platform/mod.rs b/rust-rdp/rust-desk/src/platform/mod.rs new file mode 100644 index 0000000..af94387 --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/mod.rs @@ -0,0 +1,57 @@ +#[cfg(target_os = "linux")] +pub use linux::*; +#[cfg(target_os = "macos")] +pub use macos::*; +#[cfg(windows)] +pub use windows::*; + +#[cfg(windows)] +pub mod windows; + +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(target_os = "linux")] +pub mod linux; + +use hbb_common::{message_proto::CursorData, ResultType}; +const SERVICE_INTERVAL: u64 = 300; + +pub fn is_xfce() -> bool { + #[cfg(target_os = "linux")] + { + return std::env::var_os("XDG_CURRENT_DESKTOP") == Some(std::ffi::OsString::from("XFCE")); + } + #[cfg(not(target_os = "linux"))] + { + return false; + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_cursor_data() { + for _ in 0..30 { + if let Some(hc) = get_cursor().unwrap() { + let cd = get_cursor_data(hc).unwrap(); + repng::encode( + std::fs::File::create("cursor.png").unwrap(), + cd.width as _, + cd.height as _, + &cd.colors[..], + ) + .unwrap(); + } + #[cfg(target_os = "macos")] + macos::is_process_trusted(false); + } + } + #[test] + fn test_get_cursor_pos() { + for _ in 0..30 { + assert!(!get_cursor_pos().is_none()); + } + } +} diff --git a/rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_server.plist b/rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_server.plist new file mode 100644 index 0000000..109cf47 --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_server.plist @@ -0,0 +1,29 @@ + + + + + Label + com.carriez.RustDesk_server + LimitLoadToSessionType + + LoginWindow + Aqua + + KeepAlive + + SuccessfulExit + + AfterInitialDemand + + + RunAtLoad + + ProgramArguments + + /Applications/RustDesk.app/Contents/MacOS/rustdesk + --server + + WorkingDirectory + /Applications/RustDesk.app/Contents/MacOS/ + + diff --git a/rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_service.plist b/rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_service.plist new file mode 100644 index 0000000..9502e95 --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/privileges_scripts/com.carriez.RustDesk_service.plist @@ -0,0 +1,19 @@ + + + + + Label + com.carriez.RustDesk_service + KeepAlive + + ProgramArguments + + /Applications/RustDesk.app/Contents/MacOS/rustdesk + --service + + RunAtLoad + + WorkingDirectory + /Applications/RustDesk.app/Contents/MacOS/ + + diff --git a/rust-rdp/rust-desk/src/platform/privileges_scripts/install.scpt b/rust-rdp/rust-desk/src/platform/privileges_scripts/install.scpt new file mode 100644 index 0000000..12bbdcd --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/privileges_scripts/install.scpt @@ -0,0 +1,19 @@ +on run {daemon_file, agent_file, user} + + set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + + set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;" + + set sh3 to "cp -rf /Users/" & user & "/Library/Preferences/com.carriez.RustDesk/RustDesk.toml /var/root/Library/Preferences/com.carriez.RustDesk/;" + + set sh4 to "cp -rf /Users/" & user & "/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml /var/root/Library/Preferences/com.carriez.RustDesk/;" + + set sh5 to "launchctl unload -w /Library/LaunchAgents/com.carriez.RustDesk_server.plist; launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + + set sh6 to "pkill -f rustdesk; launchctl unload -w /Library/LaunchAgents/com.carriez.RustDesk_server.plist; launchctl load -w /Library/LaunchAgents/com.carriez.RustDesk_server.plist; open /Applications/RustDesk.app" + + set sh to sh1 & sh2 & sh3 & sh4 & sh5 + + do shell script sh with prompt "RustDesk want to install daemon and agent" with administrator privileges + do shell script sh6 +end run diff --git a/rust-rdp/rust-desk/src/platform/privileges_scripts/load.scpt b/rust-rdp/rust-desk/src/platform/privileges_scripts/load.scpt new file mode 100644 index 0000000..a725af2 --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/privileges_scripts/load.scpt @@ -0,0 +1,16 @@ +on run {user} + + set sh1 to "cp -rf /Users/" & user & "/Library/Preferences/com.carriez.RustDesk/RustDesk.toml /var/root/Library/Preferences/com.carriez.RustDesk/;" + + set sh2 to "cp -rf /Users/" & user & "/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml /var/root/Library/Preferences/com.carriez.RustDesk/;" + + set sh3 to "launchctl load -w /Library/LaunchDaemons/com.carriez.rustdesk_service.plist;" + + set sh4 to "launchctl load -w /Library/LaunchAgents/com.carriez.rustdesk_server.plist;" + + set sh to sh1 & sh2 & sh3 + + do shell script sh with prompt "RustDesk want to launch daemon" with administrator privileges + do shell script sh4 + +end run diff --git a/rust-rdp/rust-desk/src/platform/privileges_scripts/unload.scpt b/rust-rdp/rust-desk/src/platform/privileges_scripts/unload.scpt new file mode 100644 index 0000000..f0c143f --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/privileges_scripts/unload.scpt @@ -0,0 +1,6 @@ +set sh1 to "launchctl unload -w /Library/LaunchDaemons/com.carriez.rustdesk_service.plist;" +set sh2 to "launchctl unload -w /Library/LaunchAgents/com.carriez.rustdesk_server.plist;" + +do shell script sh1 with prompt "RustDesk want to unload daemon" with administrator privileges +do shell script sh2 + diff --git a/rust-rdp/rust-desk/src/platform/windows.rs b/rust-rdp/rust-desk/src/platform/windows.rs new file mode 100644 index 0000000..1bc7fdb --- /dev/null +++ b/rust-rdp/rust-desk/src/platform/windows.rs @@ -0,0 +1,1098 @@ +use super::{CursorData, ResultType}; +use crate::ipc; +use hbb_common::{ + allow_err, bail, + config::{Config, APP_NAME}, + log, sleep, timeout, tokio, +}; +use std::io::prelude::*; +use std::{ + ffi::OsString, + io, mem, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; +use winapi::{ + shared::{minwindef::*, ntdef::NULL, windef::*}, + um::{ + errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, + processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*, + }, +}; +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, +}; + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + unsafe { + let mut out = mem::MaybeUninit::uninit().assume_init(); + if GetCursorPos(&mut out) == FALSE { + return None; + } + return Some((out.x, out.y)); + } +} + +pub fn reset_input_cache() {} + +pub fn get_cursor() -> ResultType> { + unsafe { + let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init(); + ci.cbSize = std::mem::size_of::() as _; + if GetCursorInfo(&mut ci) == FALSE { + return Err(io::Error::last_os_error().into()); + } + if ci.flags & CURSOR_SHOWING == 0 { + Ok(None) + } else { + Ok(Some(ci.hCursor as _)) + } + } +} + +struct IconInfo(ICONINFO); + +impl IconInfo { + fn new(icon: HICON) -> ResultType { + unsafe { + let mut ii = mem::MaybeUninit::uninit().assume_init(); + if GetIconInfo(icon, &mut ii) == FALSE { + Err(io::Error::last_os_error().into()) + } else { + let ii = Self(ii); + if ii.0.hbmMask.is_null() { + bail!("Cursor bitmap handle is NULL"); + } + return Ok(ii); + } + } + } + + fn is_color(&self) -> bool { + !self.0.hbmColor.is_null() + } +} + +impl Drop for IconInfo { + fn drop(&mut self) { + unsafe { + if !self.0.hbmColor.is_null() { + DeleteObject(self.0.hbmColor as _); + } + if !self.0.hbmMask.is_null() { + DeleteObject(self.0.hbmMask as _); + } + } + } +} + +// https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp +// https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx +pub fn get_cursor_data(hcursor: u64) -> ResultType { + unsafe { + let mut ii = IconInfo::new(hcursor as _)?; + let bm_mask = get_bitmap(ii.0.hbmMask)?; + let mut width = bm_mask.bmWidth; + let mut height = if ii.is_color() { + bm_mask.bmHeight + } else { + bm_mask.bmHeight / 2 + }; + let cbits_size = width * height * 4; + if cbits_size < 16 { + bail!("Invalid icon: too small"); // solve some crash + } + let mut cbits: Vec = Vec::new(); + cbits.resize(cbits_size as _, 0); + let mut mbits: Vec = Vec::new(); + mbits.resize((bm_mask.bmWidthBytes * bm_mask.bmHeight) as _, 0); + let r = GetBitmapBits(ii.0.hbmMask, mbits.len() as _, mbits.as_mut_ptr() as _); + if r == 0 { + bail!("Failed to copy bitmap data"); + } + if r != (mbits.len() as i32) { + bail!( + "Invalid mask cursor buffer size, got {} bytes, expected {}", + r, + mbits.len() + ); + } + let do_outline; + if ii.is_color() { + get_rich_cursor_data(ii.0.hbmColor, width, height, &mut cbits)?; + do_outline = fix_cursor_mask( + &mut mbits, + &mut cbits, + width as _, + height as _, + bm_mask.bmWidthBytes as _, + ); + } else { + do_outline = handleMask( + cbits.as_mut_ptr(), + mbits.as_ptr(), + width, + height, + bm_mask.bmWidthBytes, + bm_mask.bmHeight, + ) > 0; + } + if do_outline { + let mut outline = Vec::new(); + outline.resize(((width + 2) * (height + 2) * 4) as _, 0); + drawOutline( + outline.as_mut_ptr(), + cbits.as_ptr(), + width, + height, + outline.len() as _, + ); + cbits = outline; + width += 2; + height += 2; + ii.0.xHotspot += 1; + ii.0.yHotspot += 1; + } + + Ok(CursorData { + id: hcursor, + colors: cbits, + hotx: ii.0.xHotspot as _, + hoty: ii.0.yHotspot as _, + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +#[inline] +fn get_bitmap(handle: HBITMAP) -> ResultType { + unsafe { + let mut bm: BITMAP = mem::zeroed(); + if GetObjectA( + handle as _, + std::mem::size_of::() as _, + &mut bm as *mut BITMAP as *mut _, + ) == FALSE + { + return Err(io::Error::last_os_error().into()); + } + if bm.bmPlanes != 1 { + bail!("Unsupported multi-plane cursor"); + } + if bm.bmBitsPixel != 1 { + bail!("Unsupported cursor mask format"); + } + Ok(bm) + } +} + +struct DC(HDC); + +impl DC { + fn new() -> ResultType { + unsafe { + let dc = GetDC(0 as _); + if dc.is_null() { + bail!("Failed to get a drawing context"); + } + Ok(Self(dc)) + } + } +} + +impl Drop for DC { + fn drop(&mut self) { + unsafe { + if !self.0.is_null() { + ReleaseDC(0 as _, self.0); + } + } + } +} + +struct CompatibleDC(HDC); + +impl CompatibleDC { + fn new(existing: HDC) -> ResultType { + unsafe { + let dc = CreateCompatibleDC(existing); + if dc.is_null() { + bail!("Failed to get a compatible drawing context"); + } + Ok(Self(dc)) + } + } +} + +impl Drop for CompatibleDC { + fn drop(&mut self) { + unsafe { + if !self.0.is_null() { + DeleteDC(self.0); + } + } + } +} + +struct BitmapDC(CompatibleDC, HBITMAP); + +impl BitmapDC { + fn new(hdc: HDC, hbitmap: HBITMAP) -> ResultType { + unsafe { + let dc = CompatibleDC::new(hdc)?; + let oldbitmap = SelectObject(dc.0, hbitmap as _) as HBITMAP; + if oldbitmap.is_null() { + bail!("Failed to select CompatibleDC"); + } + Ok(Self(dc, oldbitmap)) + } + } + + fn dc(&self) -> HDC { + (self.0).0 + } +} + +impl Drop for BitmapDC { + fn drop(&mut self) { + unsafe { + if !self.1.is_null() { + SelectObject((self.0).0, self.1 as _); + } + } + } +} + +#[inline] +fn get_rich_cursor_data( + hbm_color: HBITMAP, + width: i32, + height: i32, + out: &mut Vec, +) -> ResultType<()> { + unsafe { + let dc = DC::new()?; + let bitmap_dc = BitmapDC::new(dc.0, hbm_color)?; + if get_di_bits(out.as_mut_ptr(), bitmap_dc.dc(), hbm_color, width, height) > 0 { + bail!("Failed to get di bits: {}", get_error()); + } + } + Ok(()) +} + +fn fix_cursor_mask( + mbits: &mut Vec, + cbits: &mut Vec, + width: usize, + height: usize, + bm_width_bytes: usize, +) -> bool { + let mut pix_idx = 0; + for _ in 0..height { + for _ in 0..width { + if cbits[pix_idx + 3] != 0 { + return false; + } + pix_idx += 4; + } + } + + let packed_width_bytes = (width + 7) >> 3; + let bm_size = mbits.len(); + let c_size = cbits.len(); + + // Pack and invert bitmap data (mbits) + // borrow from tigervnc + for y in 0..height { + for x in 0..packed_width_bytes { + let a = y * packed_width_bytes + x; + let b = y * bm_width_bytes + x; + if a < bm_size && b < bm_size { + mbits[a] = !mbits[b]; + } + } + } + + // Replace "inverted background" bits with black color to ensure + // cross-platform interoperability. Not beautiful but necessary code. + // borrow from tigervnc + let bytes_row = width << 2; + for y in 0..height { + let mut bitmask: u8 = 0x80; + for x in 0..width { + let mask_idx = y * packed_width_bytes + (x >> 3); + if mask_idx < bm_size { + let pix_idx = y * bytes_row + (x << 2); + if (mbits[mask_idx] & bitmask) == 0 { + for b1 in 0..4 { + let a = pix_idx + b1; + if a < c_size { + if cbits[a] != 0 { + mbits[mask_idx] ^= bitmask; + for b2 in b1..4 { + let b = pix_idx + b2; + if b < c_size { + cbits[b] = 0x00; + } + } + break; + } + } + } + } + } + bitmask >>= 1; + if bitmask == 0 { + bitmask = 0x80; + } + } + } + + // borrow from noVNC + let mut pix_idx = 0; + for y in 0..height { + for x in 0..width { + let mask_idx = y * packed_width_bytes + (x >> 3); + let mut alpha = 255; + if mask_idx < bm_size { + if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 { + alpha = 0; + } + } + let a = cbits[pix_idx + 2]; + let b = cbits[pix_idx + 1]; + let c = cbits[pix_idx]; + cbits[pix_idx] = a; + cbits[pix_idx + 1] = b; + cbits[pix_idx + 2] = c; + cbits[pix_idx + 3] = alpha; + pix_idx += 4; + } + } + return true; +} + +define_windows_service!(ffi_service_main, service_main); + +fn service_main(arguments: Vec) { + if let Err(e) = run_service(arguments) { + log::error!("run_service failed: {}", e); + } +} + +pub fn start_os_service() { + if let Err(e) = windows_service::service_dispatcher::start(APP_NAME, ffi_service_main) { + log::error!("start_service failed: {}", e); + } +} + +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +extern "C" { + fn LaunchProcessWin(cmd: *const u16, session_id: DWORD, as_user: BOOL) -> HANDLE; + fn selectInputDesktop() -> BOOL; + fn inputDesktopSelected() -> BOOL; + fn handleMask( + out: *mut u8, + mask: *const u8, + width: i32, + height: i32, + bmWidthBytes: i32, + bmHeight: i32, + ) -> i32; + fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32, out_size: i32); + fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32; + fn blank_screen(v: BOOL); +} + +extern "system" { + fn BlockInput(v: BOOL) -> BOOL; +} + +#[tokio::main(flavor = "current_thread")] +async fn run_service(_arguments: Vec) -> ResultType<()> { + let event_handler = move |control_event| -> ServiceControlHandlerResult { + log::info!("Got service control event: {:?}", control_event); + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + ServiceControl::Stop => { + send_close(crate::POSTFIX_SERVICE).ok(); + ServiceControlHandlerResult::NoError + } + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler + let status_handle = service_control_handler::register(APP_NAME, event_handler)?; + + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: SERVICE_TYPE, + // The new state + current_state: ServiceState::Running, + // Accept stop events when running + controls_accepted: ServiceControlAccept::STOP, + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + process_id: None, + }; + + // Tell the system that the service is running now + status_handle.set_service_status(next_status)?; + + let mut session_id = unsafe { WTSGetActiveConsoleSessionId() }; + log::info!("session id {}", session_id); + let mut h_process = launch_server(session_id, true).await.unwrap_or(NULL); + let mut incoming = ipc::new_listener(crate::POSTFIX_SERVICE).await?; + loop { + let res = timeout(super::SERVICE_INTERVAL, incoming.next()).await; + match res { + Ok(res) => match res { + Some(Ok(stream)) => { + let mut stream = ipc::Connection::new(stream); + if let Ok(Some(data)) = stream.next_timeout(1000).await { + match data { + ipc::Data::Close => { + log::info!("close received"); + break; + } + ipc::Data::SAS => { + send_sas(); + } + _ => {} + } + } + } + _ => {} + }, + Err(_) => { + // timeout + unsafe { + let tmp = WTSGetActiveConsoleSessionId(); + if tmp == 0xFFFFFFFF { + continue; + } + let mut close_sent = false; + if tmp != session_id { + log::info!("session changed from {} to {}", session_id, tmp); + session_id = tmp; + send_close_async("").await.ok(); + close_sent = true; + } + let mut exit_code: DWORD = 0; + if h_process.is_null() + || (GetExitCodeProcess(h_process, &mut exit_code) == TRUE + && exit_code != STILL_ACTIVE + && CloseHandle(h_process) == TRUE) + { + match launch_server(session_id, !close_sent).await { + Ok(ptr) => { + h_process = ptr; + } + Err(err) => { + log::error!("Failed to launch server: {}", err); + } + } + } + } + } + } + } + + if !h_process.is_null() { + send_close_async("").await.ok(); + unsafe { CloseHandle(h_process) }; + } + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} + +async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType { + if close_first { + // in case started some elsewhere + send_close_async("").await.ok(); + } + let cmd = format!( + "\"{}\" --server", + std::env::current_exe()?.to_str().unwrap_or("") + ); + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(&cmd) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE) }; + if h.is_null() { + log::error!("Failed to launch server: {}", get_error()); + } + Ok(h) +} + +pub fn run_as_user(arg: &str) -> ResultType> { + let cmd = format!( + "\"{}\" {}", + std::env::current_exe()?.to_str().unwrap_or(""), + arg, + ); + let session_id = unsafe { WTSGetActiveConsoleSessionId() }; + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(&cmd) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE) }; + if h.is_null() { + bail!( + "Failed to launch {} with session id {}: {}", + arg, + session_id, + get_error() + ); + } + Ok(None) +} + +#[tokio::main(flavor = "current_thread")] +async fn send_close(postfix: &str) -> ResultType<()> { + send_close_async(postfix).await +} + +async fn send_close_async(postfix: &str) -> ResultType<()> { + ipc::connect(1000, postfix) + .await? + .send(&ipc::Data::Close) + .await?; + // sleep a while to wait for closing and exit + sleep(0.1).await; + Ok(()) +} + +// https://docs.microsoft.com/en-us/windows/win32/api/sas/nf-sas-sendsas +// https://www.cnblogs.com/doutu/p/4892726.html +fn send_sas() { + #[link(name = "sas")] + extern "system" { + pub fn SendSAS(AsUser: BOOL); + } + unsafe { + log::info!("SAS received"); + SendSAS(FALSE); + } +} + +lazy_static::lazy_static! { + static ref SUPPRESS: Arc> = Arc::new(Mutex::new(Instant::now())); +} + +pub fn desktop_changed() -> bool { + unsafe { inputDesktopSelected() == FALSE } +} + +pub fn try_change_desktop() -> bool { + unsafe { + if inputDesktopSelected() == FALSE { + let res = selectInputDesktop() == TRUE; + if !res { + let mut s = SUPPRESS.lock().unwrap(); + if s.elapsed() > std::time::Duration::from_secs(3) { + log::error!("Failed to switch desktop: {}", get_error()); + *s = Instant::now(); + } + } else { + log::info!("Desktop switched"); + } + return res; + } + } + return false; +} + +fn get_error() -> String { + unsafe { + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let errno = GetLastError(); + let chars_copied = FormatMessageW( + FORMAT_MESSAGE_IGNORE_INSERTS + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + std::ptr::null(), + errno, + 0, + buff.as_mut_ptr(), + (buff_size + 1) as u32, + std::ptr::null_mut(), + ); + if chars_copied == 0 { + return "".to_owned(); + } + let mut curr_char: usize = chars_copied as usize; + while curr_char > 0 { + let ch = buff[curr_char]; + + if ch >= ' ' as u16 { + break; + } + curr_char -= 1; + } + let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char); + let err_msg = String::from_utf16(sl); + return err_msg.unwrap_or("".to_owned()); + } +} + +pub fn get_active_username() -> String { + let name = crate::username(); + if name != "SYSTEM" { + return name; + } + extern "C" { + fn get_active_user(path: *mut u16, n: u32) -> u32; + } + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let n = unsafe { get_active_user(buff.as_mut_ptr(), buff_size as _) }; + if n == 0 { + return "".to_owned(); + } + let sl = unsafe { std::slice::from_raw_parts(buff.as_ptr(), n as _) }; + String::from_utf16(sl) + .unwrap_or("??".to_owned()) + .trim_end_matches('\0') + .to_owned() +} + +/* +pub fn get_active_username() -> String { + use std::os::windows::process::CommandExt; + let name = crate::username(); + if name != "SYSTEM" { + return name; + } + const CREATE_NO_WINDOW: u32 = 0x08000000; + let mut cmd = std::process::Command::new("query"); + cmd.arg("user"); + cmd.creation_flags(CREATE_NO_WINDOW); + if let Ok(output) = cmd.output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(name) = line.split_whitespace().next() { + if name.starts_with(">") { + return name.replace(">", ""); + } + } + } + } + return "".to_owned(); +} +*/ + +pub fn is_prelogin() -> bool { + let username = get_active_username(); + username.is_empty() || username == "SYSTEM" +} + +pub fn is_root() -> bool { + crate::username() == "SYSTEM" +} + +pub fn lock_screen() { + extern "system" { + pub fn LockWorkStation() -> BOOL; + } + unsafe { + LockWorkStation(); + } +} + +pub fn get_install_info() -> (String, String, String, String) { + let subkey = format!( + "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + APP_NAME + ); + let mut pf = "C:\\Program Files".to_owned(); + if let Ok(output) = std::process::Command::new("echo") + .arg("%ProgramFiles%") + .output() + { + let tmp = String::from_utf8_lossy(&output.stdout); + if !tmp.starts_with("%") { + pf = tmp.to_string(); + } + } + let path = format!("{}\\{}", pf, APP_NAME); + let start_menu = format!( + "%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\{}", + APP_NAME + ); + let exe = format!("{}\\{}.exe", path, APP_NAME); + (subkey, path, start_menu, exe) +} + +pub fn update_me() -> ResultType<()> { + let (_, _, _, exe) = get_install_info(); + let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + let cmds = format!( + " + chcp 65001 + sc stop {app_name} + taskkill /F /IM {app_name}.exe + copy /Y \"{src_exe}\" \"{exe}\" + sc start {app_name} + ", + src_exe = src_exe, + exe = exe, + app_name = APP_NAME, + ); + std::thread::sleep(std::time::Duration::from_millis(1000)); + run_cmds(cmds, false)?; + std::thread::sleep(std::time::Duration::from_millis(2000)); + std::process::Command::new(&exe).spawn()?; + std::process::Command::new(&exe) + .args(&["--remove", &src_exe]) + .spawn()?; + Ok(()) +} + +pub fn install_me(options: &str) -> ResultType<()> { + let (subkey, path, start_menu, exe) = get_install_info(); + let mut version_major = "0"; + let mut version_minor = "0"; + let mut version_build = "0"; + let versions: Vec<&str> = crate::VERSION.split(".").collect(); + if versions.len() > 0 { + version_major = versions[0]; + } + if versions.len() > 1 { + version_minor = versions[1]; + } + if versions.len() > 2 { + version_build = versions[2]; + } + + let tmp_path = "C:\\Windows\\temp"; + let mk_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\{app_name}.lnk\" + +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" +oLink.Save + ", + tmp_path = tmp_path, + app_name = APP_NAME, + exe = exe, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + // https://superuser.com/questions/392061/how-to-make-a-shortcut-from-cmd + let uninstall_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\Uninstall {app_name}.lnk\" +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--uninstall\" + oLink.IconLocation = \"msiexec.exe\" +oLink.Save + ", + tmp_path = tmp_path, + app_name = APP_NAME, + exe = exe, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + let tray_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\" + +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--tray\" +oLink.Save + ", + tmp_path = tmp_path, + app_name = APP_NAME, + exe = exe, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + let mut shortcuts = Default::default(); + if options.contains("desktopicon") { + shortcuts = format!( + "copy /Y \"{}\\{}.lnk\" \"%PUBLIC%\\Desktop\\\"", + tmp_path, APP_NAME + ); + } + if options.contains("startmenu") { + shortcuts = format!( + "{} +md \"{start_menu}\" +copy /Y \"{tmp_path}\\{app_name}.lnk\" \"{start_menu}\\\" +copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" + ", + shortcuts, + start_menu = start_menu, + tmp_path = tmp_path, + app_name = APP_NAME + ); + } + + let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; + let size = meta.len() / 1024; + // save_tmp is for ensuring not copying file while writing + let config_path = Config::save_tmp(); + let ext = APP_NAME.to_lowercase(); + // https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa + // https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10 + // https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html + let cmds = format!( + " +chcp 65001 +md \"{path}\" +copy /Y \"{src_exe}\" \"{exe}\" +reg add {subkey} /f +reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" +reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\" +reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v InstallLocation /t REG_SZ /d \"{path}\" +reg add {subkey} /f /v Publisher /t REG_SZ /d \"{app_name}\" +reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {major} +reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {minor} +reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {build} +reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\" +reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} +reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0 +reg add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1 +\"{mk_shortcut}\" +\"{uninstall_shortcut}\" +\"{tray_shortcut}\" +copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\" +{shortcuts} +del /f \"{mk_shortcut}\" +del /f \"{uninstall_shortcut}\" +del /f \"{tray_shortcut}\" +del /f \"{tmp_path}\\{app_name}.lnk\" +del /f \"{tmp_path}\\Uninstall {app_name}.lnk\" +del /f \"{tmp_path}\\{app_name} Tray.lnk\" +reg add HKEY_CLASSES_ROOT\\.{ext} /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\" +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f +reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" --play \\\"%%1\\\"\" +sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\" +sc start {app_name} +sc stop {app_name} +sc delete {app_name} +sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\" +netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes +del /f \"{config_path}\" +del /f \"{config2_path}\" +sc start {app_name} + ", + path=path, + src_exe=std::env::current_exe()?.to_str().unwrap_or(""), + exe=exe, + subkey=subkey, + app_name=APP_NAME, + version=crate::VERSION, + major=version_major, + minor=version_minor, + build=version_build, + size=size, + mk_shortcut=mk_shortcut, + uninstall_shortcut=uninstall_shortcut, + tray_shortcut=tray_shortcut, + tmp_path=tmp_path, + shortcuts=shortcuts, + config_path=config_path, + config2_path=config_path.replace(".toml", "2.toml"), + ext=ext, + ); + run_cmds(cmds, false)?; + std::thread::sleep(std::time::Duration::from_millis(2000)); + std::process::Command::new(&exe).spawn()?; + std::process::Command::new(&exe).arg("--tray").spawn()?; + std::thread::sleep(std::time::Duration::from_millis(1000)); + Ok(()) +} + +pub fn uninstall_me() -> ResultType<()> { + let (subkey, path, start_menu, _) = get_install_info(); + let ext = APP_NAME.to_lowercase(); + let cmds = format!( + " +chcp 65001 +sc stop {app_name} +sc delete {app_name} +taskkill /F /IM {app_name}.exe +reg delete {subkey} /f +reg delete HKEY_CLASSES_ROOT\\.{ext} /f +rd /s /q \"{path}\" +rd /s /q \"{start_menu}\" +del /f /q \"%PUBLIC%\\Desktop\\{app_name}*\" +del /f /q \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" +netsh advfirewall firewall delete rule name=\"{app_name} Service\" + ", + app_name = APP_NAME, + path = path, + subkey = subkey, + start_menu = start_menu, + ext = ext, + ); + run_cmds(cmds, true) +} + +fn write_cmds(cmds: String, ext: &str) -> ResultType { + let mut tmp = std::env::temp_dir(); + tmp.push(format!("{}_{:?}.{}", APP_NAME, cmds.as_ptr(), ext)); + let mut cmds = cmds; + if ext == "cmd" { + cmds = format!("{}\ndel /f \"{}\"", cmds, tmp.to_str().unwrap_or("")); + } + let mut file = std::fs::File::create(&tmp)?; + // in case cmds mixed with \r\n and \n, make sure all ending with \r\n + // in some windows, \r\n required for cmd file to run + let cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n"); + file.write_all(cmds.as_bytes())?; + file.sync_all()?; + return Ok(tmp); +} + +fn run_cmds(cmds: String, show: bool) -> ResultType<()> { + let tmp = write_cmds(cmds, "cmd")?; + let res = runas::Command::new(tmp.to_str().unwrap_or("")) + .show(show) + .force_prompt(true) + .status(); + // double confirm delete, because below delete not work if program + // exit immediately such as --uninstall + allow_err!(std::fs::remove_file(tmp)); + let _ = res?; + Ok(()) +} + +pub fn toggle_blank_screen(v: bool) { + let v = if v { TRUE } else { FALSE }; + unsafe { + blank_screen(v); + } +} + +pub fn block_input(v: bool) -> bool { + let v = if v { TRUE } else { FALSE }; + unsafe { BlockInput(v) == TRUE } +} + +pub fn add_recent_document(path: &str) { + extern "C" { + fn AddRecentDocument(path: *const u16); + } + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(path) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + unsafe { + AddRecentDocument(wstr); + } +} + +pub fn is_installed() -> bool { + use windows_service::{ + service::ServiceAccess, + service_manager::{ServiceManager, ServiceManagerAccess}, + }; + let (_, _, _, exe) = get_install_info(); + if !std::fs::metadata(exe).is_ok() { + return false; + } + let manager_access = ServiceManagerAccess::CONNECT; + if let Ok(service_manager) = ServiceManager::local_computer(None::<&str>, manager_access) { + if let Ok(_) = service_manager.open_service(APP_NAME, ServiceAccess::QUERY_CONFIG) { + return true; + } + } + return false; +} + +pub fn get_installed_version() -> String { + let (_, _, _, exe) = get_install_info(); + if let Ok(output) = std::process::Command::new(exe).arg("--version").output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + return line.to_owned(); + } + } + "".to_owned() +} + +pub fn create_shortcut(id: &str) -> ResultType<()> { + let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + let shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +strDesktop = oWS.SpecialFolders(\"Desktop\") +Set objFSO = CreateObject(\"Scripting.FileSystemObject\") +sLinkFile = objFSO.BuildPath(strDesktop, \"{id}.lnk\") +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--connect {id}\" +oLink.Save + ", + exe = exe, + id = id, + ), + "vbs", + )? + .to_str() + .unwrap_or("") + .to_owned(); + std::process::Command::new("cscript") + .arg(&shortcut) + .output()?; + allow_err!(std::fs::remove_file(shortcut)); + Ok(()) +} diff --git a/rust-rdp/rust-desk/src/port_forward.rs b/rust-rdp/rust-desk/src/port_forward.rs new file mode 100644 index 0000000..2cd5fd9 --- /dev/null +++ b/rust-rdp/rust-desk/src/port_forward.rs @@ -0,0 +1,170 @@ +use crate::client::*; +use hbb_common::{ + allow_err, bail, + config::CONNECT_TIMEOUT, + futures::{SinkExt, StreamExt}, + log, + message_proto::*, + protobuf::Message as _, + rendezvous_proto::ConnType, + tcp, timeout, + tokio::{self, net::TcpStream, sync::mpsc}, + tokio_util::codec::{BytesCodec, Framed}, + ResultType, Stream, +}; + +fn run_rdp(port: u16) { + std::process::Command::new("mstsc") + .arg(format!("/v:localhost:{}", port)) + .spawn() + .ok(); +} + +pub async fn listen( + id: String, + port: i32, + interface: impl Interface, + ui_receiver: mpsc::UnboundedReceiver, +) -> ResultType<()> { + let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?; + let addr = listener.local_addr()?; + log::info!("listening on port {:?}", addr); + let is_rdp = port == 0; + if is_rdp { + run_rdp(addr.port()); + } + let mut ui_receiver = ui_receiver; + loop { + tokio::select! { + Ok((forward, addr)) = listener.accept() => { + log::info!("new connection from {:?}", addr); + let id = id.clone(); + let mut forward = Framed::new(forward, BytesCodec::new()); + match connect_and_login(&id, &mut ui_receiver, interface.clone(), &mut forward, is_rdp).await { + Ok(Some(stream)) => { + let interface = interface.clone(); + tokio::spawn(async move { + if let Err(err) = run_forward(forward, stream).await { + interface.msgbox("error", "Error", &err.to_string()); + } + log::info!("connection from {:?} closed", addr); + }); + } + Err(err) => { + interface.msgbox("error", "Error", &err.to_string()); + } + _ => {} + } + } + d = ui_receiver.recv() => { + match d { + Some(Data::Close) => { + break; + } + Some(Data::NewRDP) => { + run_rdp(addr.port()); + } + _ => {} + } + } + } + } + Ok(()) +} + +async fn connect_and_login( + id: &str, + ui_receiver: &mut mpsc::UnboundedReceiver, + interface: impl Interface, + forward: &mut Framed, + is_rdp: bool, +) -> ResultType> { + let conn_type = if is_rdp { + ConnType::RDP + } else { + ConnType::PORT_FORWARD + }; + let (mut stream, _) = Client::start(id, conn_type).await?; + let mut interface = interface; + let mut buffer = Vec::new(); + loop { + tokio::select! { + res = timeout(CONNECT_TIMEOUT, stream.next()) => match res { + Err(_) => { + bail!("Timeout"); + } + Ok(Some(Ok(bytes))) => { + let msg_in = Message::parse_from_bytes(&bytes)?; + match msg_in.union { + Some(message::Union::hash(hash)) => { + interface.handle_hash(hash, &mut stream).await; + } + Some(message::Union::login_response(lr)) => match lr.union { + Some(login_response::Union::error(err)) => { + interface.handle_login_error(&err); + return Ok(None); + } + Some(login_response::Union::peer_info(pi)) => { + interface.handle_peer_info(pi); + break; + } + _ => {} + } + Some(message::Union::test_delay(t)) => { + interface.handle_test_delay(t, &mut stream).await; + } + _ => {} + } + } + _ => { + bail!("Reset by the peer"); + } + }, + d = ui_receiver.recv() => { + match d { + Some(Data::Login((password, remember))) => { + interface.handle_login_from_ui(password, remember, &mut stream).await; + } + _ => {} + } + }, + res = forward.next() => { + if let Some(Ok(bytes)) = res { + buffer.extend(bytes); + } else { + return Ok(None); + } + }, + } + } + stream.set_raw(); + if !buffer.is_empty() { + allow_err!(stream.send_bytes(buffer.into()).await); + } + Ok(Some(stream)) +} + +async fn run_forward(forward: Framed, stream: Stream) -> ResultType<()> { + log::info!("new port forwarding connection started"); + let mut forward = forward; + let mut stream = stream; + loop { + tokio::select! { + res = forward.next() => { + if let Some(Ok(bytes)) = res { + allow_err!(stream.send_bytes(bytes.into()).await); + } else { + break; + } + }, + res = stream.next() => { + if let Some(Ok(bytes)) = res { + allow_err!(forward.send(bytes.into()).await); + } else { + break; + } + }, + } + } + Ok(()) +} diff --git a/rust-rdp/rust-desk/src/rendezvous_mediator.rs b/rust-rdp/rust-desk/src/rendezvous_mediator.rs new file mode 100644 index 0000000..736d73a --- /dev/null +++ b/rust-rdp/rust-desk/src/rendezvous_mediator.rs @@ -0,0 +1,596 @@ +use crate::server::{check_zombie, new as new_server, ServerPtr}; +use hbb_common::{ + allow_err, + anyhow::bail, + config::{self, Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, socket_client, + tokio::{ + self, select, + time::{interval, Duration}, + }, + udp::FramedSocket, + AddrMangle, IntoTargetAddr, ResultType, TargetAddr, +}; +use std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, + time::{Instant, SystemTime}, +}; +use uuid::Uuid; + +type Message = RendezvousMessage; + +lazy_static::lazy_static! { + static ref SOLVING_PK_MISMATCH: Arc> = Default::default(); +} +static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); + +#[derive(Clone)] +pub struct RendezvousMediator { + addr: TargetAddr<'static>, + host: String, + host_prefix: String, + last_id_pk_registry: String, +} + +impl RendezvousMediator { + pub fn restart() { + SHOULD_EXIT.store(true, Ordering::SeqCst); + log::info!("server restart"); + } + + pub async fn start_all() { + let mut nat_tested = false; + check_zombie(); + let server = new_server(); + if Config::get_nat_type() == NatType::UNKNOWN_NAT as i32 { + crate::common::test_nat_type(); + nat_tested = true; + } + let server_cloned = server.clone(); + tokio::spawn(async move { + allow_err!(direct_server(server_cloned).await); + }); + if crate::platform::is_installed() { + std::thread::spawn(move || { + allow_err!(lan_discovery()); + }); + } + loop { + Config::reset_online(); + if Config::get_option("stop-service").is_empty() { + if !nat_tested { + crate::common::test_nat_type(); + nat_tested = true; + } + let mut futs = Vec::new(); + let servers = Config::get_rendezvous_servers(); + SHOULD_EXIT.store(false, Ordering::SeqCst); + for host in servers.clone() { + let server = server.clone(); + futs.push(tokio::spawn(async move { + allow_err!(Self::start(server, host).await); + // SHOULD_EXIT here is to ensure once one exits, the others also exit. + SHOULD_EXIT.store(true, Ordering::SeqCst); + })); + } + join_all(futs).await; + } + sleep(1.).await; + } + } + + pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { + log::info!("start rendezvous mediator of {}", host); + let host_prefix: String = host + .split(".") + .next() + .map(|x| { + if x.parse::().is_ok() { + host.clone() + } else { + x.to_string() + } + }) + .unwrap_or(host.to_owned()); + let mut rz = Self { + addr: Config::get_any_listen_addr().into_target_addr()?, + host: host.clone(), + host_prefix, + last_id_pk_registry: "".to_owned(), + }; + + rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; + let any_addr = Config::get_any_listen_addr(); + let mut socket = socket_client::new_udp(any_addr, RENDEZVOUS_TIMEOUT).await?; + + const TIMER_OUT: Duration = Duration::from_secs(1); + let mut timer = interval(TIMER_OUT); + let mut last_timer = SystemTime::UNIX_EPOCH; + const REG_TIMEOUT: i64 = 3_000; + const MAX_FAILS1: i64 = 3; + const MAX_FAILS2: i64 = 6; + const DNS_INTERVAL: i64 = 60_000; + let mut fails = 0; + let mut last_register_resp = SystemTime::UNIX_EPOCH; + let mut last_register_sent = SystemTime::UNIX_EPOCH; + let mut last_dns_check = SystemTime::UNIX_EPOCH; + let mut old_latency = 0; + let mut ema_latency = 0; + loop { + let mut update_latency = || { + last_register_resp = SystemTime::now(); + fails = 0; + let mut latency = last_register_resp + .duration_since(last_register_sent) + .map(|d| d.as_micros() as i64) + .unwrap_or(0); + if latency < 0 || latency > 1_000_000 { + return; + } + if ema_latency == 0 { + ema_latency = latency; + } else { + ema_latency = latency / 30 + (ema_latency * 29 / 30); + latency = ema_latency; + } + let mut n = latency / 5; + if n < 3000 { + n = 3000; + } + if (latency - old_latency).abs() > n || old_latency <= 0 { + Config::update_latency(&host, latency); + log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); + old_latency = latency; + } + }; + select! { + n = socket.next() => { + match n { + Some(Ok((bytes, _))) => { + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::register_peer_response(rpr)) => { + update_latency(); + if rpr.request_pk { + log::info!("request_pk received from {}", host); + allow_err!(rz.register_pk(&mut socket).await); + continue; + } + } + Some(rendezvous_message::Union::register_pk_response(rpr)) => { + update_latency(); + match rpr.result.enum_value_or_default() { + register_pk_response::Result::OK => { + Config::set_key_confirmed(true); + Config::set_host_key_confirmed(&rz.host_prefix, true); + *SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned(); + } + register_pk_response::Result::UUID_MISMATCH => { + allow_err!(rz.handle_uuid_mismatch(&mut socket).await); + } + _ => {} + } + } + Some(rendezvous_message::Union::punch_hole(ph)) => { + let rz = rz.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_punch_hole(ph, server).await); + }); + } + Some(rendezvous_message::Union::request_relay(rr)) => { + let rz = rz.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_request_relay(rr, server).await); + }); + } + Some(rendezvous_message::Union::fetch_local_addr(fla)) => { + let rz = rz.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_intranet(fla, server).await); + }); + } + Some(rendezvous_message::Union::configure_update(cu)) => { + let v0 = Config::get_rendezvous_servers(); + Config::set_option("rendezvous-servers".to_owned(), cu.rendezvous_servers.join(",")); + Config::set_serial(cu.serial); + if v0 != Config::get_rendezvous_servers() { + Self::restart(); + } + } + _ => {} + } + } else { + log::debug!("Non-protobuf message bytes received: {:?}", bytes); + } + }, + Some(Err(e)) => bail!("Failed to receive next {}", e), // maybe socks5 tcp disconnected + None => { + // unreachable!() + }, + } + }, + _ = timer.tick() => { + if SHOULD_EXIT.load(Ordering::SeqCst) { + break; + } + let now = SystemTime::now(); + if now.duration_since(last_timer).map(|d| d < TIMER_OUT).unwrap_or(false) { + // a workaround of tokio timer bug + continue; + } + last_timer = now; + let elapsed_resp = now.duration_since(last_register_resp).map(|d| d.as_millis() as i64).unwrap_or(REG_INTERVAL); + let timeout = last_register_sent.duration_since(last_register_resp).map(|d| d.as_millis() as i64).unwrap_or(0) >= REG_TIMEOUT; + if timeout || elapsed_resp >= REG_INTERVAL { + allow_err!(rz.register_peer(&mut socket).await); + last_register_sent = now; + if timeout { + fails += 1; + if fails > MAX_FAILS2 { + Config::update_latency(&host, -1); + old_latency = 0; + if now.duration_since(last_dns_check).map(|d| d.as_millis() as i64).unwrap_or(0) > DNS_INTERVAL { + rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; + // in some case of network reconnect (dial IP network), + // old UDP socket not work any more after network recover + if let Some(s) = socket_client::rebind_udp(any_addr).await? { + socket = s; + } + last_dns_check = now; + } + } else if fails > MAX_FAILS1 { + Config::update_latency(&host, 0); + old_latency = 0; + } + } + } + } + } + } + Ok(()) + } + + async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> { + self.create_relay( + rr.socket_addr, + rr.relay_server, + rr.uuid, + server, + rr.secure, + false, + ) + .await + } + + async fn create_relay( + &self, + socket_addr: Vec, + relay_server: String, + uuid: String, + server: ServerPtr, + secure: bool, + initiate: bool, + ) -> ResultType<()> { + let peer_addr = AddrMangle::decode(&socket_addr); + log::info!( + "create_relay requested from from {:?}, relay_server: {}, uuid: {}, secure: {}", + peer_addr, + relay_server, + uuid, + secure, + ); + + let mut socket = socket_client::connect_tcp( + self.addr.to_owned(), + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await?; + + let mut msg_out = Message::new(); + let mut rr = RelayResponse { + socket_addr, + version: crate::VERSION.to_owned(), + ..Default::default() + }; + if initiate { + rr.uuid = uuid.clone(); + rr.relay_server = relay_server.clone(); + rr.set_id(Config::get_id()); + } + msg_out.set_relay_response(rr); + socket.send(&msg_out).await?; + crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure).await; + Ok(()) + } + + async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let peer_addr = AddrMangle::decode(&fla.socket_addr); + log::debug!("Handle intranet from {:?}", peer_addr); + let mut socket = socket_client::connect_tcp( + self.addr.to_owned(), + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await?; + let local_addr = socket.local_addr(); + let local_addr: SocketAddr = + format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?; + let mut msg_out = Message::new(); + let mut relay_server = Config::get_option("relay-server"); + if relay_server.is_empty() { + relay_server = fla.relay_server; + } + msg_out.set_local_addr(LocalAddr { + id: Config::get_id(), + socket_addr: AddrMangle::encode(peer_addr), + local_addr: AddrMangle::encode(local_addr), + relay_server, + version: crate::VERSION.to_owned(), + ..Default::default() + }); + let bytes = msg_out.write_to_bytes()?; + socket.send_raw(bytes).await?; + crate::accept_connection(server.clone(), socket, peer_addr, true).await; + Ok(()) + } + + async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> { + let mut relay_server = Config::get_option("relay-server"); + if relay_server.is_empty() { + relay_server = ph.relay_server; + } + if ph.nat_type.enum_value_or_default() == NatType::SYMMETRIC + || Config::get_nat_type() == NatType::SYMMETRIC as i32 + { + let uuid = Uuid::new_v4().to_string(); + return self + .create_relay(ph.socket_addr, relay_server, uuid, server, true, true) + .await; + } + let peer_addr = AddrMangle::decode(&ph.socket_addr); + log::debug!("Punch hole to {:?}", peer_addr); + let mut socket = { + let socket = socket_client::connect_tcp( + self.addr.to_owned(), + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await?; + let local_addr = socket.local_addr(); + allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 300).await); + socket + }; + let mut msg_out = Message::new(); + use hbb_common::protobuf::ProtobufEnum; + let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT); + msg_out.set_punch_hole_sent(PunchHoleSent { + socket_addr: ph.socket_addr, + id: Config::get_id(), + relay_server, + nat_type: nat_type.into(), + version: crate::VERSION.to_owned(), + ..Default::default() + }); + let bytes = msg_out.write_to_bytes()?; + socket.send_raw(bytes).await?; + crate::accept_connection(server.clone(), socket, peer_addr, true).await; + Ok(()) + } + + async fn register_pk(&mut self, socket: &mut FramedSocket) -> ResultType<()> { + let mut msg_out = Message::new(); + let pk = Config::get_key_pair().1; + let uuid = if let Ok(id) = machine_uid::get() { + log::info!("machine uid: {}", id); + id.into() + } else { + pk.clone() + }; + let id = Config::get_id(); + self.last_id_pk_registry = id.clone(); + msg_out.set_register_pk(RegisterPk { + id, + uuid, + pk, + ..Default::default() + }); + socket.send(&msg_out, self.addr.to_owned()).await?; + Ok(()) + } + + async fn handle_uuid_mismatch(&mut self, socket: &mut FramedSocket) -> ResultType<()> { + if self.last_id_pk_registry != Config::get_id() { + return Ok(()); + } + { + let mut solving = SOLVING_PK_MISMATCH.lock().unwrap(); + if solving.is_empty() || *solving == self.host { + log::info!("UUID_MISMATCH received from {}", self.host); + Config::set_key_confirmed(false); + Config::update_id(); + *solving = self.host.clone(); + } else { + return Ok(()); + } + } + self.register_pk(socket).await + } + + async fn register_peer(&mut self, socket: &mut FramedSocket) -> ResultType<()> { + if !SOLVING_PK_MISMATCH.lock().unwrap().is_empty() { + return Ok(()); + } + if !Config::get_key_confirmed() || !Config::get_host_key_confirmed(&self.host_prefix) { + log::info!( + "register_pk of {} due to key not confirmed", + self.host_prefix + ); + return self.register_pk(socket).await; + } + let id = Config::get_id(); + log::trace!( + "Register my id {:?} to rendezvous server {:?}", + id, + self.addr, + ); + let mut msg_out = Message::new(); + let serial = Config::get_serial(); + msg_out.set_register_peer(RegisterPeer { + id, + serial, + ..Default::default() + }); + socket.send(&msg_out, self.addr.to_owned()).await?; + Ok(()) + } +} + +async fn direct_server(server: ServerPtr) -> ResultType<()> { + let port = RENDEZVOUS_PORT + 2; + let addr = format!("0.0.0.0:{}", port); + let mut listener = None; + loop { + if !Config::get_option("direct-server").is_empty() && listener.is_none() { + listener = Some(hbb_common::tcp::new_listener(&addr, false).await?); + log::info!( + "Direct server listening on: {}", + &listener.as_ref().unwrap().local_addr()? + ); + } + if let Some(l) = listener.as_mut() { + if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await { + if Config::get_option("direct-server").is_empty() { + continue; + } + stream.set_nodelay(true).ok(); + log::info!("direct access from {}", addr); + let local_addr = stream.local_addr()?; + let server = server.clone(); + tokio::spawn(async move { + allow_err!( + crate::server::create_tcp_connection( + server, + hbb_common::Stream::from(stream, local_addr), + addr, + false, + ) + .await + ); + }); + } else { + sleep(0.1).await; + } + } else { + sleep(1.).await; + } + } +} + +#[inline] +pub fn get_broadcast_port() -> u16 { + (RENDEZVOUS_PORT + 3) as _ +} + +pub fn get_mac() -> String { + if let Ok(Some(mac)) = mac_address::get_mac_address() { + mac.to_string() + } else { + "".to_owned() + } +} + +fn lan_discovery() -> ResultType<()> { + let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); + let socket = std::net::UdpSocket::bind(addr)?; + socket.set_read_timeout(Some(std::time::Duration::from_millis(1000)))?; + log::info!("lan discovery listener started"); + loop { + let mut buf = [0; 2048]; + if let Ok((len, addr)) = socket.recv_from(&mut buf) { + if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { + match msg_in.union { + Some(rendezvous_message::Union::peer_discovery(p)) => { + if p.cmd == "ping" { + let mut msg_out = Message::new(); + let peer = PeerDiscovery { + cmd: "pong".to_owned(), + mac: get_mac(), + id: Config::get_id(), + hostname: whoami::hostname(), + username: crate::platform::get_active_username(), + platform: whoami::platform().to_string(), + ..Default::default() + }; + msg_out.set_peer_discovery(peer); + socket.send_to(&msg_out.write_to_bytes()?, addr).ok(); + } + } + _ => {} + } + } + } + } +} + +pub fn discover() -> ResultType<()> { + let addr = SocketAddr::from(([0, 0, 0, 0], 0)); + let socket = std::net::UdpSocket::bind(addr)?; + socket.set_broadcast(true)?; + let mut msg_out = Message::new(); + let peer = PeerDiscovery { + cmd: "ping".to_owned(), + ..Default::default() + }; + msg_out.set_peer_discovery(peer); + let maddr = SocketAddr::from(([255, 255, 255, 255], get_broadcast_port())); + socket.send_to(&msg_out.write_to_bytes()?, maddr)?; + log::info!("discover ping sent"); + let mut last_recv_time = Instant::now(); + let mut last_write_time = Instant::now(); + let mut last_write_n = 0; + // to-do: load saved peers, and update incrementally (then we can see offline) + let mut peers = Vec::new(); + let mac = get_mac(); + socket.set_read_timeout(Some(std::time::Duration::from_millis(10)))?; + loop { + let mut buf = [0; 2048]; + if let Ok((len, _)) = socket.recv_from(&mut buf) { + if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { + match msg_in.union { + Some(rendezvous_message::Union::peer_discovery(p)) => { + last_recv_time = Instant::now(); + if p.cmd == "pong" { + if p.mac != mac { + peers.push((p.id, p.username, p.hostname, p.platform)); + } + } + } + _ => {} + } + } + } + if last_write_time.elapsed().as_millis() > 300 && last_write_n != peers.len() { + config::LanPeers::store(serde_json::to_string(&peers)?); + last_write_time = Instant::now(); + last_write_n = peers.len(); + } + if last_recv_time.elapsed().as_millis() > 3_000 { + break; + } + } + log::info!("discover ping done"); + config::LanPeers::store(serde_json::to_string(&peers)?); + Ok(()) +} diff --git a/rust-rdp/rust-desk/src/server.rs b/rust-rdp/rust-desk/src/server.rs new file mode 100644 index 0000000..b522ffa --- /dev/null +++ b/rust-rdp/rust-desk/src/server.rs @@ -0,0 +1,434 @@ +#[cfg(target_os = "macos")] +use crate::ipc::ConnectionTmpl; +use crate::ipc::Data; +use connection::{ConnInner, Connection}; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + bail, + config::{Config, CONNECT_TIMEOUT, RELAY_PORT}, + log, + message_proto::*, + protobuf::{Message as _, ProtobufEnum}, + rendezvous_proto::*, + sleep, socket_client, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, tokio, ResultType, Stream, +}; +#[cfg(target_os = "macos")] +use notify::{watcher, RecursiveMode, Watcher}; +#[cfg(target_os = "macos")] +use parity_tokio_ipc::ConnectionClient; +use service::{GenericService, Service, ServiceTmpl, Subscriber}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + +mod audio_service; +mod clipboard_service; +mod connection; +pub mod input_service; +mod service; +mod video_service; + +use hbb_common::tcp::new_listener; + +pub type Childs = Arc>>; +type ConnMap = HashMap; + +lazy_static::lazy_static! { + pub static ref CHILD_PROCESS: Childs = Default::default(); +} + +pub struct Server { + connections: ConnMap, + services: HashMap<&'static str, Box>, + id_count: i32, +} + +pub type ServerPtr = Arc>; +pub type ServerPtrWeak = Weak>; + +pub fn new() -> ServerPtr { + let mut server = Server { + connections: HashMap::new(), + services: HashMap::new(), + id_count: 0, + }; + server.add_service(Box::new(audio_service::new())); + server.add_service(Box::new(video_service::new())); + server.add_service(Box::new(clipboard_service::new())); + server.add_service(Box::new(input_service::new_cursor())); + server.add_service(Box::new(input_service::new_pos())); + Arc::new(RwLock::new(server)) +} + +async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> { + let local_addr = socket.local_addr(); + drop(socket); + // even we drop socket, below still may fail if not use reuse_addr, + // there is TIME_WAIT before socket really released, so sometimes we + // see “Only one usage of each socket address is normally permitted” on windows sometimes, + let listener = new_listener(local_addr, true).await?; + log::info!("Server listening on: {}", &listener.local_addr()?); + if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { + stream.set_nodelay(true).ok(); + let stream_addr = stream.local_addr()?; + create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; + } + Ok(()) +} + +pub async fn create_tcp_connection( + server: ServerPtr, + stream: Stream, + addr: SocketAddr, + secure: bool, +) -> ResultType<()> { + let mut stream = stream; + let id = { + let mut w = server.write().unwrap(); + w.id_count += 1; + w.id_count + }; + let (sk, pk) = Config::get_key_pair(); + if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES { + let mut sk_ = [0u8; sign::SECRETKEYBYTES]; + sk_[..].copy_from_slice(&sk); + let sk = sign::SecretKey(sk_); + let mut msg_out = Message::new(); + let (our_pk_b, our_sk_b) = box_::gen_keypair(); + let signed_id = sign::sign( + format!("{}\0{}", Config::get_id(), base64::encode(our_pk_b.0)).as_bytes(), + &sk, + ); + msg_out.set_signed_id(SignedId { + id: signed_id, + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; + match timeout(CONNECT_TIMEOUT, stream.next()).await? { + Some(res) => { + let bytes = res?; + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if let Some(message::Union::public_key(pk)) = msg_in.union { + if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES { + let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); + let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; + pk_[..].copy_from_slice(&pk.asymmetric_value); + let their_pk_b = box_::PublicKey(pk_); + let symmetric_key = + box_::open(&pk.symmetric_value, &nonce, &their_pk_b, &our_sk_b) + .map_err(|_| { + anyhow!("Handshake failed: box decryption failure") + })?; + if symmetric_key.len() != secretbox::KEYBYTES { + bail!("Handshake failed: invalid secret key length from peer"); + } + let mut key = [0u8; secretbox::KEYBYTES]; + key[..].copy_from_slice(&symmetric_key); + stream.set_key(secretbox::Key(key)); + } else if pk.asymmetric_value.is_empty() { + Config::set_key_confirmed(false); + log::info!("Force to update pk"); + } else { + bail!("Handshake failed: invalid public sign key length from peer"); + } + } else { + log::error!("Handshake failed: invalid message type"); + } + } else { + bail!("Handshake failed: invalid message format"); + } + } + None => { + bail!("Failed to receive public key"); + } + } + } + + Connection::start(addr, stream, id, Arc::downgrade(&server)).await; + Ok(()) +} + +pub async fn accept_connection( + server: ServerPtr, + socket: Stream, + peer_addr: SocketAddr, + secure: bool, +) { + if let Err(err) = accept_connection_(server, socket, secure).await { + log::error!("Failed to accept connection from {}: {}", peer_addr, err); + } +} + +pub async fn create_relay_connection( + server: ServerPtr, + relay_server: String, + uuid: String, + peer_addr: SocketAddr, + secure: bool, +) { + if let Err(err) = + create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await + { + log::error!( + "Failed to create relay connection for {} with uuid {}: {}", + peer_addr, + uuid, + err + ); + } +} + +async fn create_relay_connection_( + server: ServerPtr, + relay_server: String, + uuid: String, + peer_addr: SocketAddr, + secure: bool, +) -> ResultType<()> { + let mut stream = socket_client::connect_tcp( + crate::check_port(relay_server, RELAY_PORT), + Config::get_any_listen_addr(), + CONNECT_TIMEOUT, + ) + .await?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_request_relay(RequestRelay { + uuid, + ..Default::default() + }); + stream.send(&msg_out).await?; + create_tcp_connection(server, stream, peer_addr, secure).await?; + Ok(()) +} + +impl Server { + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { + for s in self.services.values() { + if !noperms.contains(&s.name()) { + s.on_subscribe(conn.clone()); + } + } + self.connections.insert(conn.id(), conn); + } + + pub fn remove_connection(&mut self, conn: &ConnInner) { + for s in self.services.values() { + s.on_unsubscribe(conn.id()); + } + self.connections.remove(&conn.id()); + } + + fn add_service(&mut self, service: Box) { + let name = service.name(); + self.services.insert(name, service); + } + + pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) { + if let Some(s) = self.services.get(&name) { + if s.is_subed(conn.id()) == sub { + return; + } + if sub { + s.on_subscribe(conn.clone()); + } else { + s.on_unsubscribe(conn.id()); + } + } + } +} + +impl Drop for Server { + fn drop(&mut self) { + for s in self.services.values() { + s.join(); + } + } +} + +pub fn check_zombie() { + std::thread::spawn(|| loop { + let mut lock = CHILD_PROCESS.lock().unwrap(); + let mut i = 0; + while i != lock.len() { + let c = &mut (*lock)[i]; + if let Ok(Some(_)) = c.try_wait() { + lock.remove(i); + } else { + i += 1; + } + } + drop(lock); + std::thread::sleep(Duration::from_millis(100)); + }); +} + +#[tokio::main] +pub async fn start_server(is_server: bool, _tray: bool) { + #[cfg(target_os = "linux")] + { + log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); + log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); + } + + if is_server { + std::thread::spawn(move || { + if let Err(err) = crate::ipc::start("") { + log::error!("Failed to start ipc: {}", err); + std::process::exit(-1); + } + }); + input_service::fix_key_down_timeout_loop(); + #[cfg(target_os = "macos")] + tokio::spawn(async { sync_and_watch_config_dir().await }); + crate::RendezvousMediator::start_all().await; + } else { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + allow_err!(conn.send(&Data::SystemInfo(None)).await); + if let Ok(Some(data)) = conn.next_timeout(1000).await { + log::info!("server info: {:?}", data); + } + // sync key pair + let mut n = 0; + loop { + if Config::get_key_confirmed() { + // check ipc::get_id(), key_confirmed may change, so give some chance to correct + n += 1; + if n > 3 { + break; + } else { + sleep(1.).await; + } + } else { + allow_err!(conn.send(&Data::ConfirmedKey(None)).await); + if let Ok(Some(Data::ConfirmedKey(Some(pair)))) = + conn.next_timeout(1000).await + { + Config::set_key_pair(pair); + Config::set_key_confirmed(true); + log::info!("key pair synced"); + break; + } else { + sleep(1.).await; + } + } + } + } + Err(err) => { + log::info!("server not started (will try to start): {}", err); + std::thread::spawn(|| start_server(true, false)); + } + } + } +} + +#[cfg(target_os = "macos")] +async fn sync_and_watch_config_dir() { + if crate::username() == "root" { + return; + } + + match crate::ipc::connect(1000, "_service").await { + Ok(mut conn) => { + match sync_config_to_user(&mut conn).await { + Err(e) => log::error!("sync config to user failed:{}", e), + _ => {} + } + + tokio::spawn(async move { + log::info!( + "watching config dir: {}", + Config::path("").to_str().unwrap().to_string() + ); + + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); + watcher + .watch(Config::path("").as_path(), RecursiveMode::Recursive) + .unwrap(); + + loop { + let ev = rx.recv(); + match ev { + Ok(event) => match event { + notify::DebouncedEvent::Write(path) => { + log::info!( + "config file changed, call ipc_service to sync: {}", + path.to_str().unwrap().to_string() + ); + + match sync_config_to_root(&mut conn, path).await { + Err(e) => log::error!("sync config to root failed: {}", e), + _ => {} + } + } + x => { + log::debug!("another {:?}", x) + } + }, + Err(e) => println!("watch error: {:?}", e), + } + } + }); + } + Err(_) => { + log::info!("connect ipc_service failed, skip config sync"); + return; + } + } +} + +#[cfg(target_os = "macos")] +async fn sync_config_to_user(conn: &mut ConnectionTmpl) -> ResultType<()> { + allow_err!( + conn.send(&Data::SyncConfigToUserReq { + username: crate::username(), + to: Config::path("").to_str().unwrap().to_string(), + }) + .await + ); + + if let Some(data) = conn.next_timeout(2000).await? { + match data { + Data::SyncConfigToUserResp(success) => { + log::info!("copy and reload config dir success: {:?}", success); + } + _ => {} + }; + }; + + Ok(()) +} + +#[cfg(target_os = "macos")] +async fn sync_config_to_root( + conn: &mut ConnectionTmpl, + from: std::path::PathBuf, +) -> ResultType<()> { + allow_err!( + conn.send(&Data::SyncConfigToRootReq { + from: from.to_str().unwrap().to_string() + }) + .await + ); + + // todo: this code will block outer loop, resolve it later. + // if let Some(data) = conn.next_timeout(2000).await? { + // match data { + // Data::SyncConfigToRootResp(success) => { + // log::info!("copy config to root dir success: {:?}", success); + // } + // x => { + // log::info!("receive another {:?}", x) + // } + // }; + // }; + + Ok(()) +} diff --git a/rust-rdp/rust-desk/src/server/audio_service.rs b/rust-rdp/rust-desk/src/server/audio_service.rs new file mode 100644 index 0000000..6411cdb --- /dev/null +++ b/rust-rdp/rust-desk/src/server/audio_service.rs @@ -0,0 +1,371 @@ +// both soundio and cpal use wasapi on windows and coreaudio on mac, they do not support loopback. +// libpulseaudio support loopback because pulseaudio is a standalone audio service with some +// configuration, but need to install the library and start the service on OS, not a good choice. +// windows: https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording +// mac: https://github.com/mattingalls/Soundflower +// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient +// https://github.com/ExistentialAudio/BlackHole + +// if pactl not work, please run +// sudo apt-get --purge --reinstall install pulseaudio +// https://askubuntu.com/questions/403416/how-to-listen-live-sounds-from-input-from-external-sound-card +// https://wiki.debian.org/audio-loopback +// https://github.com/krruzic/pulsectl + +use super::*; +use magnum_opus::{Application::*, Channels::*, Encoder}; + +pub const NAME: &'static str = "audio"; + +#[cfg(not(target_os = "linux"))] +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.repeat::(33, cpal_impl::run); + sp +} + +#[cfg(target_os = "linux")] +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run(pa_impl::run); + sp +} + +#[cfg(target_os = "linux")] +mod pa_impl { + use super::*; + #[tokio::main(flavor = "current_thread")] + pub async fn run(sp: GenericService) -> ResultType<()> { + hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc + let mut stream = crate::ipc::connect(1000, "_pa").await?; + unsafe { + AUDIO_ZERO_COUNT = 0; + } + let mut encoder = Encoder::new(crate::platform::linux::PA_SAMPLE_RATE, Stereo, LowDelay)?; + allow_err!( + stream + .send(&crate::ipc::Data::Config(( + "audio-input".to_owned(), + Some(Config::get_option("audio-input")) + ))) + .await + ); + while sp.ok() { + sp.snapshot(|sps| { + sps.send(create_format_msg(crate::platform::linux::PA_SAMPLE_RATE, 2)); + Ok(()) + })?; + if let Some(data) = stream.next_timeout2(1000).await { + match data? { + Some(crate::ipc::Data::RawMessage(bytes)) => { + let data = unsafe { + std::slice::from_raw_parts::(bytes.as_ptr() as _, bytes.len() / 4) + }; + send_f32(data, &mut encoder, &sp); + } + _ => {} + } + } + } + Ok(()) + } +} + +#[cfg(not(target_os = "linux"))] +mod cpal_impl { + use super::*; + use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, SupportedStreamConfig, + }; + + lazy_static::lazy_static! { + static ref HOST: Host = cpal::default_host(); + } + + #[derive(Default)] + pub struct State { + stream: Option<(Box, Arc)>, + } + + impl super::service::Reset for State { + fn reset(&mut self) { + self.stream.take(); + } + } + + pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> { + sp.snapshot(|sps| { + match &state.stream { + None => { + state.stream = Some(play(&sp)?); + } + _ => {} + } + if let Some((_, format)) = &state.stream { + sps.send_shared(format.clone()); + } + Ok(()) + })?; + Ok(()) + } + + fn send( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, + encoder: &mut Encoder, + sp: &GenericService, + ) { + let buffer; + let data = if sample_rate0 != sample_rate { + buffer = crate::common::resample_channels(data, sample_rate0, sample_rate, channels); + &buffer + } else { + data + }; + send_f32(data, encoder, sp); + } + + #[cfg(windows)] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = Config::get_option("audio-input"); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + let device = HOST + .default_output_device() + .with_context(|| "Failed to get default output device for loopback")?; + log::info!( + "Default output device: {}", + device.name().unwrap_or("".to_owned()) + ); + let format = device + .default_output_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get default output format")?; + log::info!("Default output format: {:?}", format); + Ok((device, format)) + } + + #[cfg(not(windows))] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = Config::get_option("audio-input"); + get_audio_input(&audio_input) + } + + fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> { + if audio_input == "Mute" { + bail!("Mute"); + } + let mut device = None; + if !audio_input.is_empty() { + for d in HOST + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + if device.is_none() { + device = Some( + HOST.default_input_device() + .with_context(|| "Failed to get default input device for loopback")?, + ); + } + let device = device.unwrap(); + log::info!("Input device: {}", device.name().unwrap_or("".to_owned())); + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get default input format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + + fn play(sp: &GenericService) -> ResultType<(Box, Arc)> { + let (device, config) = get_device()?; + let sp = sp.clone(); + let err_fn = move |err| { + // too many UnknownErrno, will improve later + log::trace!("an error occurred on stream: {}", err); + }; + // Sample rate must be one of 8000, 12000, 16000, 24000, or 48000. + let sample_rate_0 = config.sample_rate().0; + let sample_rate = if sample_rate_0 < 12000 { + 8000 + } else if sample_rate_0 < 16000 { + 12000 + } else if sample_rate_0 < 24000 { + 16000 + } else if sample_rate_0 < 48000 { + 24000 + } else { + 48000 + }; + log::debug!("Audio sample rate : {}", sample_rate); + unsafe { + AUDIO_ZERO_COUNT = 0; + } + let mut encoder = Encoder::new( + sample_rate, + if config.channels() > 1 { Stereo } else { Mono }, + LowDelay, + )?; + let channels = config.channels(); + let stream = match config.sample_format() { + cpal::SampleFormat::F32 => device.build_input_stream( + &config.into(), + move |data, _: &_| { + send( + data, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + }, + err_fn, + )?, + cpal::SampleFormat::I16 => device.build_input_stream( + &config.into(), + move |data: &[i16], _: &_| { + let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect(); + send( + &buffer, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + }, + err_fn, + )?, + cpal::SampleFormat::U16 => device.build_input_stream( + &config.into(), + move |data: &[u16], _: &_| { + let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect(); + send( + &buffer, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + }, + err_fn, + )?, + }; + stream.play()?; + Ok(( + Box::new(stream), + Arc::new(create_format_msg(sample_rate, channels)), + )) + } +} + +fn create_format_msg(sample_rate: u32, channels: u16) -> Message { + let format = AudioFormat { + sample_rate, + channels: channels as _, + ..Default::default() + }; + let mut misc = Misc::new(); + misc.set_audio_format(format); + let mut msg = Message::new(); + msg.set_misc(misc); + msg +} + +// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time +// every audio data length is set to 480 +// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows) +const MAX_AUDIO_ZERO_COUNT: u16 = 800; +static mut AUDIO_ZERO_COUNT: u16 = 0; + +fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { + if data.iter().filter(|x| **x != 0.).next().is_some() { + unsafe { + AUDIO_ZERO_COUNT = 0; + } + } else { + unsafe { + if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT { + if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 { + log::debug!("Audio Zero Gate Attack"); + AUDIO_ZERO_COUNT += 1; + } + return; + } + AUDIO_ZERO_COUNT += 1; + } + } + match encoder.encode_vec_float(data, data.len() * 6) { + Ok(data) => { + let mut msg_out = Message::new(); + msg_out.set_audio_frame(AudioFrame { + data, + ..Default::default() + }); + sp.send(msg_out); + } + Err(_) => {} + } +} + +#[cfg(test)] +mod tests { + #[cfg(target_os = "linux")] + #[test] + fn test_pulse() { + use libpulse_binding as pulse; + use libpulse_simple_binding as psimple; + let spec = pulse::sample::Spec { + format: pulse::sample::SAMPLE_FLOAT32NE, + channels: 2, + rate: 24000, + }; + let hspec = hound::WavSpec { + channels: spec.channels as _, + sample_rate: spec.rate as _, + bits_per_sample: (4 * 8) as _, + sample_format: hound::SampleFormat::Float, + }; + const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav"); + let mut writer = + hound::WavWriter::create(PATH, hspec).expect("Could not create hsound writer"); + let device = crate::platform::linux::get_pa_monitor(); + let s = psimple::Simple::new( + None, // Use the default server + "Test", // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + "Test", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) + .expect("Could not create simple pulse"); + let mut out: Vec = Vec::with_capacity(1024); + unsafe { + out.set_len(out.capacity()); + } + for _ in 0..600 { + s.read(&mut out).expect("Could not read pcm"); + let out2 = + unsafe { std::slice::from_raw_parts::(out.as_ptr() as _, out.len() / 4) }; + for v in out2 { + writer.write_sample(*v).ok(); + } + } + println!("{:?} {}", device, out.len()); + writer.finalize().expect("Could not finalize writer"); + } +} diff --git a/rust-rdp/rust-desk/src/server/clipboard_service.rs b/rust-rdp/rust-desk/src/server/clipboard_service.rs new file mode 100644 index 0000000..e44b0ed --- /dev/null +++ b/rust-rdp/rust-desk/src/server/clipboard_service.rs @@ -0,0 +1,121 @@ +use super::*; +pub use crate::common::{ + check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, + CONTENT, +}; +use clipboard_master::{CallbackResult, ClipboardHandler, Master}; +use hbb_common::{anyhow, ResultType}; +use std::{ + io, sync, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::SyncSender, + }, + time::Duration, +}; + +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run::<_>(listen::run); + sp +} + +mod listen { + use super::*; + + static RUNNING: AtomicBool = AtomicBool::new(true); + static WAIT: Duration = Duration::from_millis(33); + + struct ClipHandle { + tx: SyncSender<()>, + } + + impl ClipboardHandler for ClipHandle { + fn on_clipboard_change(&mut self) -> CallbackResult { + if !RUNNING.load(Ordering::SeqCst) { + return CallbackResult::Stop; + } + + let _ = self.tx.send(()); + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + if !RUNNING.load(Ordering::SeqCst) { + CallbackResult::Stop + } else { + CallbackResult::StopWithError(error) + } + } + } + + #[tokio::main] + pub async fn run(sp: GenericService) -> ResultType<()> { + let mut ctx = match ClipboardContext::new() { + Ok(ctx) => ctx, + Err(err) => { + log::error!("Failed to start {}: {}", NAME, err); + return Err(anyhow::Error::from(err)); + } + }; + + if !RUNNING.load(Ordering::SeqCst) { + RUNNING.store(true, Ordering::SeqCst); + } + + let (tx, rx) = sync::mpsc::sync_channel(12); + let listener = tokio::spawn(async { + log::info!("Clipboard listener running!"); + let _ = Master::new(ClipHandle { tx }).run(); + }); + + check_clipboard(&mut ctx, None); // initialize CONTENT for snapshot + while sp.ok() { + let mut update = None; + sp.snapshot(|sps| { + if sps.has_subscribes() { + update = check_clipboard(&mut ctx, None); + } + // if there is update, msg will be later together, + // otherwise it will be only sent to new subscriber, + // but old subscribers ignored + if update.is_none() { + let txt = crate::CONTENT.lock().unwrap().clone(); + if !txt.is_empty() { + let msg_out = crate::create_clipboard_msg(txt); + sps.send_shared(Arc::new(msg_out)); + } + } + Ok(()) + })?; + if let Some(msg) = update { + sp.send(msg); + } + + if let Ok(_) = rx.recv_timeout(WAIT) { + if let Some(msg) = check_clipboard(&mut ctx, None) { + sp.send(msg); + } + } + } + + RUNNING.store(false, Ordering::SeqCst); + trigger(&mut ctx); + let _ = listener.await; + log::info!("Clipboard listener stopped!"); + + *CONTENT.lock().unwrap() = Default::default(); + Ok(()) + } + + fn trigger(ctx: &mut ClipboardContext) { + let mut old_text = "".to_owned(); + let _ = match ctx.get_text() { + Ok(text) => { + old_text = text; + } + Err(_) => {} + }; + ctx.set_text(old_text).ok(); + } +} diff --git a/rust-rdp/rust-desk/src/server/connection.rs b/rust-rdp/rust-desk/src/server/connection.rs new file mode 100644 index 0000000..0e899ec --- /dev/null +++ b/rust-rdp/rust-desk/src/server/connection.rs @@ -0,0 +1,1148 @@ +use super::{input_service::*, *}; +use crate::{common::update_clipboard, ipc}; +use hbb_common::{ + config::Config, + fs, + futures::{SinkExt, StreamExt}, + message_proto::{option_message::BoolOption, permission_info::Permission}, + sleep, timeout, + tokio::{ + net::TcpStream, + sync::mpsc, + time::{self, Duration, Instant, Interval}, + }, + tokio_util::codec::{BytesCodec, Framed}, +}; +use sha2::{Digest, Sha256}; +use std::sync::mpsc as std_mpsc; + +pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; + +lazy_static::lazy_static! { + static ref LOGIN_FAILURES: Arc::>> = Default::default(); +} + +#[derive(Clone, Default)] +pub struct ConnInner { + id: i32, + tx: Option, + tx_video: Option, +} + +enum MessageInput { + Mouse((MouseEvent, i32)), + Key((KeyEvent, bool)), + BlockOn, + BlockOff, + PrivacyOn, + PrivacyOff, + Exit, +} + +pub struct Connection { + inner: ConnInner, + stream: super::Stream, + server: super::ServerPtrWeak, + hash: Hash, + read_jobs: Vec, + timer: Interval, + file_transfer: Option<(String, bool)>, + port_forward_socket: Option>, + port_forward_address: String, + tx_to_cm: mpsc::UnboundedSender, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + last_test_delay: i64, + image_quality: i32, + lock_after_session_end: bool, + show_remote_cursor: bool, // by peer + privacy_mode: bool, + ip: String, + disable_clipboard: bool, // by peer + disable_audio: bool, // by peer + tx_input: std_mpsc::Sender, // handle input messages + video_ack_required: bool, +} + +impl Subscriber for ConnInner { + #[inline] + fn id(&self) -> i32 { + self.id + } + + #[inline] + fn send(&mut self, msg: Arc) { + match &msg.union { + Some(message::Union::video_frame(_)) => { + self.tx_video.as_mut().map(|tx| { + allow_err!(tx.send((Instant::now(), msg))); + }); + } + _ => { + self.tx.as_mut().map(|tx| { + allow_err!(tx.send((Instant::now(), msg))); + }); + } + } + } +} + +const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(3); +const SEC30: Duration = Duration::from_secs(30); +const H1: Duration = Duration::from_secs(3600); +const MILLI1: Duration = Duration::from_millis(1); +const SEND_TIMEOUT_VIDEO: u64 = 12_000; +const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; + +impl Connection { + pub async fn start( + addr: SocketAddr, + stream: super::Stream, + id: i32, + server: super::ServerPtrWeak, + ) { + let hash = Hash { + salt: Config::get_salt(), + challenge: Config::get_auto_password(), + ..Default::default() + }; + let (tx_from_cm, mut rx_from_cm) = mpsc::unbounded_channel::(); + let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); + let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); + let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); + let (tx_input, rx_input) = std_mpsc::channel(); + + let tx_cloned = tx.clone(); + let mut conn = Self { + inner: ConnInner { + id, + tx: Some(tx), + tx_video: Some(tx_video), + }, + stream, + server, + hash, + read_jobs: Vec::new(), + timer: time::interval(SEC30), + file_transfer: None, + port_forward_socket: None, + port_forward_address: "".to_owned(), + tx_to_cm, + authorized: false, + keyboard: Config::get_option("enable-keyboard").is_empty(), + clipboard: Config::get_option("enable-clipboard").is_empty(), + audio: Config::get_option("audio-input") != "Mute", + last_test_delay: 0, + image_quality: ImageQuality::Balanced.value(), + lock_after_session_end: false, + show_remote_cursor: false, + privacy_mode: false, + ip: "".to_owned(), + disable_audio: false, + disable_clipboard: false, + tx_input, + video_ack_required: false, + }; + tokio::spawn(async move { + if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await { + log::error!("ipc to connection manager exit: {}", err); + } + }); + if !conn.on_open(addr).await { + return; + } + if !conn.keyboard { + conn.send_permission(Permission::Keyboard, false).await; + } + if !conn.clipboard { + conn.send_permission(Permission::Clipboard, false).await; + } + if !conn.audio { + conn.send_permission(Permission::Audio, false).await; + } + let mut test_delay_timer = + time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT); + let mut last_recv_time = Instant::now(); + + conn.stream.set_send_timeout( + if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() { + SEND_TIMEOUT_OTHER + } else { + SEND_TIMEOUT_VIDEO + }, + ); + + let handler_input = std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned)); + + loop { + tokio::select! { + biased; // video has higher priority + + Some(data) = rx_from_cm.recv() => { + match data { + ipc::Data::Authorize => { + conn.send_logon_response().await; + if conn.port_forward_socket.is_some() { + break; + } + } + ipc::Data::Close => { + let mut misc = Misc::new(); + misc.set_close_reason("Closed manually by the peer".into()); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + conn.send(msg_out).await; + conn.on_close("Close requested from connection manager", false); + break; + } + ipc::Data::ChatMessage{text} => { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + conn.send(msg_out).await; + } + ipc::Data::SwitchPermission{name, enabled} => { + log::info!("Change permission {} -> {}", name, enabled); + if &name == "keyboard" { + conn.keyboard = enabled; + conn.send_permission(Permission::Keyboard, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + conn.inner.clone(), enabled || conn.show_remote_cursor); + } + } else if &name == "clipboard" { + conn.clipboard = enabled; + conn.send_permission(Permission::Clipboard, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.clipboard_enabled() && conn.keyboard); + } + } else if &name == "audio" { + conn.audio = enabled; + conn.send_permission(Permission::Audio, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } + } + ipc::Data::RawMessage(bytes) => { + allow_err!(conn.stream.send_raw(bytes).await); + } + _ => {} + } + }, + res = conn.stream.next() => { + if let Some(res) = res { + match res { + Err(err) => { + conn.on_close(&err.to_string(), true); + break; + }, + Ok(bytes) => { + last_recv_time = Instant::now(); + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if !conn.on_message(msg_in).await { + break; + } + } + } + } + } else { + conn.on_close("Reset by the peer", true); + break; + } + }, + _ = conn.timer.tick() => { + if !conn.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { + conn.on_close(&err.to_string(), false); + break; + } + } else { + conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + }, + Some((instant, value)) = rx_video.recv() => { + if !conn.video_ack_required { + video_service::notify_video_frame_feched(id, Some(instant.into())); + } + if let Err(err) = conn.stream.send(&value as &Message).await { + conn.on_close(&err.to_string(), false); + break; + } + }, + Some((instant, value)) = rx.recv() => { + let latency = instant.elapsed().as_millis() as i64; + let msg: &Message = &value; + + if latency > 1000 { + match &msg.union { + Some(message::Union::audio_frame(_)) => { + // log::info!("audio frame latency {}", instant.elapsed().as_secs_f32()); + continue; + } + _ => {} + } + } + if let Err(err) = conn.stream.send(msg).await { + conn.on_close(&err.to_string(), false); + break; + } + }, + _ = test_delay_timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + conn.on_close("Timeout", true); + break; + } + let time = crate::get_time(); + if time > 0 && conn.last_test_delay == 0 { + conn.last_test_delay = time; + let mut msg_out = Message::new(); + msg_out.set_test_delay(TestDelay{ + time, + ..Default::default() + }); + conn.inner.send(msg_out.into()); + } + } + } + } + + video_service::notify_video_frame_feched(id, None); + super::video_service::update_test_latency(id, 0); + super::video_service::update_image_quality(id, None); + if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { + conn.on_close(&err.to_string(), false); + } + + conn.tx_input.send(MessageInput::Exit).ok(); + // join at the end so that not blocking video + if let Err(e) = handler_input.join() { + log::error!("Failed to join input thread, {:?}", e); + } else { + log::info!("input thread exited"); + } + + let _ = crate::platform::block_input(false); + crate::platform::toggle_blank_screen(false); + log::info!("#{} connection loop exited", id); + } + + fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { + let mut block_input_mode = false; + let (tx_blank, rx_blank) = std_mpsc::channel(); + + let handler_blank = std::thread::spawn(|| Self::handle_blank(rx_blank)); + + loop { + match receiver.recv_timeout(std::time::Duration::from_millis(500)) { + Ok(v) => match v { + MessageInput::Mouse((msg, id)) => { + handle_mouse(&msg, id); + } + MessageInput::Key((mut msg, press)) => { + if press { + msg.down = true; + } + handle_key(&msg); + if press { + msg.down = false; + handle_key(&msg); + } + } + MessageInput::BlockOn => { + if crate::platform::block_input(true) { + block_input_mode = true; + } else { + Self::send_option_error(&tx, "Failed to turn on block input mode"); + } + } + MessageInput::BlockOff => { + if crate::platform::block_input(false) { + block_input_mode = false; + } else { + Self::send_option_error(&tx, "Failed to turn off block input mode"); + } + } + MessageInput::PrivacyOn => { + if crate::platform::block_input(true) { + block_input_mode = true; + } + tx_blank.send(MessageInput::PrivacyOn).ok(); + } + MessageInput::PrivacyOff => { + if crate::platform::block_input(false) { + block_input_mode = false; + } + tx_blank.send(MessageInput::PrivacyOff).ok(); + } + MessageInput::Exit => break, + }, + _ => { + if block_input_mode { + let _ = crate::platform::block_input(true); + } + } + } + } + + tx_blank.send(MessageInput::Exit).ok(); + if let Err(_) = handler_blank.join() { + log::error!("Failed to join blank thread handler"); + } else { + log::info!("Blank thread exited"); + } + } + + fn handle_blank(receiver: std_mpsc::Receiver) { + let mut last_privacy = false; + loop { + match receiver.recv_timeout(std::time::Duration::from_millis(500)) { + Ok(v) => match v { + MessageInput::PrivacyOn => { + crate::platform::toggle_blank_screen(true); + last_privacy = true; + } + MessageInput::PrivacyOff => { + crate::platform::toggle_blank_screen(false); + last_privacy = false; + } + _ => break, + }, + _ => { + if last_privacy { + crate::platform::toggle_blank_screen(true); + } + } + } + } + } + + async fn try_port_forward_loop( + &mut self, + rx_from_cm: &mut mpsc::UnboundedReceiver, + ) -> ResultType<()> { + let mut last_recv_time = Instant::now(); + if let Some(forward) = self.port_forward_socket.as_mut() { + log::info!("Running port forwarding loop"); + self.stream.set_raw(); + loop { + tokio::select! { + Some(data) = rx_from_cm.recv() => { + match data { + ipc::Data::Close => { + bail!("Close requested from selfection manager"); + } + _ => {} + } + } + res = forward.next() => { + if let Some(res) = res { + last_recv_time = Instant::now(); + self.stream.send_bytes(res?.into()).await?; + } else { + bail!("Forward reset by the peer"); + } + }, + res = self.stream.next() => { + if let Some(res) = res { + last_recv_time = Instant::now(); + timeout(SEND_TIMEOUT_OTHER, forward.send(res?.into())).await??; + } else { + bail!("Stream reset by the peer"); + } + }, + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= H1 { + bail!("Timeout"); + } + } + } + } + } + Ok(()) + } + + async fn send_permission(&mut self, permission: Permission, enabled: bool) { + let mut misc = Misc::new(); + misc.set_permission_info(PermissionInfo { + permission: permission.into(), + enabled, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + + async fn on_open(&mut self, addr: SocketAddr) -> bool { + log::debug!("#{} Connection opened from {}.", self.inner.id, addr); + let whitelist: Vec = Config::get_option("whitelist") + .split(",") + .filter(|x| !x.is_empty()) + .map(|x| x.to_owned()) + .collect(); + if !whitelist.is_empty() + && whitelist + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() + && whitelist + .iter() + .filter(|x| x.parse() == Ok(addr.ip())) + .next() + .is_none() + { + self.send_login_error("Your ip is blocked by the peer") + .await; + sleep(1.).await; + return false; + } + self.ip = addr.ip().to_string(); + let mut msg_out = Message::new(); + msg_out.set_hash(self.hash.clone()); + self.send(msg_out).await; + true + } + + async fn send_logon_response(&mut self) { + if self.authorized { + return; + } + #[allow(unused_mut)] + let mut username = crate::platform::get_active_username(); + let mut res = LoginResponse::new(); + if self.port_forward_socket.is_some() { + let mut msg_out = Message::new(); + res.set_peer_info(PeerInfo { + hostname: whoami::hostname(), + username, + platform: whoami::platform().to_string(), + version: crate::VERSION.to_owned(), + ..Default::default() + }); + msg_out.set_login_response(res); + self.send(msg_out).await; + return; + } + #[cfg(target_os = "linux")] + if !self.file_transfer.is_some() { + let dtype = crate::platform::linux::get_display_server(); + if dtype != "x11" { + res.set_error(format!( + "Unsupported display server type {}, x11 expected", + dtype + )); + let mut msg_out = Message::new(); + msg_out.set_login_response(res); + self.send(msg_out).await; + return; + } + } + #[allow(unused_mut)] + let mut sas_enabled = false; + #[cfg(windows)] + if crate::platform::is_root() { + sas_enabled = true; + } + if self.file_transfer.is_some() { + if crate::platform::is_prelogin() || self.tx_to_cm.send(ipc::Data::Test).is_err() { + username = "".to_owned(); + } + } + self.authorized = true; + let mut pi = PeerInfo { + hostname: whoami::hostname(), + username, + platform: whoami::platform().to_string(), + version: crate::VERSION.to_owned(), + sas_enabled, + ..Default::default() + }; + let mut sub_service = false; + if self.file_transfer.is_some() { + res.set_peer_info(pi); + } else { + try_activate_screen(); + match super::video_service::get_displays() { + Err(err) => { + res.set_error(format!("X11 error: {}", err)); + } + Ok((current, displays)) => { + pi.displays = displays.into(); + pi.current_display = current as _; + res.set_peer_info(pi); + sub_service = true; + } + } + } + let mut msg_out = Message::new(); + msg_out.set_login_response(res); + self.send(msg_out).await; + if let Some((dir, show_hidden)) = self.file_transfer.clone() { + let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { + &dir + } else { + "" + }; + self.read_dir(dir, show_hidden); + } else if sub_service { + if let Some(s) = self.server.upgrade() { + let mut noperms = Vec::new(); + if !self.keyboard && !self.show_remote_cursor { + noperms.push(NAME_CURSOR); + } + if !self.show_remote_cursor { + noperms.push(NAME_POS); + } + if !self.clipboard_enabled() || !self.keyboard { + noperms.push(super::clipboard_service::NAME); + } + if !self.audio_enabled() { + noperms.push(super::audio_service::NAME); + } + s.write() + .unwrap() + .add_connection(self.inner.clone(), &noperms); + } + } + } + + fn clipboard_enabled(&self) -> bool { + self.clipboard && !self.disable_clipboard + } + + fn audio_enabled(&self) -> bool { + self.audio && !self.disable_audio + } + + async fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { + self.send_to_cm(ipc::Data::Login { + id: self.inner.id(), + is_file_transfer: self.file_transfer.is_some(), + port_forward: self.port_forward_address.clone(), + peer_id, + name, + authorized, + keyboard: self.keyboard, + clipboard: self.clipboard, + audio: self.audio, + }); + } + + #[inline] + fn send_to_cm(&mut self, data: ipc::Data) { + self.tx_to_cm.send(data).ok(); + } + + #[inline] + fn send_fs(&mut self, data: ipc::FS) { + self.send_to_cm(ipc::Data::FS(data)); + } + + async fn send_login_error(&mut self, err: T) { + let mut msg_out = Message::new(); + let mut res = LoginResponse::new(); + res.set_error(err.to_string()); + msg_out.set_login_response(res); + self.send(msg_out).await; + } + + fn send_option_error(s: &Sender, err: T) { + let mut msg_out = Message::new(); + let mut res = OptionResponse::new(); + let mut misc = Misc::new(); + res.error = err.to_string(); + + misc.set_option_response(res); + msg_out.set_misc(misc); + s.send((Instant::now(), Arc::new(msg_out))).ok(); + } + + #[inline] + fn input_mouse(&self, msg: MouseEvent, conn_id: i32) { + self.tx_input.send(MessageInput::Mouse((msg, conn_id))).ok(); + } + + #[inline] + fn input_key(&self, msg: KeyEvent, press: bool) { + self.tx_input.send(MessageInput::Key((msg, press))).ok(); + } + + async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::login_request(lr)) = msg.union { + if let Some(o) = lr.option.as_ref() { + self.update_option(o).await; + } + self.video_ack_required = lr.video_ack_required; + if self.authorized { + return true; + } + match lr.union { + Some(login_request::Union::file_transfer(ft)) => { + if !Config::get_option("enable-file-transfer").is_empty() { + self.send_login_error("No permission of file transfer") + .await; + sleep(1.).await; + return false; + } + self.file_transfer = Some((ft.dir, ft.show_hidden)); + } + Some(login_request::Union::port_forward(mut pf)) => { + if !Config::get_option("enable-tunnel").is_empty() { + self.send_login_error("No permission of IP tunneling").await; + sleep(1.).await; + return false; + } + let mut is_rdp = false; + if pf.host == "RDP" && pf.port == 0 { + pf.host = "localhost".to_owned(); + pf.port = 3389; + is_rdp = true; + } + if pf.host.is_empty() { + pf.host = "localhost".to_owned(); + } + let mut addr = format!("{}:{}", pf.host, pf.port); + self.port_forward_address = addr.clone(); + match timeout(3000, TcpStream::connect(&addr)).await { + Ok(Ok(sock)) => { + self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new())); + } + _ => { + if is_rdp { + addr = "RDP".to_owned(); + } + self.send_login_error(format!( + "Failed to access remote {}, please make sure if it is open", + addr + )) + .await; + } + } + } + _ => {} + } + if !crate::is_ip(&lr.username) && lr.username != Config::get_id() { + self.send_login_error("Offline").await; + } else if lr.password.is_empty() { + self.try_start_cm(lr.my_id, lr.my_name, false).await; + } else { + let mut hasher = Sha256::new(); + hasher.update(&Config::get_password()); + hasher.update(&self.hash.salt); + let mut hasher2 = Sha256::new(); + hasher2.update(&hasher.finalize()[..]); + hasher2.update(&self.hash.challenge); + let mut failure = LOGIN_FAILURES + .lock() + .unwrap() + .get(&self.ip) + .map(|x| x.clone()) + .unwrap_or((0, 0, 0)); + let time = (crate::get_time() / 60_000) as i32; + if failure.2 > 30 { + self.send_login_error("Too many wrong password attempts") + .await; + } else if time == failure.0 && failure.1 > 6 { + self.send_login_error("Please try 1 minute later").await; + } else if hasher2.finalize()[..] != lr.password[..] { + if failure.0 == time { + failure.1 += 1; + failure.2 += 1; + } else { + failure.0 = time; + failure.1 = 1; + failure.2 += 1; + } + LOGIN_FAILURES + .lock() + .unwrap() + .insert(self.ip.clone(), failure); + self.send_login_error("Wrong Password").await; + self.try_start_cm(lr.my_id, lr.my_name, false).await; + } else { + if failure.0 != 0 { + LOGIN_FAILURES.lock().unwrap().remove(&self.ip); + } + self.send_logon_response().await; + self.try_start_cm(lr.my_id, lr.my_name, true).await; + if self.port_forward_socket.is_some() { + return false; + } + } + } + } else if let Some(message::Union::test_delay(t)) = msg.union { + if t.from_client { + let mut msg_out = Message::new(); + msg_out.set_test_delay(t); + self.inner.send(msg_out.into()); + } else { + self.last_test_delay = 0; + let latency = crate::get_time() - t.time; + if latency > 0 { + super::video_service::update_test_latency(self.inner.id(), latency); + } + } + } else if self.authorized { + match msg.union { + Some(message::Union::mouse_event(me)) => { + if self.keyboard { + self.input_mouse(me, self.inner.id()); + } + } + Some(message::Union::key_event(me)) => { + if self.keyboard { + // handle all down as press + // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which + // make sure all key are released + let is_press = if cfg!(target_os = "linux") { + (me.press || me.down) && !crate::is_modifier(&me) + } else { + me.press + }; + if is_press { + match me.union { + Some(key_event::Union::unicode(_)) + | Some(key_event::Union::seq(_)) => { + self.input_key(me, false); + } + _ => { + self.input_key(me, true); + } + } + } else { + self.input_key(me, false); + } + } + } + Some(message::Union::clipboard(cb)) => { + if self.clipboard { + update_clipboard(cb, None); + } + } + Some(message::Union::file_action(fa)) => { + if self.file_transfer.is_some() { + match fa.union { + Some(file_action::Union::read_dir(rd)) => { + self.read_dir(&rd.path, rd.include_hidden); + } + Some(file_action::Union::all_files(f)) => { + match fs::get_recursive_files(&f.path, f.include_hidden) { + Err(err) => { + self.send(fs::new_error(f.id, err, -1)).await; + } + Ok(files) => { + self.send(fs::new_dir(f.id, files)).await; + } + } + } + Some(file_action::Union::send(s)) => { + let id = s.id; + match fs::TransferJob::new_read(id, s.path, s.include_hidden) { + Err(err) => { + self.send(fs::new_error(id, err, 0)).await; + } + Ok(job) => { + self.send(fs::new_dir(id, job.files().to_vec())).await; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + Some(file_action::Union::receive(r)) => { + self.send_fs(ipc::FS::NewWrite { + path: r.path, + id: r.id, + files: r + .files + .to_vec() + .drain(..) + .map(|f| (f.name, f.modified_time)) + .collect(), + }); + } + Some(file_action::Union::remove_dir(d)) => { + self.send_fs(ipc::FS::RemoveDir { + path: d.path, + id: d.id, + recursive: d.recursive, + }); + } + Some(file_action::Union::remove_file(f)) => { + self.send_fs(ipc::FS::RemoveFile { + path: f.path, + id: f.id, + file_num: f.file_num, + }); + } + Some(file_action::Union::create(c)) => { + self.send_fs(ipc::FS::CreateDir { + path: c.path, + id: c.id, + }); + } + Some(file_action::Union::cancel(c)) => { + self.send_fs(ipc::FS::CancelWrite { id: c.id }); + fs::remove_job(c.id, &mut self.read_jobs); + } + _ => {} + } + } + } + Some(message::Union::file_response(fr)) => match fr.union { + Some(file_response::Union::block(block)) => { + self.send_fs(ipc::FS::WriteBlock { + id: block.id, + file_num: block.file_num, + data: block.data, + compressed: block.compressed, + }); + } + Some(file_response::Union::done(d)) => { + self.send_fs(ipc::FS::WriteDone { + id: d.id, + file_num: d.file_num, + }); + } + _ => {} + }, + Some(message::Union::misc(misc)) => match misc.union { + Some(misc::Union::switch_display(s)) => { + super::video_service::switch_display(s.display); + } + Some(misc::Union::chat_message(c)) => { + self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); + } + Some(misc::Union::option(o)) => { + self.update_option(&o).await; + } + Some(misc::Union::refresh_video(r)) => { + if r { + super::video_service::refresh(); + } + } + Some(misc::Union::video_received(_)) => { + video_service::notify_video_frame_feched( + self.inner.id, + Some(Instant::now().into()), + ); + } + _ => {} + }, + _ => {} + } + } + true + } + + async fn update_option(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); + if let Ok(q) = o.image_quality.enum_value() { + self.image_quality = q.value(); + super::video_service::update_image_quality(self.inner.id(), Some(q.value())); + } + let q = o.custom_image_quality; + if q > 0 { + self.image_quality = q; + super::video_service::update_image_quality(self.inner.id(), Some(q)); + } + if let Ok(q) = o.lock_after_session_end.enum_value() { + if q != BoolOption::NotSet { + self.lock_after_session_end = q == BoolOption::Yes; + } + } + if let Ok(q) = o.show_remote_cursor.enum_value() { + if q != BoolOption::NotSet { + self.show_remote_cursor = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.keyboard || self.show_remote_cursor, + ); + s.write().unwrap().subscribe( + NAME_POS, + self.inner.clone(), + self.show_remote_cursor, + ); + } + } + } + if let Ok(q) = o.disable_audio.enum_value() { + if q != BoolOption::NotSet { + self.disable_audio = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } + } + } + if let Ok(q) = o.disable_clipboard.enum_value() { + if q != BoolOption::NotSet { + self.disable_clipboard = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + self.inner.clone(), + self.clipboard_enabled() && self.keyboard, + ); + } + } + } + if let Ok(q) = o.privacy_mode.enum_value() { + if self.keyboard { + match q { + BoolOption::Yes => { + self.privacy_mode = true; + self.tx_input.send(MessageInput::PrivacyOn).ok(); + } + BoolOption::No => { + self.privacy_mode = false; + self.tx_input.send(MessageInput::PrivacyOff).ok(); + } + _ => {} + } + } + } + if self.keyboard { + if let Ok(q) = o.block_input.enum_value() { + match q { + BoolOption::Yes => { + self.tx_input.send(MessageInput::BlockOn).ok(); + } + BoolOption::No => { + self.tx_input.send(MessageInput::BlockOff).ok(); + } + _ => {} + } + } + } + } + + fn on_close(&mut self, reason: &str, lock: bool) { + if let Some(s) = self.server.upgrade() { + s.write().unwrap().remove_connection(&self.inner); + } + log::info!("#{} Connection closed: {}", self.inner.id(), reason); + if lock && self.lock_after_session_end && self.keyboard { + crate::platform::lock_screen(); + super::video_service::switch_to_primary(); + } + self.tx_to_cm.send(ipc::Data::Close).ok(); + self.port_forward_socket.take(); + } + + fn read_dir(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadDir { + dir, + include_hidden, + }); + } + + #[inline] + async fn send(&mut self, msg: Message) { + allow_err!(self.stream.send(&msg).await); + } +} + +async fn start_ipc( + mut rx_to_cm: mpsc::UnboundedReceiver, + tx_from_cm: mpsc::UnboundedSender, +) -> ResultType<()> { + loop { + if !crate::platform::is_prelogin() { + break; + } + sleep(1.).await; + } + let mut stream = None; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + } else { + let run_done; + if crate::platform::is_root() { + let mut res = Ok(None); + for _ in 0..10 { + res = crate::platform::run_as_user("--cm"); + if res.is_ok() { + break; + } + sleep(1.).await; + } + if let Some(task) = res? { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + super::CHILD_PROCESS + .lock() + .unwrap() + .push(crate::run_me(vec!["--cm"])?); + } + for _ in 0..10 { + sleep(0.3).await; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + break; + } + } + if stream.is_none() { + bail!("Failed to connect to connection manager"); + } + } + let mut stream = stream.unwrap(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + return Err(err.into()); + } + Ok(Some(data)) => { + tx_from_cm.send(data)?; + } + _ => {} + } + } + res = rx_to_cm.recv() => { + match res { + Some(data) => { + stream.send(&data).await?; + } + None => { + bail!("expected"); + } + } + } + } + } +} + +// in case screen is sleep and blank, here to activate it +fn try_activate_screen() { + #[cfg(windows)] + std::thread::spawn(|| { + mouse_move_relative(-6, -6); + std::thread::sleep(std::time::Duration::from_millis(30)); + mouse_move_relative(6, 6); + }); +} diff --git a/rust-rdp/rust-desk/src/server/input_service.rs b/rust-rdp/rust-desk/src/server/input_service.rs new file mode 100644 index 0000000..7f12baa --- /dev/null +++ b/rust-rdp/rust-desk/src/server/input_service.rs @@ -0,0 +1,671 @@ +use super::*; +#[cfg(target_os = "macos")] +use dispatch::Queue; +use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; +use hbb_common::{config::COMPRESS_LEVEL, protobuf::ProtobufEnumOrUnknown}; +use std::{ + convert::TryFrom, + sync::atomic::{AtomicBool, Ordering}, + time::Instant, +}; + +#[derive(Default)] +struct StateCursor { + hcursor: u64, + cursor_data: Arc, + cached_cursor_data: HashMap>, +} + +impl super::service::Reset for StateCursor { + fn reset(&mut self) { + *self = Default::default(); + crate::platform::reset_input_cache(); + } +} + +#[derive(Default)] +struct StatePos { + cursor_pos: (i32, i32), +} + +impl super::service::Reset for StatePos { + fn reset(&mut self) { + self.cursor_pos = (0, 0); + } +} + +#[derive(Default)] +struct Input { + conn: i32, + time: i64, +} + +const KEY_CHAR_START: u64 = 9999; + +#[derive(Clone, Default)] +pub struct MouseCursorSub { + inner: ConnInner, + cached: HashMap>, +} + +impl From for MouseCursorSub { + fn from(inner: ConnInner) -> Self { + Self { + inner, + cached: HashMap::new(), + } + } +} + +impl Subscriber for MouseCursorSub { + #[inline] + fn id(&self) -> i32 { + self.inner.id() + } + + #[inline] + fn send(&mut self, msg: Arc) { + if let Some(message::Union::cursor_data(cd)) = &msg.union { + if let Some(msg) = self.cached.get(&cd.id) { + self.inner.send(msg.clone()); + } else { + self.inner.send(msg.clone()); + let mut tmp = Message::new(); + // only send id out, require client side cache also + tmp.set_cursor_id(cd.id); + self.cached.insert(cd.id, Arc::new(tmp)); + } + } else { + self.inner.send(msg); + } + } +} + +pub const NAME_CURSOR: &'static str = "mouse_cursor"; +pub const NAME_POS: &'static str = "mouse_pos"; +pub type MouseCursorService = ServiceTmpl; + +pub fn new_cursor() -> MouseCursorService { + let sp = MouseCursorService::new(NAME_CURSOR, true); + sp.repeat::(33, run_cursor); + sp +} + +pub fn new_pos() -> GenericService { + let sp = GenericService::new(NAME_POS, false); + sp.repeat::(33, run_pos); + sp +} + +fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { + if let Some((x, y)) = crate::get_cursor_pos() { + if state.cursor_pos.0 != x || state.cursor_pos.1 != y { + state.cursor_pos = (x, y); + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x, + y, + ..Default::default() + }); + let exclude = { + let now = crate::get_time(); + let lock = LATEST_INPUT.lock().unwrap(); + if now - lock.time < 300 { + lock.conn + } else { + 0 + } + }; + sp.send_without(msg_out, exclude); + } + } + + sp.snapshot(|sps| { + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x: state.cursor_pos.0, + y: state.cursor_pos.1, + ..Default::default() + }); + sps.send(msg_out); + Ok(()) + })?; + Ok(()) +} + +fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> { + if let Some(hcursor) = crate::get_cursor()? { + if hcursor != state.hcursor { + let msg; + if let Some(cached) = state.cached_cursor_data.get(&hcursor) { + super::log::trace!("Cursor data cached, hcursor: {}", hcursor); + msg = cached.clone(); + } else { + let mut data = crate::get_cursor_data(hcursor)?; + data.colors = hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL); + let mut tmp = Message::new(); + tmp.set_cursor_data(data); + msg = Arc::new(tmp); + state.cached_cursor_data.insert(hcursor, msg.clone()); + super::log::trace!("Cursor data updated, hcursor: {}", hcursor); + } + state.hcursor = hcursor; + sp.send_shared(msg.clone()); + state.cursor_data = msg; + } + } + sp.snapshot(|sps| { + sps.send_shared(state.cursor_data.clone()); + Ok(()) + })?; + Ok(()) +} + +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); + static ref KEYS_DOWN: Arc>> = Default::default(); + static ref LATEST_INPUT: Arc> = Default::default(); +} +static EXITING: AtomicBool = AtomicBool::new(false); + +// mac key input must be run in main thread, otherwise crash on >= osx 10.15 +#[cfg(target_os = "macos")] +lazy_static::lazy_static! { + static ref QUEUE: Queue = Queue::main(); + static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); +} + +pub fn is_left_up(evt: &MouseEvent) -> bool { + let buttons = evt.mask >> 3; + let evt_type = evt.mask & 0x7; + return buttons == 1 && evt_type == 2; +} + +#[cfg(windows)] +pub fn mouse_move_relative(x: i32, y: i32) { + crate::platform::windows::try_change_desktop(); + let mut en = ENIGO.lock().unwrap(); + en.mouse_move_relative(x, y); +} + +#[cfg(not(target_os = "macos"))] +fn modifier_sleep() { + // sleep for a while, this is only for keying in rdp in peer so far + #[cfg(windows)] + std::thread::sleep(std::time::Duration::from_nanos(1)); +} + +#[inline] +fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { + // on Linux, if RightAlt is down, RightAlt status is false, Alt status is true + // but on Windows, both are true + let x = en.get_key_state(key.clone()); + match key { + Key::Shift => x || en.get_key_state(Key::RightShift), + Key::Control => x || en.get_key_state(Key::RightControl), + Key::Alt => x || en.get_key_state(Key::RightAlt), + Key::Meta => x || en.get_key_state(Key::RWin), + Key::RightShift => x || en.get_key_state(Key::Shift), + Key::RightControl => x || en.get_key_state(Key::Control), + Key::RightAlt => x || en.get_key_state(Key::Alt), + Key::RWin => x || en.get_key_state(Key::Meta), + _ => x, + } +} + +pub fn handle_mouse(evt: &MouseEvent, conn: i32) { + #[cfg(target_os = "macos")] + if !*IS_SERVER { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_mouse_(&evt, conn)); + return; + } + handle_mouse_(evt, conn); +} + +pub fn fix_key_down_timeout_loop() { + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_millis(300)); + fix_key_down_timeout(false); + }); + if let Err(err) = ctrlc::set_handler(move || { + fix_key_down_timeout_at_exit(); + std::process::exit(0); // will call atexit on posix, but not on Windows + }) { + log::error!("Failed to set Ctrl-C handler: {}", err); + } +} + +pub fn fix_key_down_timeout_at_exit() { + if EXITING.load(Ordering::SeqCst) { + return; + } + EXITING.store(true, Ordering::SeqCst); + fix_key_down_timeout(true); + log::info!("fix_key_down_timeout_at_exit"); +} + +#[inline] +fn get_layout(key: u32) -> Key { + Key::Layout(std::char::from_u32(key).unwrap_or('\0')) +} + +fn fix_key_down_timeout(force: bool) { + if KEYS_DOWN.lock().unwrap().is_empty() { + return; + } + let cloned = (*KEYS_DOWN.lock().unwrap()).clone(); + log::debug!("{} keys in key down timeout map", cloned.len()); + for (key, value) in cloned.into_iter() { + if force || value.elapsed().as_millis() >= 3_000 { + KEYS_DOWN.lock().unwrap().remove(&key); + let key = if key < KEY_CHAR_START { + if let Some(key) = KEY_MAP.get(&(key as _)) { + Some(*key) + } else { + None + } + } else { + Some(get_layout((key - KEY_CHAR_START) as _)) + }; + if let Some(key) = key { + let func = move || { + let mut en = ENIGO.lock().unwrap(); + if get_modifier_state(key, &mut en) { + en.key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + }; + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); + } + } + } +} + +// e.g. current state of ctrl is down, but ctrl not in modifier, we should change ctrl to up, to make modifier state sync between remote and local +#[inline] +fn fix_modifier( + modifiers: &[ProtobufEnumOrUnknown], + key0: ControlKey, + key1: Key, + en: &mut Enigo, +) { + if get_modifier_state(key1, en) && !modifiers.contains(&ProtobufEnumOrUnknown::new(key0)) { + en.key_up(key1); + log::debug!("Fixed {:?}", key1); + } +} + +fn fix_modifiers(modifiers: &[ProtobufEnumOrUnknown], en: &mut Enigo, ck: i32) { + if ck != ControlKey::Shift.value() { + fix_modifier(modifiers, ControlKey::Shift, Key::Shift, en); + } + if ck != ControlKey::RShift.value() { + fix_modifier(modifiers, ControlKey::Shift, Key::RightShift, en); + } + if ck != ControlKey::Alt.value() { + fix_modifier(modifiers, ControlKey::Alt, Key::Alt, en); + } + if ck != ControlKey::RAlt.value() { + fix_modifier(modifiers, ControlKey::Alt, Key::RightAlt, en); + } + if ck != ControlKey::Control.value() { + fix_modifier(modifiers, ControlKey::Control, Key::Control, en); + } + if ck != ControlKey::RControl.value() { + fix_modifier(modifiers, ControlKey::Control, Key::RightControl, en); + } + if ck != ControlKey::Meta.value() { + fix_modifier(modifiers, ControlKey::Meta, Key::Meta, en); + } + if ck != ControlKey::RWin.value() { + fix_modifier(modifiers, ControlKey::Meta, Key::RWin, en); + } +} + +fn handle_mouse_(evt: &MouseEvent, conn: i32) { + if EXITING.load(Ordering::SeqCst) { + return; + } + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let buttons = evt.mask >> 3; + let evt_type = evt.mask & 0x7; + if evt_type == 0 { + let time = crate::get_time(); + *LATEST_INPUT.lock().unwrap() = Input { time, conn }; + } + let mut en = ENIGO.lock().unwrap(); + #[cfg(not(target_os = "macos"))] + let mut to_release = Vec::new(); + fix_modifiers(&evt.modifiers[..], &mut en, 0); + if evt_type == 1 { + #[cfg(target_os = "macos")] + en.reset_flag(); + for ref ck in evt.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + #[cfg(target_os = "macos")] + en.add_flag(key); + #[cfg(not(target_os = "macos"))] + if key != &Key::CapsLock && key != &Key::NumLock { + if !get_modifier_state(key.clone(), &mut en) { + en.key_down(key.clone()).ok(); + modifier_sleep(); + to_release.push(key); + } else { + KEYS_DOWN.lock().unwrap().insert(ck.value() as _, Instant::now()); + } + } + } + } + } + match evt_type { + 0 => { + en.mouse_move_to(evt.x, evt.y); + } + 1 => match buttons { + 1 => { + allow_err!(en.mouse_down(MouseButton::Left)); + } + 2 => { + allow_err!(en.mouse_down(MouseButton::Right)); + } + 4 => { + allow_err!(en.mouse_down(MouseButton::Middle)); + } + _ => {} + }, + 2 => match buttons { + 1 => { + en.mouse_up(MouseButton::Left); + } + 2 => { + en.mouse_up(MouseButton::Right); + } + 4 => { + en.mouse_up(MouseButton::Middle); + } + _ => {} + }, + 3 => { + #[allow(unused_mut)] + let mut x = evt.x; + #[allow(unused_mut)] + let mut y = evt.y; + #[cfg(not(windows))] + { + x = -x; + y = -y; + } + if x != 0 { + en.mouse_scroll_x(x); + } + if y != 0 { + en.mouse_scroll_y(y); + } + } + _ => {} + } + #[cfg(not(target_os = "macos"))] + for key in to_release { + en.key_up(key.clone()); + } +} + +pub fn is_enter(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::control_key(ck)) = evt.union { + if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value() + { + return true; + } + } + return false; +} + +lazy_static::lazy_static! { + static ref KEY_MAP: HashMap = + [ + (ControlKey::Alt, Key::Alt), + (ControlKey::Backspace, Key::Backspace), + (ControlKey::CapsLock, Key::CapsLock), + (ControlKey::Control, Key::Control), + (ControlKey::Delete, Key::Delete), + (ControlKey::DownArrow, Key::DownArrow), + (ControlKey::End, Key::End), + (ControlKey::Escape, Key::Escape), + (ControlKey::F1, Key::F1), + (ControlKey::F10, Key::F10), + (ControlKey::F11, Key::F11), + (ControlKey::F12, Key::F12), + (ControlKey::F2, Key::F2), + (ControlKey::F3, Key::F3), + (ControlKey::F4, Key::F4), + (ControlKey::F5, Key::F5), + (ControlKey::F6, Key::F6), + (ControlKey::F7, Key::F7), + (ControlKey::F8, Key::F8), + (ControlKey::F9, Key::F9), + (ControlKey::Home, Key::Home), + (ControlKey::LeftArrow, Key::LeftArrow), + (ControlKey::Meta, Key::Meta), + (ControlKey::Option, Key::Option), + (ControlKey::PageDown, Key::PageDown), + (ControlKey::PageUp, Key::PageUp), + (ControlKey::Return, Key::Return), + (ControlKey::RightArrow, Key::RightArrow), + (ControlKey::Shift, Key::Shift), + (ControlKey::Space, Key::Space), + (ControlKey::Tab, Key::Tab), + (ControlKey::UpArrow, Key::UpArrow), + (ControlKey::Numpad0, Key::Numpad0), + (ControlKey::Numpad1, Key::Numpad1), + (ControlKey::Numpad2, Key::Numpad2), + (ControlKey::Numpad3, Key::Numpad3), + (ControlKey::Numpad4, Key::Numpad4), + (ControlKey::Numpad5, Key::Numpad5), + (ControlKey::Numpad6, Key::Numpad6), + (ControlKey::Numpad7, Key::Numpad7), + (ControlKey::Numpad8, Key::Numpad8), + (ControlKey::Numpad9, Key::Numpad9), + (ControlKey::Cancel, Key::Cancel), + (ControlKey::Clear, Key::Clear), + (ControlKey::Menu, Key::Alt), + (ControlKey::Pause, Key::Pause), + (ControlKey::Kana, Key::Kana), + (ControlKey::Hangul, Key::Hangul), + (ControlKey::Junja, Key::Junja), + (ControlKey::Final, Key::Final), + (ControlKey::Hanja, Key::Hanja), + (ControlKey::Kanji, Key::Kanji), + (ControlKey::Convert, Key::Convert), + (ControlKey::Select, Key::Select), + (ControlKey::Print, Key::Print), + (ControlKey::Execute, Key::Execute), + (ControlKey::Snapshot, Key::Snapshot), + (ControlKey::Insert, Key::Insert), + (ControlKey::Help, Key::Help), + (ControlKey::Sleep, Key::Sleep), + (ControlKey::Separator, Key::Separator), + (ControlKey::Scroll, Key::Scroll), + (ControlKey::NumLock, Key::NumLock), + (ControlKey::RWin, Key::RWin), + (ControlKey::Apps, Key::Apps), + (ControlKey::Multiply, Key::Multiply), + (ControlKey::Add, Key::Add), + (ControlKey::Subtract, Key::Subtract), + (ControlKey::Decimal, Key::Decimal), + (ControlKey::Divide, Key::Divide), + (ControlKey::Equals, Key::Equals), + (ControlKey::NumpadEnter, Key::NumpadEnter), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::RWin, Key::RWin), + (ControlKey::RControl, Key::RightControl), + (ControlKey::RShift, Key::RightShift), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); + static ref NUMPAD_KEY_MAP: HashMap = + [ + (ControlKey::Home, true), + (ControlKey::UpArrow, true), + (ControlKey::PageUp, true), + (ControlKey::LeftArrow, true), + (ControlKey::RightArrow, true), + (ControlKey::End, true), + (ControlKey::DownArrow, true), + (ControlKey::PageDown, true), + (ControlKey::Insert, true), + (ControlKey::Delete, true), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); +} + +pub fn handle_key(evt: &KeyEvent) { + #[cfg(target_os = "macos")] + if !*IS_SERVER { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_key_(&evt)); + return; + } + handle_key_(evt); +} + +fn handle_key_(evt: &KeyEvent) { + if EXITING.load(Ordering::SeqCst) { + return; + } + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let mut en = ENIGO.lock().unwrap(); + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + #[cfg(windows)] + let mut disable_numlock = false; + #[cfg(target_os = "macos")] + en.reset_flag(); + #[cfg(not(target_os = "macos"))] + let mut to_release = Vec::new(); + #[cfg(not(target_os = "macos"))] + let mut has_cap = false; + #[cfg(windows)] + let mut has_numlock = false; + if evt.down { + let ck = if let Some(key_event::Union::control_key(ck)) = evt.union { + ck.value() + } else { + -1 + }; + fix_modifiers(&evt.modifiers[..], &mut en, ck); + for ref ck in evt.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + #[cfg(target_os = "macos")] + en.add_flag(key); + #[cfg(not(target_os = "macos"))] + { + if key == &Key::CapsLock { + has_cap = true; + } else if key == &Key::NumLock { + #[cfg(windows)] + { + has_numlock = true; + } + } else { + if !get_modifier_state(key.clone(), &mut en) { + en.key_down(key.clone()).ok(); + modifier_sleep(); + to_release.push(key); + } else { + KEYS_DOWN.lock().unwrap().insert(ck.value() as _, Instant::now()); + } + } + } + } + } + } + #[cfg(not(target_os = "macos"))] + if has_cap != en.get_key_state(Key::CapsLock) { + en.key_down(Key::CapsLock).ok(); + en.key_up(Key::CapsLock); + } + #[cfg(windows)] + if crate::common::valid_for_numlock(evt) { + if has_numlock != en.get_key_state(Key::NumLock) { + en.key_down(Key::NumLock).ok(); + en.key_up(Key::NumLock); + } + } + match evt.union { + Some(key_event::Union::control_key(ck)) => { + if let Some(key) = KEY_MAP.get(&ck.value()) { + #[cfg(windows)] + if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) { + disable_numlock = en.get_key_state(Key::NumLock); + if disable_numlock { + en.key_down(Key::NumLock).ok(); + en.key_up(Key::NumLock); + } + } + if evt.down { + allow_err!(en.key_down(key.clone())); + KEYS_DOWN + .lock() + .unwrap() + .insert(ck.value() as _, Instant::now()); + } else { + en.key_up(key.clone()); + KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); + } + } else if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + } else if ck.value() == ControlKey::LockScreen.value() { + crate::platform::lock_screen(); + super::video_service::switch_to_primary(); + } + } + Some(key_event::Union::chr(chr)) => { + if evt.down { + allow_err!(en.key_down(get_layout(chr))); + KEYS_DOWN + .lock() + .unwrap() + .insert(chr as u64 + KEY_CHAR_START, Instant::now()); + } else { + en.key_up(get_layout(chr)); + KEYS_DOWN + .lock() + .unwrap() + .remove(&(chr as u64 + KEY_CHAR_START)); + } + } + Some(key_event::Union::unicode(chr)) => { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } + } + Some(key_event::Union::seq(ref seq)) => { + en.key_sequence(&seq); + } + _ => {} + } + #[cfg(not(target_os = "macos"))] + for key in to_release { + en.key_up(key.clone()); + } + #[cfg(windows)] + if disable_numlock { + en.key_down(Key::NumLock).ok(); + en.key_up(Key::NumLock); + } +} + +#[tokio::main(flavor = "current_thread")] +async fn send_sas() -> ResultType<()> { + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; + Ok(()) +} diff --git a/rust-rdp/rust-desk/src/server/service.rs b/rust-rdp/rust-desk/src/server/service.rs new file mode 100644 index 0000000..adb659b --- /dev/null +++ b/rust-rdp/rust-desk/src/server/service.rs @@ -0,0 +1,264 @@ +use super::*; +use std::{ + collections::HashSet, + thread::{self, JoinHandle}, + time, +}; + +pub trait Service: Send + Sync { + fn name(&self) -> &'static str; + fn on_subscribe(&self, sub: ConnInner); + fn on_unsubscribe(&self, id: i32); + fn is_subed(&self, id: i32) -> bool; + fn join(&self); +} + +pub trait Subscriber: Default + Send + Sync + 'static { + fn id(&self) -> i32; + fn send(&mut self, msg: Arc); +} + +#[derive(Default)] +pub struct ServiceInner> { + name: &'static str, + handle: Option>, + subscribes: HashMap, + new_subscribes: HashMap, + active: bool, + need_snapshot: bool, +} + +pub trait Reset { + fn reset(&mut self); +} + +pub struct ServiceTmpl>(Arc>>); +pub struct ServiceSwap>(ServiceTmpl); +pub type GenericService = ServiceTmpl; +pub const HIBERNATE_TIMEOUT: u64 = 30; +pub const MAX_ERROR_TIMEOUT: u64 = 1_000; + +impl> ServiceInner { + fn send_new_subscribes(&mut self, msg: Arc) { + for s in self.new_subscribes.values_mut() { + s.send(msg.clone()); + } + } + + fn swap_new_subscribes(&mut self) { + for (_, s) in self.new_subscribes.drain() { + self.subscribes.insert(s.id(), s); + } + assert!(self.new_subscribes.is_empty()); + } + + #[inline] + fn has_subscribes(&self) -> bool { + self.subscribes.len() > 0 || self.new_subscribes.len() > 0 + } +} + +impl> Service for ServiceTmpl { + #[inline] + fn name(&self) -> &'static str { + self.0.read().unwrap().name + } + + fn is_subed(&self, id: i32) -> bool { + self.0.read().unwrap().subscribes.get(&id).is_some() + } + + fn on_subscribe(&self, sub: ConnInner) { + let mut lock = self.0.write().unwrap(); + if lock.subscribes.get(&sub.id()).is_some() { + return; + } + if lock.need_snapshot { + lock.new_subscribes.insert(sub.id(), sub.into()); + } else { + lock.subscribes.insert(sub.id(), sub.into()); + } + } + + fn on_unsubscribe(&self, id: i32) { + let mut lock = self.0.write().unwrap(); + if let None = lock.subscribes.remove(&id) { + lock.new_subscribes.remove(&id); + } + } + + fn join(&self) { + self.0.write().unwrap().active = false; + self.0.write().unwrap().handle.take().map(JoinHandle::join); + } +} + +impl> Clone for ServiceTmpl { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl> ServiceTmpl { + pub fn new(name: &'static str, need_snapshot: bool) -> Self { + Self(Arc::new(RwLock::new(ServiceInner:: { + name, + active: true, + need_snapshot, + ..Default::default() + }))) + } + + #[inline] + pub fn has_subscribes(&self) -> bool { + self.0.read().unwrap().has_subscribes() + } + + #[inline] + pub fn ok(&self) -> bool { + let lock = self.0.read().unwrap(); + lock.active && lock.has_subscribes() + } + + pub fn snapshot(&self, callback: F) -> ResultType<()> + where + F: FnMut(ServiceSwap) -> ResultType<()>, + { + if self.0.read().unwrap().new_subscribes.len() > 0 { + log::info!("Call snapshot of {} service", self.name()); + let mut callback = callback; + callback(ServiceSwap::(self.clone()))?; + } + Ok(()) + } + + #[inline] + pub fn send(&self, msg: Message) { + self.send_shared(Arc::new(msg)); + } + + pub fn send_shared(&self, msg: Arc) { + let mut lock = self.0.write().unwrap(); + for s in lock.subscribes.values_mut() { + s.send(msg.clone()); + } + } + + pub fn send_video_frame(&self, msg: Message) -> HashSet { + self.send_video_frame_shared(Arc::new(msg)) + } + + pub fn send_video_frame_shared(&self, msg: Arc) -> HashSet { + let mut conn_ids = HashSet::new(); + let mut lock = self.0.write().unwrap(); + for s in lock.subscribes.values_mut() { + s.send(msg.clone()); + conn_ids.insert(s.id()); + } + conn_ids + } + + pub fn send_without(&self, msg: Message, sub: i32) { + let mut lock = self.0.write().unwrap(); + let msg = Arc::new(msg); + for s in lock.subscribes.values_mut() { + if sub != s.id() { + s.send(msg.clone()); + } + } + } + + pub fn repeat(&self, interval_ms: u64, callback: F) + where + F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send, + S: 'static + Default + Reset, + { + let interval = time::Duration::from_millis(interval_ms); + let mut callback = callback; + let sp = self.clone(); + let thread = thread::spawn(move || { + let mut state = S::default(); + while sp.active() { + let now = time::Instant::now(); + if sp.has_subscribes() { + if let Err(err) = callback(sp.clone(), &mut state) { + log::error!("Error of {} service: {}", sp.name(), err); + thread::sleep(time::Duration::from_millis(MAX_ERROR_TIMEOUT)); + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + } + } else { + state.reset(); + } + let elapsed = now.elapsed(); + if elapsed < interval { + thread::sleep(interval - elapsed); + } + } + }); + self.0.write().unwrap().handle = Some(thread); + } + + pub fn run(&self, callback: F) + where + F: 'static + FnMut(Self) -> ResultType<()> + Send, + { + let sp = self.clone(); + let mut callback = callback; + let thread = thread::spawn(move || { + let mut error_timeout = HIBERNATE_TIMEOUT; + while sp.active() { + if sp.has_subscribes() { + log::debug!("Enter {} service inner loop", sp.name()); + let tm = time::Instant::now(); + if let Err(err) = callback(sp.clone()) { + log::error!("Error of {} service: {}", sp.name(), err); + if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) { + error_timeout = HIBERNATE_TIMEOUT; + } else { + error_timeout *= 2; + } + if error_timeout > MAX_ERROR_TIMEOUT { + error_timeout = MAX_ERROR_TIMEOUT; + } + thread::sleep(time::Duration::from_millis(error_timeout)); + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + } else { + log::debug!("Exit {} service inner loop", sp.name()); + } + } + thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT)); + } + }); + self.0.write().unwrap().handle = Some(thread); + } + + #[inline] + pub fn active(&self) -> bool { + self.0.read().unwrap().active + } +} + +impl> ServiceSwap { + #[inline] + pub fn send(&self, msg: Message) { + self.send_shared(Arc::new(msg)); + } + + #[inline] + pub fn send_shared(&self, msg: Arc) { + (self.0).0.write().unwrap().send_new_subscribes(msg); + } + + #[inline] + pub fn has_subscribes(&self) -> bool { + (self.0).0.read().unwrap().subscribes.len() > 0 + } +} + +impl> Drop for ServiceSwap { + fn drop(&mut self) { + (self.0).0.write().unwrap().swap_new_subscribes(); + } +} diff --git a/rust-rdp/rust-desk/src/server/video_service.rs b/rust-rdp/rust-desk/src/server/video_service.rs new file mode 100644 index 0000000..68faf17 --- /dev/null +++ b/rust-rdp/rust-desk/src/server/video_service.rs @@ -0,0 +1,532 @@ +// 24FPS (actually 23.976FPS) is what video professionals ages ago determined to be the +// slowest playback rate that still looks smooth enough to feel real. +// Our eyes can see a slight difference and even though 30FPS actually shows +// more information and is more realistic. +// 60FPS is commonly used in game, teamviewer 12 support this for video editing user. + +// how to capture with mouse cursor: +// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/desktop-dup-api?redirectedfrom=MSDN + +// RECORD: The following Project has implemented audio capture, hardware codec and mouse cursor drawn. +// https://github.com/PHZ76/DesktopSharing + +// dxgi memory leak issue +// https://stackoverflow.com/questions/47801238/memory-leak-in-creating-direct2d-device +// but per my test, it is more related to AcquireNextFrame, +// https://forums.developer.nvidia.com/t/dxgi-outputduplication-memory-leak-when-using-nv-but-not-amd-drivers/108582 + +// to-do: +// https://slhck.info/video/2017/03/01/rate-control.html + +use super::*; +use hbb_common::tokio::{ + runtime::Runtime, + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, +}; +use scrap::{Capturer, Config, Display, EncodeFrame, Encoder, VideoCodecId, STRIDE_ALIGN}; +use std::{ + collections::HashSet, + io::ErrorKind::WouldBlock, + time::{self, Duration, Instant}, +}; + +const WAIT_BASE: i32 = 17; +pub const NAME: &'static str = "video"; + +lazy_static::lazy_static! { + static ref CURRENT_DISPLAY: Arc> = Arc::new(Mutex::new(usize::MAX)); + static ref LAST_ACTIVE: Arc> = Arc::new(Mutex::new(Instant::now())); + static ref SWITCH: Arc> = Default::default(); + static ref TEST_LATENCIES: Arc>> = Default::default(); + static ref IMAGE_QUALITIES: Arc>> = Default::default(); + static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option)>, Arc)>>>) = { + let (tx, rx) = unbounded_channel(); + (tx, Arc::new(TokioMutex::new(rx))) + }; +} + +pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option) { + FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() +} + +struct VideoFrameController { + cur: Instant, + send_conn_ids: HashSet, + rt: Runtime, +} + +impl VideoFrameController { + fn new() -> Self { + Self { + cur: Instant::now(), + send_conn_ids: HashSet::new(), + rt: Runtime::new().unwrap(), + } + } + + fn reset(&mut self) { + self.send_conn_ids.clear(); + } + + fn set_send(&mut self, tm: Instant, conn_ids: HashSet) { + if !conn_ids.is_empty() { + self.cur = tm; + self.send_conn_ids = conn_ids; + } + } + + fn blocking_wait_next(&mut self, timeout_millis: u128) { + if self.send_conn_ids.is_empty() { + return; + } + + let send_conn_ids = self.send_conn_ids.clone(); + self.rt.block_on(async move { + let mut fetched_conn_ids = HashSet::new(); + let begin = Instant::now(); + while begin.elapsed().as_millis() < timeout_millis { + let timeout_dur = + Duration::from_millis((timeout_millis - begin.elapsed().as_millis()) as u64); + match tokio::time::timeout( + timeout_dur, + FRAME_FETCHED_NOTIFIER.1.lock().await.recv(), + ) + .await + { + Err(_) => { + // break if timeout + // log::error!("blocking wait frame receiving timeout {}", timeout_millis); + break; + } + Ok(Some((id, instant))) => { + if let Some(tm) = instant { + log::trace!("channel recv latency: {}", tm.elapsed().as_secs_f32()); + } + fetched_conn_ids.insert(id); + + // break if all connections have received current frame + if fetched_conn_ids.len() >= send_conn_ids.len() { + break; + } + } + Ok(None) => { + // this branch would nerver be reached + } + } + } + }); + } +} + +pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp.run(run); + sp +} + +fn check_display_changed( + last_n: usize, + last_current: usize, + last_width: usize, + last_hegiht: usize, +) -> bool { + let displays = match Display::all() { + Ok(d) => d, + _ => return false, + }; + + let n = displays.len(); + if n != last_n { + return true; + }; + + for (i, d) in displays.iter().enumerate() { + if d.is_primary() { + if i != last_current { + return true; + }; + if d.width() != last_width || d.height() != last_hegiht { + return true; + }; + } + } + + return false; +} + +fn run(sp: GenericService) -> ResultType<()> { + let fps = 30; + let spf = time::Duration::from_secs_f32(1. / (fps as f32)); + let (ndisplay, current, display) = get_current_display()?; + let (origin, width, height) = (display.origin(), display.width(), display.height()); + log::debug!( + "#displays={}, current={}, origin: {:?}, width={}, height={}", + ndisplay, + current, + &origin, + width, + height + ); + // Capturer object is expensive, avoiding to create it frequently. + let mut c = Capturer::new(display, true).with_context(|| "Failed to create capturer")?; + + let q = get_image_quality(); + let (bitrate, rc_min_quantizer, rc_max_quantizer, speed) = get_quality(width, height, q); + log::info!("bitrate={}, rc_min_quantizer={}", bitrate, rc_min_quantizer); + let mut wait = WAIT_BASE; + let cfg = Config { + width: width as _, + height: height as _, + timebase: [1, 1000], // Output timestamp precision + bitrate, + codec: VideoCodecId::VP9, + rc_min_quantizer, + rc_max_quantizer, + speed, + }; + let mut vpx; + match Encoder::new(&cfg, (num_cpus::get() / 2) as _) { + Ok(x) => vpx = x, + Err(err) => bail!("Failed to create encoder: {}", err), + } + + if *SWITCH.lock().unwrap() { + log::debug!("Broadcasting display switch"); + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display: current as _, + x: origin.0 as _, + y: origin.1 as _, + width: width as _, + height: height as _, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + *SWITCH.lock().unwrap() = false; + sp.send(msg_out); + } + + let mut frame_controller = VideoFrameController::new(); + + let mut crc = (0, 0); + let start = time::Instant::now(); + let mut last_check_displays = time::Instant::now(); + #[cfg(windows)] + let mut try_gdi = 1; + #[cfg(windows)] + log::info!("gdi: {}", c.is_gdi()); + while sp.ok() { + if *SWITCH.lock().unwrap() { + bail!("SWITCH"); + } + if current != *CURRENT_DISPLAY.lock().unwrap() { + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + if get_image_quality() != q { + bail!("SWITCH"); + } + #[cfg(windows)] + { + if crate::platform::windows::desktop_changed() { + bail!("Desktop changed"); + } + } + let now = time::Instant::now(); + if last_check_displays.elapsed().as_millis() > 1000 { + last_check_displays = now; + if ndisplay != get_display_num() { + log::info!("Displays changed"); + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + } + *LAST_ACTIVE.lock().unwrap() = now; + + frame_controller.reset(); + + match c.frame(wait as _) { + Ok(frame) => { + let time = now - start; + let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; + let send_conn_ids = handle_one_frame(&sp, &frame, ms, &mut crc, &mut vpx)?; + frame_controller.set_send(now, send_conn_ids); + #[cfg(windows)] + { + try_gdi = 0; + } + } + Err(ref e) if e.kind() == WouldBlock => { + // https://github.com/NVIDIA/video-sdk-samples/tree/master/nvEncDXGIOutputDuplicationSample + wait = WAIT_BASE - now.elapsed().as_millis() as i32; + if wait < 0 { + wait = 0 + } + #[cfg(windows)] + if try_gdi > 0 && !c.is_gdi() { + if try_gdi > 3 { + c.set_gdi(); + try_gdi = 0; + log::info!("No image, fall back to gdi"); + } + try_gdi += 1; + } + continue; + } + Err(err) => { + if check_display_changed(ndisplay, current, width, height) { + log::info!("Displays changed"); + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + + return Err(err.into()); + } + } + + // i love 3, 6, 8 + frame_controller.blocking_wait_next(3_000); + + let elapsed = now.elapsed(); + // may need to enable frame(timeout) + log::trace!("{:?} {:?}", time::Instant::now(), elapsed); + if elapsed < spf { + std::thread::sleep(spf - elapsed); + } + } + Ok(()) +} + +#[inline] +fn create_msg(vp9s: Vec) -> Message { + let mut msg_out = Message::new(); + let mut vf = VideoFrame::new(); + vf.set_vp9s(VP9s { + frames: vp9s.into(), + ..Default::default() + }); + msg_out.set_video_frame(vf); + msg_out +} + +#[inline] +fn create_frame(frame: &EncodeFrame) -> VP9 { + VP9 { + data: frame.data.to_vec(), + key: frame.key, + pts: frame.pts, + ..Default::default() + } +} + +#[inline] +fn handle_one_frame( + sp: &GenericService, + frame: &[u8], + ms: i64, + _crc: &mut (u32, u32), + vpx: &mut Encoder, +) -> ResultType> { + sp.snapshot(|sps| { + // so that new sub and old sub share the same encoder after switch + if sps.has_subscribes() { + bail!("SWITCH"); + } + Ok(()) + })?; + + /* + // crc runs faster on my i7-4790, around 0.5ms for 720p picture, + // but it is super slow on my Linux (in virtualbox) on the same machine, 720ms consumed. + // crc do save band width for static scenario (especially for gdi), + // Disable it since its uncertainty, who know what will happen on the other machines. + let mut hasher = crc32fast::Hasher::new(); + hasher.update(frame); + let checksum = hasher.finalize(); + if checksum != crc.0 { + crc.0 = checksum; + crc.1 = 0; + } else { + crc.1 += 1; + } + let encode = crc.1 <= 180 && crc.1 % 5 == 0; + */ + let encode = true; + + let mut send_conn_ids: HashSet = Default::default(); + if encode { + let mut frames = Vec::new(); + for ref frame in vpx + .encode(ms, frame, STRIDE_ALIGN) + .with_context(|| "Failed to encode")? + { + frames.push(create_frame(frame)); + } + for ref frame in vpx.flush().with_context(|| "Failed to flush")? { + frames.push(create_frame(frame)); + } + + // to-do: flush periodically, e.g. 1 second + if frames.len() > 0 { + send_conn_ids = sp.send_video_frame(create_msg(frames)); + } + } + Ok(send_conn_ids) +} + +fn get_display_num() -> usize { + if let Ok(d) = Display::all() { + d.len() + } else { + 0 + } +} + +pub fn get_displays() -> ResultType<(usize, Vec)> { + // switch to primary display if long time (30 seconds) no users + if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 { + *CURRENT_DISPLAY.lock().unwrap() = usize::MAX; + } + let mut displays = Vec::new(); + let mut primary = 0; + for (i, d) in Display::all()?.iter().enumerate() { + if d.is_primary() { + primary = i; + } + displays.push(DisplayInfo { + x: d.origin().0 as _, + y: d.origin().1 as _, + width: d.width() as _, + height: d.height() as _, + name: d.name(), + online: d.is_online(), + ..Default::default() + }); + } + let mut lock = CURRENT_DISPLAY.lock().unwrap(); + if *lock >= displays.len() { + *lock = primary + } + Ok((*lock, displays)) +} + +pub fn switch_display(i: i32) { + let i = i as usize; + if let Ok((_, displays)) = get_displays() { + if i < displays.len() { + *CURRENT_DISPLAY.lock().unwrap() = i; + } + } +} + +pub fn refresh() { + *SWITCH.lock().unwrap() = true; +} + +fn get_primary() -> usize { + if let Ok(all) = Display::all() { + for (i, d) in all.iter().enumerate() { + if d.is_primary() { + return i; + } + } + } + 0 +} + +pub fn switch_to_primary() { + switch_display(get_primary() as _); +} + +fn get_current_display() -> ResultType<(usize, usize, Display)> { + let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize; + let mut displays = Display::all()?; + if displays.len() == 0 { + bail!("No displays"); + } + let n = displays.len(); + if current >= n { + current = 0; + for (i, d) in displays.iter().enumerate() { + if d.is_primary() { + current = i; + break; + } + } + *CURRENT_DISPLAY.lock().unwrap() = current; + } + return Ok((n, current, displays.remove(current))); +} + +#[inline] +fn update_latency(id: i32, latency: i64, latencies: &mut HashMap) { + if latency <= 0 { + latencies.remove(&id); + } else { + latencies.insert(id, latency); + } +} + +pub fn update_test_latency(id: i32, latency: i64) { + update_latency(id, latency, &mut *TEST_LATENCIES.lock().unwrap()); +} + +fn convert_quality(q: i32) -> i32 { + let q = { + if q == ImageQuality::Balanced.value() { + (100 * 2 / 3, 12) + } else if q == ImageQuality::Low.value() { + (100 / 2, 18) + } else if q == ImageQuality::Best.value() { + (100, 12) + } else { + let bitrate = q >> 8 & 0xFF; + let quantizer = q & 0xFF; + (bitrate * 2, (100 - quantizer) * 36 / 100) + } + }; + if q.0 <= 0 { + 0 + } else { + q.0 << 8 | q.1 + } +} + +pub fn update_image_quality(id: i32, q: Option) { + match q { + Some(q) => { + let q = convert_quality(q); + if q > 0 { + IMAGE_QUALITIES.lock().unwrap().insert(id, q); + } else { + IMAGE_QUALITIES.lock().unwrap().remove(&id); + } + } + None => { + IMAGE_QUALITIES.lock().unwrap().remove(&id); + } + } +} + +fn get_image_quality() -> i32 { + IMAGE_QUALITIES + .lock() + .unwrap() + .values() + .min() + .unwrap_or(&convert_quality(ImageQuality::Balanced.value())) + .clone() +} + +#[inline] +fn get_quality(w: usize, h: usize, q: i32) -> (u32, u32, u32, i32) { + // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/ + let bitrate = q >> 8 & 0xFF; + let quantizer = q & 0xFF; + let b = ((w * h) / 1000) as u32; + (bitrate as u32 * b / 100, quantizer as _, 56, 7) +} diff --git a/rust-rdp/rust-desk/src/tray-icon.ico b/rust-rdp/rust-desk/src/tray-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4df961d5d91237b10de4a3e1b0e982b5573281be GIT binary patch literal 4286 zcmc(jJ7^U_6oy9vHZ~T)`Zhv9u(7qUu@khlvP)xg1tB07;wu5j=6*Yd{?e@IOSzFT00Jp9;jO?dHrG~H!5c--UL zj;;Fs(>%^o=hk!1i}(3{k*PtEXQLuF4i%gk<9fO&a&>?1K30;n$M3uQBI|3iX>TVA ztcRyJhc^EB*cQ2dFqcPs{=`X<)t_YyuHZvV9-rHKZ|%3p%$2y$)L5cv%--BB{CAu4 z>Q3(*#!r;>4tacfU5@|Uqxjs}k$ev1)XHLrSo}?FHSes3ml{P(>?(5R=HWaJaJELg z&+e}F(S&|ZD!oT<;y$la9n&u@jRL>6A8t+T#u4u6?T^JDk_`9s<| zi~n-0w|$Unq5b-nR(BR3y+hM}ZRgo*o*mZcg?3)IYx);?8`{&0=|0H0(1=}1?@>-R zhWN_qfwl%&Pd=w`;CX3lKQ;H1KR90(KrP$+llVIZJpzrb?s&L;G+le;fL@(^F-Cu- z$1xXNYK=VYWcr*|zd7f7r?zUT80wcfF+;CjdS}Rycy`(w&wS?DdQ~p;+yBK@9=!kA zcMR?47N5MytyjgVI$t|b-^rT%6s0wTUouW%)QEg)qSvc q^!w}YH{i$)XfSNPfzbgy>dc4Y``c@-*lp+fF7PjcZ8k959{UfehX0lT literal 0 HcmV?d00001 diff --git a/rust-rdp/rust-desk/src/ui.rs b/rust-rdp/rust-desk/src/ui.rs new file mode 100644 index 0000000..c59f012 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui.rs @@ -0,0 +1,825 @@ +mod cm; +#[cfg(feature = "inline")] +mod inline; +#[cfg(target_os = "macos")] +mod macos; +mod remote; +use crate::common::SOFTWARE_UPDATE_URL; +use crate::ipc; +use hbb_common::{ + allow_err, + config::{self, Config, Fav, PeerConfig, APP_NAME, ICON}, + log, sleep, + tokio::{self, time}, +}; +use sciter::Value; +use std::{ + collections::HashMap, + iter::FromIterator, + process::Child, + sync::{Arc, Mutex}, +}; + +pub type Childs = Arc)>>; + +lazy_static::lazy_static! { + // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ + static ref STUPID_VALUES: Mutex>>> = Default::default(); +} + +#[derive(Default)] +struct UI( + Childs, + Arc>, + Arc>>, +); + +struct UIHostHandler; + +pub fn start(args: &mut [String]) { + // https://github.com/c-smile/sciter-sdk/blob/master/include/sciter-x-types.h + // https://github.com/rustdesk/rustdesk/issues/132#issuecomment-886069737 + #[cfg(windows)] + allow_err!(sciter::set_options(sciter::RuntimeOptions::GfxLayer( + sciter::GFX_LAYER::WARP + ))); + #[cfg(windows)] + if args.len() > 0 && args[0] == "--tray" { + let mut res; + // while switching from prelogin to user screen, start_tray may fails, + // so we try more times + loop { + res = start_tray(); + if res.is_ok() { + log::info!("tray started with username {}", crate::username()); + break; + } + std::thread::sleep(std::time::Duration::from_secs(1)); + } + allow_err!(res); + return; + } + use sciter::SCRIPT_RUNTIME_FEATURES::*; + allow_err!(sciter::set_options(sciter::RuntimeOptions::ScriptFeatures( + ALLOW_FILE_IO as u8 | ALLOW_SOCKET_IO as u8 | ALLOW_EVAL as u8 | ALLOW_SYSINFO as u8 + ))); + let mut frame = sciter::WindowBuilder::main_window().create(); + #[cfg(windows)] + allow_err!(sciter::set_options(sciter::RuntimeOptions::UxTheming(true))); + frame.set_title(APP_NAME); + #[cfg(target_os = "macos")] + macos::make_menubar(); + let page; + if args.len() > 1 && args[0] == "--play" { + args[0] = "--connect".to_owned(); + let path: std::path::PathBuf = (&args[1]).into(); + let id = path + .file_stem() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + args[1] = id; + } + if args.is_empty() { + let childs: Childs = Default::default(); + let cloned = childs.clone(); + std::thread::spawn(move || check_zombie(cloned)); + crate::common::check_software_update(); + frame.event_handler(UI::new(childs)); + frame.sciter_handler(UIHostHandler {}); + page = "index.html"; + } else if args[0] == "--install" { + let childs: Childs = Default::default(); + frame.event_handler(UI::new(childs)); + frame.sciter_handler(UIHostHandler {}); + page = "install.html"; + } else if args[0] == "--cm" { + frame.register_behavior("connection-manager", move || { + Box::new(cm::ConnectionManager::new()) + }); + page = "cm.html"; + } else if (args[0] == "--connect" + || args[0] == "--file-transfer" + || args[0] == "--port-forward" + || args[0] == "--rdp") + && args.len() > 1 + { + let mut iter = args.iter(); + let cmd = iter.next().unwrap().clone(); + let id = iter.next().unwrap().clone(); + let args: Vec = iter.map(|x| x.clone()).collect(); + frame.set_title(&id); + frame.register_behavior("native-remote", move || { + Box::new(remote::Handler::new(cmd.clone(), id.clone(), args.clone())) + }); + page = "remote.html"; + } else { + log::error!("Wrong command: {:?}", args); + return; + } + #[cfg(feature = "inline")] + { + let html = if page == "index.html" { + inline::get_index() + } else if page == "cm.html" { + inline::get_cm() + } else if page == "install.html" { + inline::get_install() + } else { + inline::get_remote() + }; + frame.load_html(html.as_bytes(), Some(page)); + } + #[cfg(not(feature = "inline"))] + frame.load_file(&format!( + "file://{}/src/ui/{}", + std::env::current_dir() + .map(|c| c.display().to_string()) + .unwrap_or("".to_owned()), + page + )); + frame.run_app(); +} + +#[cfg(windows)] +fn start_tray() -> hbb_common::ResultType<()> { + let mut app = systray::Application::new()?; + let icon = include_bytes!("./tray-icon.ico"); + app.set_icon_from_buffer(icon, 32, 32).unwrap(); + app.add_menu_item("Open Window", |_| { + crate::run_me(Vec::<&str>::new()).ok(); + Ok::<_, systray::Error>(()) + })?; + let options = check_connect_status(false).1; + let idx_stopped = Arc::new(Mutex::new((0, 0))); + app.set_timer(std::time::Duration::from_millis(1000), move |app| { + let stopped = if let Some(v) = options.lock().unwrap().get("stop-service") { + !v.is_empty() + } else { + false + }; + let stopped = if stopped { 2 } else { 1 }; + let mut old = *idx_stopped.lock().unwrap(); + if stopped != old.1 { + if old.0 > 0 { + app.remove_menu_item(old.0) + } + if stopped == 1 { + old.0 = app.add_menu_item("Stop Service", |_| { + ipc::set_option("stop-service", "Y"); + Ok::<_, systray::Error>(()) + })?; + } else { + old.0 = app.add_menu_item("Start Service", |_| { + ipc::set_option("stop-service", ""); + Ok::<_, systray::Error>(()) + })?; + } + old.1 = stopped; + *idx_stopped.lock().unwrap() = old; + } + Ok::<_, systray::Error>(()) + })?; + allow_err!(app.wait_for_message()); + + Ok(()) +} + +impl UI { + fn new(childs: Childs) -> Self { + let res = check_connect_status(true); + Self(childs, res.0, res.1) + } + + fn recent_sessions_updated(&mut self) -> bool { + let mut lock = self.0.lock().unwrap(); + if lock.0 { + lock.0 = false; + true + } else { + false + } + } + + fn get_id(&mut self) -> String { + ipc::get_id() + } + + fn get_password(&mut self) -> String { + ipc::get_password() + } + + fn update_password(&mut self, password: String) { + if password.is_empty() { + allow_err!(ipc::set_password(Config::get_auto_password())); + } else { + allow_err!(ipc::set_password(password)); + } + } + + fn get_remote_id(&mut self) -> String { + Config::get_remote_id() + } + + fn set_remote_id(&mut self, id: String) { + Config::set_remote_id(&id); + } + + fn goto_install(&mut self) { + allow_err!(crate::run_me(vec!["--install"])); + } + + fn install_me(&mut self, _options: String) { + #[cfg(windows)] + std::thread::spawn(move || { + allow_err!(crate::platform::windows::install_me(&_options)); + std::process::exit(0); + }); + } + + fn update_me(&self, _path: String) { + #[cfg(target_os = "linux")] + { + std::process::Command::new("pkexec") + .args(&["apt", "install", "-f", &_path]) + .spawn() + .ok(); + std::fs::remove_file(&_path).ok(); + crate::run_me(Vec::<&str>::new()).ok(); + } + #[cfg(windows)] + { + let mut path = _path; + if path.is_empty() { + if let Ok(tmp) = std::env::current_exe() { + path = tmp.to_string_lossy().to_string(); + } + } + std::process::Command::new(path) + .arg("--update") + .spawn() + .ok(); + std::process::exit(0); + } + } + + fn get_option(&self, key: String) -> String { + if let Some(v) = self.2.lock().unwrap().get(&key) { + v.to_owned() + } else { + "".to_owned() + } + } + + fn get_local_option(&self, key: String) -> String { + Config::get_option(&key) + } + + fn peer_has_password(&self, id: String) -> bool { + !PeerConfig::load(&id).password.is_empty() + } + + fn forget_password(&self, id: String) { + let mut c = PeerConfig::load(&id); + c.password.clear(); + c.store(&id); + } + + fn get_peer_option(&self, id: String, name: String) -> String { + let c = PeerConfig::load(&id); + c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() + } + + fn set_peer_option(&self, id: String, name: String, value: String) { + let mut c = PeerConfig::load(&id); + if value.is_empty() { + c.options.remove(&name); + } else { + c.options.insert(name, value); + } + c.store(&id); + } + + fn get_options(&self) -> Value { + let mut m = Value::map(); + for (k, v) in self.2.lock().unwrap().iter() { + m.set_item(k, v); + } + m + } + + fn test_if_valid_server(&self, host: String) -> String { + hbb_common::socket_client::test_if_valid_server(&host) + } + + fn get_sound_inputs(&self) -> Value { + let mut a = Value::array(0); + #[cfg(windows)] + { + let inputs = Arc::new(Mutex::new(Vec::new())); + let cloned = inputs.clone(); + // can not call below in UI thread, because conflict with sciter sound com initialization + std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs()) + .join() + .ok(); + for name in inputs.lock().unwrap().drain(..) { + a.push(name); + } + } + #[cfg(not(windows))] + for name in get_sound_inputs() { + a.push(name); + } + a + } + + fn set_options(&self, v: Value) { + let mut m = HashMap::new(); + for (k, v) in v.items() { + if let Some(k) = k.as_string() { + if let Some(v) = v.as_string() { + if !v.is_empty() { + m.insert(k, v); + } + } + } + } + + *self.2.lock().unwrap() = m.clone(); + ipc::set_options(m).ok(); + } + + fn set_option(&self, key: String, value: String) { + let mut options = self.2.lock().unwrap(); + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + ipc::set_options(options.clone()).ok(); + + #[cfg(target_os = "macos")] + if &key == "stop-service" { + crate::platform::macos::launch(value != "Y"); + } + } + + fn install_path(&mut self) -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_info().1; + #[cfg(not(windows))] + return "".to_owned(); + } + + fn get_socks(&self) -> Value { + let s = ipc::get_socks(); + match s { + None => Value::null(), + Some(s) => { + let mut v = Value::array(0); + v.push(s.proxy); + v.push(s.username); + v.push(s.password); + v + } + } + } + + fn set_socks(&self, proxy: String, username: String, password: String) { + ipc::set_socks(config::Socks5Server { + proxy, + username, + password, + }) + .ok(); + } + + fn is_installed(&mut self) -> bool { + crate::platform::is_installed() + } + + fn is_installed_lower_version(&self) -> bool { + #[cfg(not(windows))] + return false; + #[cfg(windows)] + { + let installed_version = crate::platform::windows::get_installed_version(); + let a = hbb_common::get_version_number(crate::VERSION); + let b = hbb_common::get_version_number(&installed_version); + return a > b; + } + } + + fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { + crate::server::input_service::fix_key_down_timeout_at_exit(); + Config::set_size(x, y, w, h); + } + + fn get_size(&mut self) -> Value { + let s = Config::get_size(); + let mut v = Value::array(0); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + v + } + + fn get_connect_status(&mut self) -> Value { + let mut v = Value::array(0); + let x = *self.1.lock().unwrap(); + v.push(x.0); + v.push(x.1); + v + } + + #[inline] + fn get_peer_value(id: String, p: PeerConfig) -> Value { + let values = vec![ + id, + p.info.username.clone(), + p.info.hostname.clone(), + p.info.platform.clone(), + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ]; + Value::from_iter(values) + } + + fn get_peer(&self, id: String) -> Value { + let c = PeerConfig::load(&id); + Self::get_peer_value(id, c) + } + + fn get_fav(&self) -> Value { + Value::from_iter(Fav::load().peers) + } + + fn store_fav(&self, fav: Value) { + let mut tmp = vec![]; + fav.values().for_each(|v| { + if let Some(v) = v.as_string() { + if !v.is_empty() { + tmp.push(v); + } + } + }); + Fav::store(tmp); + } + + fn get_recent_sessions(&mut self) -> Value { + let peers: Vec = PeerConfig::peers() + .drain(..) + .map(|p| Self::get_peer_value(p.0, p.2)) + .collect(); + Value::from_iter(peers) + } + + fn get_icon(&mut self) -> String { + ICON.to_owned() + } + + fn remove_peer(&mut self, id: String) { + PeerConfig::remove(&id); + } + + fn new_remote(&mut self, id: String, remote_type: String) { + let mut lock = self.0.lock().unwrap(); + let args = vec![format!("--{}", remote_type), id.clone()]; + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } + } + + fn is_process_trusted(&mut self, _prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_process_trusted(_prompt); + #[cfg(not(target_os = "macos"))] + return true; + } + + fn is_can_screen_recording(&mut self, _prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_screen_recording(_prompt); + #[cfg(not(target_os = "macos"))] + return true; + } + + fn is_installed_daemon(&mut self, _prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_installed_daemon(_prompt); + #[cfg(not(target_os = "macos"))] + return true; + } + + fn get_error(&mut self) -> String { + #[cfg(target_os = "linux")] + { + let dtype = crate::platform::linux::get_display_server(); + if "wayland" == dtype { + return "".to_owned(); + } + if dtype != "x11" { + return format!("Unsupported display server type {}, x11 expected!", dtype); + } + } + return "".to_owned(); + } + + fn is_login_wayland(&mut self) -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::is_login_wayland(); + #[cfg(not(target_os = "linux"))] + return false; + } + + fn fix_login_wayland(&mut self) { + #[cfg(target_os = "linux")] + return crate::platform::linux::fix_login_wayland(); + } + + fn current_is_wayland(&mut self) -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::current_is_wayland(); + #[cfg(not(target_os = "linux"))] + return false; + } + + fn modify_default_login(&mut self) -> String { + #[cfg(target_os = "linux")] + return crate::platform::linux::modify_default_login(); + #[cfg(not(target_os = "linux"))] + return "".to_owned(); + } + + fn get_software_update_url(&self) -> String { + SOFTWARE_UPDATE_URL.lock().unwrap().clone() + } + + fn get_new_version(&self) -> String { + hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) + } + + fn get_version(&self) -> String { + crate::VERSION.to_owned() + } + + fn get_app_name(&self) -> String { + APP_NAME.to_owned() + } + + fn get_software_ext(&self) -> String { + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() + } + + fn get_software_store_path(&self) -> String { + let mut p = std::env::temp_dir(); + let name = SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(APP_NAME.to_owned()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) + } + + fn create_shortcut(&self, _id: String) { + #[cfg(windows)] + crate::platform::windows::create_shortcut(&_id).ok(); + } + + fn discover(&self) { + std::thread::spawn(move || { + allow_err!(crate::rendezvous_mediator::discover()); + }); + } + + fn get_lan_peers(&self) -> String { + config::LanPeers::load().peers + } + + fn open_url(&self, url: String) { + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = "xdg-open"; + allow_err!(std::process::Command::new(p).arg(url).spawn()); + } + + fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + fn is_xfce(&self) -> bool { + crate::platform::is_xfce() + } +} + +impl sciter::EventHandler for UI { + sciter::dispatch_script_call! { + fn t(String); + fn is_xfce(); + fn get_id(); + fn get_password(); + fn update_password(String); + fn get_remote_id(); + fn set_remote_id(String); + fn save_size(i32, i32, i32, i32); + fn get_size(); + fn new_remote(String, bool); + fn remove_peer(String); + fn get_connect_status(); + fn get_recent_sessions(); + fn get_peer(String); + fn get_fav(); + fn store_fav(Value); + fn recent_sessions_updated(); + fn get_icon(); + fn install_me(String); + fn is_installed(); + fn set_socks(String, String, String); + fn get_socks(); + fn is_installed_lower_version(); + fn install_path(); + fn goto_install(); + fn is_process_trusted(bool); + fn is_can_screen_recording(bool); + fn is_installed_daemon(bool); + fn get_error(); + fn is_login_wayland(); + fn fix_login_wayland(); + fn current_is_wayland(); + fn modify_default_login(); + fn get_options(); + fn get_option(String); + fn get_local_option(String); + fn get_peer_option(String, String); + fn peer_has_password(String); + fn forget_password(String); + fn set_peer_option(String, String, String); + fn test_if_valid_server(String); + fn get_sound_inputs(); + fn set_options(Value); + fn set_option(String, String); + fn get_software_update_url(); + fn get_new_version(); + fn get_version(); + fn update_me(String); + fn get_app_name(); + fn get_software_store_path(); + fn get_software_ext(); + fn open_url(String); + fn create_shortcut(String); + fn discover(); + fn get_lan_peers(); + } +} + +impl sciter::host::HostHandler for UIHostHandler { + fn on_graphics_critical_failure(&mut self) { + log::error!("Critical rendering error: e.g. DirectX gfx driver error. Most probably bad gfx drivers."); + } +} + +pub fn check_zombie(childs: Childs) { + let mut deads = Vec::new(); + loop { + let mut lock = childs.lock().unwrap(); + let mut n = 0; + for (id, c) in lock.1.iter_mut() { + if let Ok(Some(_)) = c.try_wait() { + deads.push(id.clone()); + n += 1; + } + } + for ref id in deads.drain(..) { + lock.1.remove(id); + } + if n > 0 { + lock.0 = true; + } + drop(lock); + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +// notice: avoiding create ipc connection repeatedly, +// because windows named pipe has serious memory leak issue. +#[tokio::main(flavor = "current_thread")] +async fn check_connect_status_( + reconnect: bool, + status: Arc>, + options: Arc>>, +) { + let mut key_confirmed = false; + loop { + if let Ok(mut c) = ipc::connect(1000, "").await { + let mut timer = time::interval(time::Duration::from_secs(1)); + loop { + tokio::select! { + res = c.next() => { + match res { + Err(err) => { + log::error!("ipc connection closed: {}", err); + break; + } + Ok(Some(ipc::Data::Options(Some(v)))) => { + *options.lock().unwrap() = v + } + Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { + if x > 0 { + x = 1 + } + key_confirmed = c; + *status.lock().unwrap() = (x as _, key_confirmed); + } + _ => {} + } + } + _ = timer.tick() => { + c.send(&ipc::Data::OnlineStatus(None)).await.ok(); + c.send(&ipc::Data::Options(None)).await.ok(); + } + } + } + } + if !reconnect { + std::process::exit(0); + } + *status.lock().unwrap() = (-1, key_confirmed); + sleep(1.).await; + } +} + +#[cfg(not(target_os = "linux"))] +fn get_sound_inputs() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out +} + +#[cfg(target_os = "linux")] +fn get_sound_inputs() -> Vec { + crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect() +} + +fn check_connect_status( + reconnect: bool, +) -> (Arc>, Arc>>) { + let status = Arc::new(Mutex::new((0, false))); + let options = Arc::new(Mutex::new(HashMap::new())); + let cloned = status.clone(); + let cloned_options = options.clone(); + std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options)); + (status, options) +} + +// sacrifice some memory +pub fn value_crash_workaround(values: &[Value]) -> Arc> { + let persist = Arc::new(values.to_vec()); + STUPID_VALUES.lock().unwrap().push(persist.clone()); + persist +} diff --git a/rust-rdp/rust-desk/src/ui/ab.tis b/rust-rdp/rust-desk/src/ui/ab.tis new file mode 100644 index 0000000..d43476a --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/ab.tis @@ -0,0 +1,350 @@ +var svg_tile = ; +var svg_list = ; +var search_icon = ; +var clear_icon = ; + +function getSessionsStyleOption(type) { + return (type || "recent") + "-sessions-style"; +} + +function getSessionsStyle(type) { + var v = handler.get_local_option(getSessionsStyleOption(type)); + if (!v) v = type == "ab" ? "list" : "tile"; + return v; +} + +var searchPatterns = {}; + +class SearchBar: Reactor.Component { + this var type = ""; + + function this(params) { + this.type = (params || {}).type || ""; + } + + function render() { + var value = searchPatterns[this.type] || ""; + var me = this; + self.timer(1ms, function() { (me.search_id || {}).value = value; }); + return

+ {search_icon} + + {value && {clear_icon}} +
; + } + + event click $(span.clear-input) { + this.onChange(''); + } + + event change $(input) (_, el) { + this.onChange(el.value.trim()); + } + + function onChange(v) { + searchPatterns[this.type] = v; + app.multipleSessions.update(); + } +} + +class SessionStyle: Reactor.Component { + this var type = ""; + + function this(params) { + this.type = (params || {}).type || ""; + } + + function render() { + var sessionsStyle = getSessionsStyle(this.type); + return
+ {svg_tile} + {svg_list} +
; + } + + event click $(span.inactive) { + var option = getSessionsStyleOption(this.type); + var sessionsStyle = getSessionsStyle(this.type); + handler.set_option(option, sessionsStyle == "tile" ? "list" : "tile"); + app.multipleSessions.update(); + } +} + +class SessionList: Reactor.Component { + this var sessions = []; + this var type = ""; + this var style; + + function this(params) { + this.sessions = params.sessions; + this.type = params.type || ""; + this.style = getSessionsStyle(this.type); + } + + function getSessions() { + var p = searchPatterns[this.type]; + if (!p) return this.sessions; + var tmp = []; + this.sessions.map(function(s) { + var name = s[4] || s.alias || s[0] || s.id || ""; + if (name.indexOf(p) >= 0) tmp.push(s); + }); + return tmp; + } + + function render() { + var sessions = this.getSessions(); + if (sessions.length == 0) { + return
{translate("Empty")}
; + } + var me = this; + sessions = sessions.map(function(x) { return me.getSession(x); }); + return
+ + +
  • {translate('Connect')}
  • +
  • {translate('Transfer File')}
  • +
  • {translate('TCP Tunneling')}
  • +
  • RDP
  • +
    +
  • {translate('Rename')}
  • + {this.type != "fav" && this.type != "lan" &&
  • {translate('Remove')}
  • } + {is_win &&
  • {translate('Create Desktop Shortcut')}
  • } +
  • {translate('Unremember Password')}
  • + {(!this.type || this.type == "fav") &&
  • {translate('Add to Favorites')}
  • } + {(!this.type || this.type == "fav") &&
  • {translate('Remove from Favorites')}
  • } + + + {sessions} +
    ; + } + + function getSession(s) { + var id = s[0] || s.id || ""; + var username = s[1] || s.username || ""; + var hostname = s[2] || s.hostname || ""; + var platform = s[3] || s.platform || ""; + var alias = s[4] || s.alias || ""; + if (this.style == "list") { + return
    +
    + {platform && platformSvg(platform, "white")} +
    +
    +
    +
    {alias ? alias : formatId(id)}
    +
    {username}@{hostname}
    +
    +
    +
    + {svg_menu} +
    +
    ; + } + return
    +
    + {platform && platformSvg(platform, "white")} +
    {username}@{hostname}
    +
    +
    +
    {alias ? alias : formatId(id)}
    + {svg_menu} +
    +
    ; + } + + event dblclick $(div.remote-session-link) (evt, me) { + createNewConnect(me.id, "connect"); + } + + event click $(#menu) (_, me) { + var id = me.parent.parent.id; + var platform = me.parent.parent.attributes["platform"]; + this.$(#rdp).style.set{ + display: (platform == "Windows" && is_win) ? "block" : "none", + }; + this.$(#forget-password).style.set{ + display: handler.peer_has_password(id) ? "block" : "none", + }; + if (!this.type || this.type == "fav") { + var in_fav = handler.get_fav().indexOf(id) >= 0; + this.$(#add-fav).style.set{ + display: in_fav ? "none" : "block", + }; + this.$(#remove-fav).style.set{ + display: in_fav ? "block" : "none", + }; + } + // https://sciter.com/forums/topic/replacecustomize-context-menu/ + var menu = this.$(menu#remote-context); + menu.attributes["remote-id"] = id; + var conn = this.$(menu #connect); + if (conn) { + var alias = me.parent.parent.$(#alias); + if (alias) { + alias = alias.text.replace(' ', ''); + if (alias != id) { + conn.text = translate('Connect') + ' ' + id; + } else { + conn.text = translate('Connect'); + } + } + } + me.popup(menu); + } + + event click $(menu#remote-context li) (evt, me) { + var action = me.id; + var id = me.parent.attributes["remote-id"]; + if (action == "connect") { + createNewConnect(id, "connect"); + } else if (action == "transfer") { + createNewConnect(id, "file-transfer"); + } else if (action == "remove") { + if (!this.type) { + handler.remove_peer(id); + app.update(); + } + } else if (action == "forget-password") { + handler.forget_password(id); + } else if (action == "shortcut") { + handler.create_shortcut(id); + } else if (action == "rdp") { + createNewConnect(id, "rdp"); + } else if (action == "add-fav") { + var favs = handler.get_fav(); + if (favs.indexOf(id) < 0) { + favs = [id].concat(favs); + handler.store_fav(favs); + } + app.multipleSessions.update(); + app.update(); + } else if (action == "remove-fav") { + var favs = handler.get_fav(); + var i = favs.indexOf(id); + favs.splice(i, 1); + handler.store_fav(favs); + app.multipleSessions.update(); + } else if (action == "tunnel") { + createNewConnect(id, "port-forward"); + } else if (action == "rename") { + var old_name = handler.get_peer_option(id, "alias"); + msgbox("custom-rename", "Rename", "
    \ +
    \ +
    \ + ", function(res=null) { + if (!res) return; + var name = (res.name || "").trim(); + if (name != old_name) { + handler.set_peer_option(id, "alias", name); + } + app.update(); + }); + } + } +} + +function getSessionsType() { + return handler.get_local_option("show-sessions-type"); +} + +class Favorites: Reactor.Component { + function render() { + var sessions = handler.get_fav().map(function(f) { + return handler.get_peer(f); + }); + return ; + } +} + +class MultipleSessions: Reactor.Component { + function render() { + var type = getSessionsType(); + return
    +
    +
    + {translate('Recent Sessions')} + {translate('Favorites')} + {handler.is_installed() && {translate('Discovered')}} +
    + {!this.hidden && } + {!this.hidden && } +
    + {!this.hidden && + ((type == "fav" && ) || + (type == "lan" && handler.is_installed() && ) || + )} +
    ; + } + + function stupidUpdate() { + /* hidden is workaround of stupid sciter bug */ + this.hidden = true; + this.update(); + var me = this; + self.timer(60ms, function() { + me.hidden = false; + me.update(); + self.timer(30ms, function() { me.onSize(); }); + }); + } + + event click $(div#sessions-type span.inactive) (_, el) { + if (el.id == "lan") { + discover(); + } + handler.set_option('show-sessions-type', el.id || ""); + this.stupidUpdate(); + } + + function onSize() { + var w = this.$(.sessions-bar .sessions-tab).box(#width); + var len = translate('Recent Sessions').length; + var totalChars = 0; + var nEle = 0; + for (var el in this.$$(#sessions-type span)) { + nEle += 1; + totalChars += el.text.length; + } + for (var el in this.$$(#sessions-type span)) { + var maxWidth = (w - nEle * 2 * 8) * el.text.length / totalChars; + if (maxWidth < 0) maxWidth = 36; + el.style.set{ + "max-width": maxWidth + "px", + }; + } + } +} + +function discover() { + handler.discover(); + var tries = 15; + function update() { + self.timer(300ms, function() { + tries -= 1; + if (tries == 0) return; + update(); + var p = (app || {}).multipleSessions; + if (p) { + p.update(); + } + }); + } + update(); +} + +if (getSessionsType() == "lan" && handler.is_installed()) { + discover(); +} + +class LanPeers: Reactor.Component { + function render() { + var sessions = []; + try { + sessions = JSON.parse(handler.get_lan_peers()); + } catch (_) {} + return ; + } +} + +view.on("size", function() { if (app && app.multipleSessions) app.multipleSessions.onSize(); }); diff --git a/rust-rdp/rust-desk/src/ui/chatbox.html b/rust-rdp/rust-desk/src/ui/chatbox.html new file mode 100644 index 0000000..86a3354 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/chatbox.html @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/rust-rdp/rust-desk/src/ui/cm.css b/rust-rdp/rust-desk/src/ui/cm.css new file mode 100644 index 0000000..59a7450 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/cm.css @@ -0,0 +1,233 @@ +body { + behavior: connection-manager; +} + +div.content { + flow: horizontal; + size: *; +} + +div.left-panel { + size: *; + padding: 1em; + border-spacing: 1em; + overflow-x: scroll-indicator; + position: relative; +} + +div.chaticon svg { + size: 24px; + margin: 4px; + opacity: 0.66; +} + +div.chaticon { + position: absolute; + right: 0; + top: 0; + size: 32px; + background-color: color(gray-bg); +} + +div.chaticon:hover svg { + opacity: 1; +} + +div.chaticon:active { + background: white; +} + +div.right-panel { + background: white; + border-left: color(border) 1px solid; + size: *; +} + +div.icon-and-id { + flow: horizontal; + border-spacing: 1em; +} + +div.icon { + size: 96px; + text-align: center; + font-size: 96px; + line-height: 96px; + color: white; + font-weight: bold; +} + +div.id { + @ELLIPSIS; + color: color(green-blue); +} + +div.permissions { + flow: horizontal; + border-spacing: 0.5em; +} + +div.permissions > div { + size: 48px; + background: color(accent); +} + +div.permissions icon { + margin: *; + size: 32px; + background-size: cover; + background-repeat: no-repeat; + display: block; +} + +div.permissions > div.disabled { + background: #ddd; +} + +div.permissions > div:active { + opacity: 0.5; +} + +icon.keyboard { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII='); +} + +icon.clipboard { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='); +} + +icon.audio { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='); +} + +div.buttons { + width: *; + border-spacing: 0.5em; + text-align: center; +} + +div.buttons button { + width: 80px; + height: 40px; + margin: 0.5em; +} + +button#disconnect { + width: 160px; + background: color(blood-red); + border: none; +} + +button#disconnect:active { + opacity: 0.5; +} + +@media platform != "OSX" { +header .window-toolbar { + left: 40px; + top: 8px; +} +} + +@media platform == "OSX" { +header .tabs-wrapper { + margin-left: 80px; + margin-top: 8px; +} +} + +div.tabs-wrapper { + size: *; + position: relative; + overflow: hidden; +} + +div.tabs { + size: *; + flow: horizontal; + white-space: nowrap; + overflow: hidden; +} + +header { + height: 32px; + border-bottom: none; +} + +div.border-bottom { + position: absolute; + bottom: 0; + left: 0; + width: *; + height: 1px; + background: color(border) 1px solid; +} + +header div.window-icon { + size: 32px; +} + +div.tabs > div { + display: inline-block; + height: 24px; + line-height: 24px; +} + +div.tab { + width: 70px; + @ELLIPSIS; + text-align: center; + position: relative; + padding: 0 5px; +} + +div.active-tab { + background: color(gray-bg); + border: color(border) 1px solid; + border-bottom: none; + font-weight: bold; +} + +span.unreaded { + position: absolute; + font-size: 11px; + size: 15px; + border-radius: 15px; + line-height: 15px; + background: color(blood-red); + display: inline-block; + color: white; +} + +div.left-panel { + background: color(gray-bg); +} + +button.window#minimize { + right: 0px!important; +} + +div.tab-arrows { + position: absolute; + right: 2px; + font-weight: bold; + background: white; +} + +div.tab-arrows span { + display: inline-block; + height: *; + margin: 0; + padding: 6px 2px; + line-height: 20px; + opacity: 0.66; +} + +div.tab-arrows span:hover { + opacity: 1; +} + +div.tab-arrows span:active { + opacity: 1; + background-color: #ddd; +} \ No newline at end of file diff --git a/rust-rdp/rust-desk/src/ui/cm.html b/rust-rdp/rust-desk/src/ui/cm.html new file mode 100644 index 0000000..4edb4a7 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/cm.html @@ -0,0 +1,21 @@ + + + + + +
    +
    + + + + + + + + \ No newline at end of file diff --git a/rust-rdp/rust-desk/src/ui/cm.rs b/rust-rdp/rust-desk/src/ui/cm.rs new file mode 100644 index 0000000..32f7fc6 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/cm.rs @@ -0,0 +1,460 @@ +use crate::ipc::{self, new_listener, Connection, Data}; +use hbb_common::{ + allow_err, + config::{Config, ICON}, + fs, log, + message_proto::*, + protobuf::Message as _, + tokio::{self, sync::mpsc, task::spawn_blocking}, +}; +use sciter::{make_args, Element, Value, HELEMENT}; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, RwLock}, +}; + +pub struct ConnectionManagerInner { + root: Option, + senders: HashMap>, +} + +#[derive(Clone)] +pub struct ConnectionManager(Arc>); + +impl Deref for ConnectionManager { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ConnectionManager { + pub fn new() -> Self { + #[cfg(target_os = "linux")] + std::thread::spawn(start_pa); + let inner = ConnectionManagerInner { + root: None, + senders: HashMap::new(), + }; + let cm = Self(Arc::new(RwLock::new(inner))); + #[cfg(target_os = "macos")] + { + let cloned = cm.clone(); + *super::macos::SHOULD_OPEN_UNTITLED_FILE_CALLBACK + .lock() + .unwrap() = Some(Box::new(move || { + cloned.call("awake", &make_args!()); + })); + } + let cloned = cm.clone(); + std::thread::spawn(move || start_ipc(cloned)); + cm + } + + fn get_icon(&mut self) -> String { + ICON.to_owned() + } + + #[inline] + fn call(&self, func: &str, args: &[Value]) { + let r = self.read().unwrap(); + if let Some(ref e) = r.root { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } + } + + fn add_connection( + &self, + id: i32, + is_file_transfer: bool, + port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + tx: mpsc::UnboundedSender, + ) { + self.call( + "addConnection", + &make_args!( + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio + ), + ); + self.write().unwrap().senders.insert(id, tx); + } + + fn remove_connection(&self, id: i32) { + self.write().unwrap().senders.remove(&id); + self.call("removeConnection", &make_args!(id)); + } + + async fn handle_data( + &self, + id: i32, + data: Data, + write_jobs: &mut Vec, + conn: &mut Connection, + ) { + match data { + Data::ChatMessage { text } => { + self.call("newMessage", &make_args!(id, text)); + } + Data::FS(v) => match v { + ipc::FS::ReadDir { + dir, + include_hidden, + } => { + Self::read_dir(&dir, include_hidden, conn).await; + } + ipc::FS::RemoveDir { + path, + id, + recursive, + } => { + Self::remove_dir(path, id, recursive, conn).await; + } + ipc::FS::RemoveFile { path, id, file_num } => { + Self::remove_file(path, id, file_num, conn).await; + } + ipc::FS::CreateDir { path, id } => { + Self::create_dir(path, id, conn).await; + } + ipc::FS::NewWrite { + path, + id, + mut files, + } => { + write_jobs.push(fs::TransferJob::new_write( + id, + path, + files + .drain(..) + .map(|f| FileEntry { + name: f.0, + modified_time: f.1, + ..Default::default() + }) + .collect(), + )); + } + ipc::FS::CancelWrite { id } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.remove_download_file(); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteDone { id, file_num } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.modify_time(); + Self::send(fs::new_done(id, file_num), conn).await; + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteBlock { + id, + file_num, + data, + compressed, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + if let Err(err) = job + .write(FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }) + .await + { + Self::send(fs::new_error(id, err, file_num), conn).await; + } + } + } + }, + _ => {} + } + } + + async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) { + let path = { + if dir.is_empty() { + Config::get_home() + } else { + fs::get_path(dir) + } + }; + if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_dir(fd); + msg_out.set_file_response(file_response); + Self::send(msg_out, conn).await; + } + } + + async fn handle_result( + res: std::result::Result, S>, + id: i32, + file_num: i32, + conn: &mut Connection, + ) { + match res { + Err(err) => { + Self::send(fs::new_error(id, err, file_num), conn).await; + } + Ok(Err(err)) => { + Self::send(fs::new_error(id, err, file_num), conn).await; + } + Ok(Ok(())) => { + Self::send(fs::new_done(id, file_num), conn).await; + } + } + } + + async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) { + Self::handle_result( + spawn_blocking(move || fs::remove_file(&path)).await, + id, + file_num, + conn, + ) + .await; + } + + async fn create_dir(path: String, id: i32, conn: &mut Connection) { + Self::handle_result( + spawn_blocking(move || fs::create_dir(&path)).await, + id, + 0, + conn, + ) + .await; + } + + async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) { + let path = fs::get_path(&path); + Self::handle_result( + spawn_blocking(move || { + if recursive { + fs::remove_all_empty_dir(&path) + } else { + std::fs::remove_dir(&path).map_err(|err| err.into()) + } + }) + .await, + id, + 0, + conn, + ) + .await; + } + + async fn send(msg: Message, conn: &mut Connection) { + match msg.write_to_bytes() { + Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await), + err => allow_err!(err), + } + } + + fn switch_permission(&self, id: i32, name: String, enabled: bool) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::SwitchPermission { name, enabled })); + } + } + + fn close(&self, id: i32) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::Close)); + } + } + + fn send_msg(&self, id: i32, text: String) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::ChatMessage { text })); + } + } + + fn authorize(&self, id: i32) { + let lock = self.read().unwrap(); + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(Data::Authorize)); + } + } + + fn exit(&self) { + std::process::exit(0); + } + + fn t(&self, name: String) -> String { + crate::client::translate(name) + } +} + +impl sciter::EventHandler for ConnectionManager { + fn attached(&mut self, root: HELEMENT) { + self.write().unwrap().root = Some(Element::from(root)); + } + + sciter::dispatch_script_call! { + fn t(String); + fn get_icon(); + fn close(i32); + fn authorize(i32); + fn switch_permission(i32, String, bool); + fn send_msg(i32, String); + fn exit(); + } +} + +#[tokio::main(flavor = "current_thread")] +async fn start_ipc(cm: ConnectionManager) { + match new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let cm = cm.clone(); + tokio::spawn(async move { + let mut conn_id: i32 = 0; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut write_jobs: Vec = Vec::new(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio} => { + conn_id = id; + cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, tx.clone()); + } + Data::Close => { + log::info!("cm ipc connection closed from connection request"); + break; + } + _ => { + cm.handle_data(conn_id, data, &mut write_jobs, &mut stream).await; + } + } + } + _ => {} + } + } + Some(data) = rx.recv() => { + allow_err!(stream.send(&data).await); + } + } + } + cm.remove_connection(conn_id); + }); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + std::process::exit(-1); +} + +#[cfg(target_os = "linux")] +#[tokio::main(flavor = "current_thread")] +async fn start_pa() { + use hbb_common::config::APP_NAME; + use libpulse_binding as pulse; + use libpulse_simple_binding as psimple; + match new_listener("_pa").await { + Ok(mut incoming) => { + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let mut device: String = "".to_owned(); + if let Some(Ok(Some(Data::Config((_, Some(x)))))) = + stream.next_timeout2(1000).await + { + device = x; + } + if device == "Mute" { + break; + } + if !device.is_empty() { + device = crate::platform::linux::get_pa_source_name(&device); + } + if device.is_empty() { + device = crate::platform::linux::get_pa_monitor(); + } + if device.is_empty() { + break; + } + let spec = pulse::sample::Spec { + format: pulse::sample::Format::F32le, + channels: 2, + rate: crate::platform::linux::PA_SAMPLE_RATE, + }; + log::info!("pa monitor: {:?}", device); + // systemctl --user status pulseaudio.service + let mut buf: Vec = vec![0; 480 * 4]; + match psimple::Simple::new( + None, // Use the default server + APP_NAME, // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + APP_NAME, // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) { + Ok(s) => loop { + if let Some(Err(_)) = stream.next_timeout2(1).await { + break; + } + if let Ok(_) = s.read(&mut buf) { + allow_err!( + stream.send(&Data::RawMessage(buf.clone())).await + ); + } + }, + Err(err) => { + log::error!("Could not create simple pulse: {}", err); + } + } + } + Err(err) => { + log::error!("Couldn't get pa client: {:?}", err); + } + } + } + } + } + Err(err) => { + log::error!("Failed to start pa ipc server: {}", err); + } + } +} diff --git a/rust-rdp/rust-desk/src/ui/cm.tis b/rust-rdp/rust-desk/src/ui/cm.tis new file mode 100644 index 0000000..8fdb3f1 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/cm.tis @@ -0,0 +1,402 @@ +view.windowFrame = is_osx ? #extended : #solid; + +var body; +var connections = []; +var show_chat = false; + +class Body: Reactor.Component +{ + this var cur = 0; + + function this() { + body = this; + } + + function render() { + if (connections.length == 0) return
    ; + var c = connections[this.cur]; + this.connection = c; + this.cid = c.id; + var auth = c.authorized; + var me = this; + var callback = function(msg) { + me.sendMsg(msg); + }; + var right_style = show_chat ? "" : "display: none"; + return
    +
    +
    +
    + {c.name[0].toUpperCase()} +
    +
    +
    {c.name}
    +
    ({c.peer_id})
    +
    {translate('Connected')} {" "} {getElaspsed(c.time)}
    +
    +
    +
    + {c.is_file_transfer || c.port_forward ? "" :
    {translate('Permissions')}
    } + {c.is_file_transfer || c.port_forward ? "" :
    +
    +
    +
    +
    } + {c.port_forward ?
    Port Forwarding: {c.port_forward}
    : ""} +
    +
    + {auth ? "" : } + {auth ? "" : } + {auth ? : ""} +
    + {c.is_file_transfer || c.port_forward ? "" :
    {svg_chat}
    } +
    +
    + {c.is_file_transfer || c.port_forward ? "" : } +
    +
    ; + } + + function sendMsg(text) { + if (!text) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.msgs.push({ name: "me", text: text, time: getNowStr()}); + handler.send_msg(cid, text); + body.update(); + }); + } + + event click $(icon.keyboard) (e) { + var { cid, connection } = this; + checkClickTime(function() { + connection.keyboard = !connection.keyboard; + body.update(); + handler.switch_permission(cid, "keyboard", connection.keyboard); + }); + } + + event click $(icon.clipboard) { + var { cid, connection } = this; + checkClickTime(function() { + connection.clipboard = !connection.clipboard; + body.update(); + handler.switch_permission(cid, "clipboard", connection.clipboard); + }); + } + + event click $(icon.audio) { + var { cid, connection } = this; + checkClickTime(function() { + connection.audio = !connection.audio; + body.update(); + handler.switch_permission(cid, "audio", connection.audio); + }); + } + + event click $(button#accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + body.update(); + handler.authorize(cid); + self.timer(30ms, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + }); + } + + event click $(button#dismiss) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } + + event click $(button#disconnect) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } +} + +$(body).content(); + +var header; + +class Header: Reactor.Component +{ + function this() { + header = this; + } + + function render() { + var me = this; + var conn = connections[body.cur]; + if (conn && conn.unreaded > 0) {; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "inline-block", + }; + self.timer(300ms, function() { + conn.unreaded = 0; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "none", + }; + }); + } + var tabs = connections.map(function(c, i) { return me.renderTab(c, i) }); + return
    + {tabs} +
    +
    + < + > +
    +
    ; + } + + function renderTab(c, i) { + var cur = body.cur; + return
    + {c.name} + {c.unreaded > 0 ? {c.unreaded} : ""} +
    ; + } + + function update_cur(idx) { + checkClickTime(function() { + body.cur = idx; + update(); + self.timer(1ms, adjustHeader); + }); + } + + event click $(div.tab) (_, me) { + var idx = me.index; + if (idx == body.cur) return; + this.update_cur(idx); + } + + event click $(span#left-arrow) { + var cur = body.cur; + if (cur == 0) return; + this.update_cur(cur - 1); + } + + event click $(span#right-arrow) { + var cur = body.cur; + if (cur == connections.length - 1) return; + this.update_cur(cur + 1); + } +} + +if (is_osx) { + $(header).content(
    ); + $(header).attributes["role"] = "window-caption"; +} else { + $(div.window-toolbar).content(
    ); + setWindowButontsAndIcon(true); +} + +event click $(div.chaticon) { + checkClickTime(function() { + show_chat = !show_chat; + adaptSizeForChat(); + }); +} + +function checkClickTime(callback) { + callback(); +} + +function adaptSizeForChat() { + $(div.right-panel).style.set { + display: show_chat ? "block" : "none", + }; + var (x, y, w, h) = view.box(#rectw, #border, #screen); + if (show_chat && w < 600) { + view.move(x - (600 - w), y, 600, h); + } else if (!show_chat && w > 450) { + view.move(x + (w - 300), y, 300, h); + } +} + +function update() { + header.update(); + body.update(); +} + +function bring_to_top(idx=-1) { + if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) { + if (is_linux) { + view.focus = self; + } else { + view.windowState = View.WINDOW_SHOWN; + } + if (idx >= 0) body.cur = idx; + } else { + view.windowTopmost = true; + view.windowTopmost = false; + } +} + +handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio) { + var conn; + connections.map(function(c) { + if (c.id == id) conn = c; + }); + if (conn) { + conn.authorized = authorized; + update(); + return; + } + if (!name) name = "NA"; + connections.push({ + id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, + port_forward: port_forward, + name: name, authorized: authorized, time: new Date(), + keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, + audio: audio, + }); + body.cur = connections.length - 1; + bring_to_top(); + update(); + self.timer(1ms, adjustHeader); + if (authorized) { + self.timer(3s, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + } +} + +handler.removeConnection = function(id) { + var i = -1; + connections.map(function(c, idx) { + if (c.id == id) i = idx; + }); + connections.splice(i, 1); + if (connections.length == 0) { + handler.exit(); + } else { + if (body.cur >= i && body.cur > 0) body.cur -= 1; + update(); + } +} + +handler.newMessage = function(id, text) { + var idx = -1; + connections.map(function(c, i) { + if (c.id == id) idx = i; + }); + var conn = connections[idx]; + if (!conn) return; + conn.msgs.push({name: conn.name, text: text, time: getNowStr()}); + bring_to_top(idx); + if (idx == body.cur) { + var shouldAdapt = !show_chat; + show_chat = true; + if (shouldAdapt) adaptSizeForChat(); + } + conn.unreaded += 1; + update(); +} + +handler.awake = function() { + view.windowState = View.WINDOW_SHOWN; + view.focus = self; +} + +view << event statechange { + adjustBorder(); +} + +function self.ready() { + adjustBorder(); + var (sw, sh) = view.screenBox(#workarea, #dimension); + var w = 300; + var h = 400; + view.move(sw - w, 0, w, h); +} + +function getElaspsed(time) { + var now = new Date(); + var seconds = Date.diff(time, now, #seconds); + var hours = seconds / 3600; + var days = hours / 24; + hours = hours % 24; + var minutes = seconds % 3600 / 60; + seconds = seconds % 60; + var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds); + if (days > 0) { + out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out); + } + return out; +} + +function updateTime() { + self.timer(1s, function() { + var el = $(#time); + if (el) { + var c = connections[body.cur]; + if (c) { + el.text = getElaspsed(c.time); + } + } + updateTime(); + }); +} + +updateTime(); + +function self.closing() { + view.windowState = View.WINDOW_HIDDEN; + return false; +} + + +function adjustHeader() { + var hw = $(header).box(#width); + var tabswrapper = $(div.tabs-wrapper); + var tabs = $(div.tabs); + var arrows = $(div.tab-arrows); + if (!arrows) return; + var n = connections.length; + var wtab = 80; + var max = hw - 98; + var need_width = n * wtab + 2; // include border of active tab + if (need_width < max) { + arrows.style.set { + display: "none", + }; + tabs.style.set { + width: need_width, + margin-left: 0, + }; + tabswrapper.style.set { + width: need_width, + }; + } else { + var margin = (body.cur + 1) * wtab - max + 30; + if (margin < 0) margin = 0; + arrows.style.set { + display: "block", + }; + tabs.style.set { + width: (max - 20 + margin) + 'px', + margin-left: -margin + 'px' + }; + tabswrapper.style.set { + width: (max + 10) + 'px', + }; + } +} + +view.on("size", adjustHeader); + +// handler.addConnection(0, false, 0, "", "test1", true, false, false, false); +// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false); +// handler.addConnection(2, false, 0, "", "test3", true, false, false, false); +// handler.newMessage(0, 'h'); diff --git a/rust-rdp/rust-desk/src/ui/common.css b/rust-rdp/rust-desk/src/ui/common.css new file mode 100644 index 0000000..318ac0b --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/common.css @@ -0,0 +1,384 @@ +html { + var(accent): #0071ff; + var(button): #2C8CFF; + var(gray-bg): #eee; + var(bg): white; + var(border): #ccc; + var(hover-border): #999; + var(text): #222; + var(placeholder): #aaa; + var(lighter-text): #888; + var(light-text): #666; + var(dark-red): #A72145; + var(dark-yellow): #FBC732; + var(dark-blue): #2E2459; + var(green-blue): #197260; + var(gray-blue): #2B3439; + var(blue-green): #4299bf; + var(light-green): #D4EAB7; + var(dark-green): #5CB85C; + var(blood-red): #F82600; +} + +body { + margin: 0; + color: color(text); +} + +button.button { + height: 2em; + border-radius: 0.5em; + background: color(button); + color: color(bg); + border-color: color(button); + min-width: 40px; +} + +button[type=checkbox], button[type=checkbox]:active { + background: none; + border: none; + color: unset; + height: 1.4em; +} + +button.outline { + border: color(border) solid 1px; + background: transparent; + color: color(text); +} + +button.button:active, button.active { + background: color(accent); + color: color(bg); + border-color: color(accent); +} + +button.button:hover, button.outline:hover { + border-color: color(hover-border); +} + +input[type=text], input[type=password], input[type=number] { + width: *; + font-size: 1.5em; + border-color: color(border); + border-radius: 0; + color: black; + padding-left: 0.5em; +} + +input:empty { + color: color(placeholder); +} + +input.outline-focus:focus { + outline: color(button) solid 3px; +} + +@set my-scrollbar +{ + .prev { display:none; } + .next { display:none; } + .base, .next-page, .prev-page { background: white;} + .slider { background: #bbb; border: white solid 4px; } + .base:disabled { background: transparent; } + .slider:hover { background: grey; } + .slider:active { background: grey; } + .base { size: 16px; } + .corner { background: white; } +} + +@mixin ELLIPSIS { + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} + +.ellipsis { + @ELLIPSIS; +} + +div.password svg { + padding-left: 1em; + size: 16px; + color: #ddd; + background: none; + padding-top: 4px!important; +} + +div.password input { + font-family: Consolas, Menlo, Monaco, 'Courier New'; + font-size: 1.2em; +} + +svg { + background: none; +} + +header { + border-bottom: color(border) solid 1px; + height: 22px; + flow: horizontal; + overflow-x: hidden; + position: relative; +} + +@media platform == "OSX" { + header { + background: linear-gradient(top,#E4E4E4,#D1D1D1); + } +} + +header div.window-icon { + size: 22px; +} + +@media platform != "OSX" { +header { + background: white; + height: 30px; +} + +header div.window-icon { + size: 30px; +} +} + +header div.window-icon icon { + display: block; + margin: *; + size: 16px; + background-size: cover; + background-repeat: no-repeat; +} + +header caption { + size: *; +} + +@media platform != "OSX" { + button.window { + top: 0; + padding: 0 10px; + width: 22px; + position: absolute; + color: black; + border: none; + background: none; + border-radius: 0; + } + button.window div { + size: 10px; + margin: *; + background-size: cover; + background-repeat: no-repeat; + } + button.window:hover { + background: color(gray-bg); + } + button.window#minimize { + right: 84px; + } + button.window#maximize { + right: 42px; + } + button.window#close { + right: 0px; + } + button.window#minimize div { + height: 3px; + border-bottom: black solid 1px; + width: 12px; + } + button.window#maximize div { + border: black solid 1px; + } + button.window#close:hover { + background: #F82600; + } + button.window#close:hover div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + } + button.window#close div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + size: 12px; + } + button.window#maximize.restore div { + border: none; + size: 12px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB1JREFUCNdjsP/AoCDA8P8CQ0MABipgaHBg+H8AAMfSC36WAZteAAAAAElFTkSuQmCC'); +} +} + +div.chatbox { + size: *; +} + +div.chatbox div.send svg { + size: 16px; +} + +div.chatbox div.send span:active { + opacity: 0.5; +} + +div.chatbox div.send span { + display: inline-block; + padding: 6px; +} + +div.chatbox .msgs { + border: none; + size: *; + border-bottom: color(border) 1px solid; + overflow-x: hidden; + overflow-y: scroll-indicator; + border-spacing: 1em; + padding: 1em; +} + +div.chatbox div.send { + flow: horizontal; + height: 30px; + padding: 5px; +} + +div.chatbox div.send input { + height: 20px !important; +} + +div.chatbox div.name { + color: color(dark-green); +} + +div.chatbox div.right-side div { + text-align: right; +} + +div.chatbox div.text { + margin-top: 0.5em; +} + +@media platform != "OSX" { +header .window-toolbar { + width: max-content; + background: transparent; + position: absolute; + bottom: 4px; + height: 24px; +} +} + +header svg, menu svg { + size: 14px; +} + +header span, menu span { + padding: 4px 8px; + margin: * 0.5em; + color: color(light-text); +} + +progress { + display: inline-block; + aspect: Progress; + border: none; + margin-right: 1em; + height: 0.25em; + background: transparent; +} + +menu div.separator { + height: 1px; + width: *; + margin: 5px 0; + background: color(gray-bg); + border: none; +} + +menu li { + position: relative; +} + +menu li span { + display: none; +} + +menu li.selected span { + display: inline-block; + position: absolute; + left: -10px; + top: 2px; +} + +.link { + cursor: pointer; + text-decoration: underline; +} + +.link:active { + opacity: 0.5; +} + +menu li.line-through { + text-decoration-line: line-through; + color: red; +} + +div#msgbox .msgbox-icon svg { + size: 80px; + background: white; + +} +div#msgbox .form { + border-spacing: 0.5em; +} + +div#msgbox .caption { + @ELLIPSIS; + height: 2em; + line-height: 2em; + text-align: center; + color: white; + font-weight: bold; +} + +div#msgbox .form .text { + @ELLIPSIS; +} + +div#msgbox button.button { + margin-left: 1.6em; +} + +div#msgbox div.password { + position: relative; +} + +div#msgbox div.password svg { + position: absolute; + right: 0.25em; + top: 0.25em; + padding: 0.5em; + color: color(text); +} + +div#msgbox div.set-password > div { + flow: horizontal; +} + +div#msgbox div.set-password > div > span { + width: 30%; + line-height: 2em; +} + +div#msgbox div.set-password div.password { + width: *; +} + +div#msgbox div.set-password input { + font-size: 1em; +} + +div#msgbox #error { + color: red; +} diff --git a/rust-rdp/rust-desk/src/ui/common.tis b/rust-rdp/rust-desk/src/ui/common.tis new file mode 100644 index 0000000..46fb1f7 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/common.tis @@ -0,0 +1,359 @@ +include "sciter:reactor.tis"; + +var handler = $(#handler) || view; +try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {} +var OS = view.mediaVar("platform"); +var is_osx = OS == "OSX"; +var is_win = OS == "Windows"; +var is_linux = OS == "Linux"; +var is_file_transfer; +var is_xfce = false; +try { is_xfce = handler.is_xfce(); } catch(e) {} + +function isEnterKey(evt) { + return (evt.keyCode == Event.VK_ENTER || + (is_osx && evt.keyCode == 0x4C) || + (is_linux && evt.keyCode == 65421)); +} + +function translate(name) { + try { + return handler.t(name); + } catch(_) { + return name; + } +} + +function hashCode(str) { + var hash = 160 << 16 + 114 << 8 + 91; + for (var i = 0; i < str.length; i += 1) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash % 16777216; +} + +function intToRGB(i, a = 1) { + return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0x7F) + + ',' + (i & 0xFF) + ',' + a + ')'; +} + +function string2RGB(s, a = 1) { + return intToRGB(hashCode(s), a); +} + +function getTime() { + var now = new Date(); + return now.valueOf(); +} + +function platformSvg(platform, color) { + platform = (platform || "").toLowerCase(); + if (platform == "linux") { + return + + + + + ; + } + if (platform == "mac os") { + return + + ; + } + return + + ; +} + +function centerize(w, h) { + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (w > sw) w = sw; + if (h > sh) h = sh; + var x = (sx + sw - w) / 2; + var y = (sy + sh - h) / 2; + view.move(x, y, w, h); +} + +function setWindowButontsAndIcon(only_min=false) { + if (only_min) { + $(div.window-buttons).content(
    +
    +
    ); + } else { + $(div.window-buttons).content(
    +
    +
    +
    +
    ); + } + $(div.window-icon>icon).style.set { + "background-image": "url('" + handler.get_icon() + "')", + }; +} + +function adjustBorder() { + if (is_osx) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + $(header).style.set { + display: "none", + }; + } else { + $(header).style.set { + display: "block", + padding: "0", + }; + } + return; + } + if (view.windowState == view.WINDOW_MAXIMIZED) { + self.style.set { + border: "window-frame-width solid transparent", + }; + } else if (view.windowState == view.WINDOW_FULL_SCREEN) { + self.style.set { + border: "none", + }; + } else { + self.style.set { + border: "black solid 1px", + }; + } + var el = $(button#maximize); + if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED); + el = $(span#fullscreen); + if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN); +} + +var svg_checkmark = ; +var svg_edit = + +; +var svg_eye = + + +; +var svg_send = + +; +var svg_chat = + +; + +function scrollToBottom(el) { + var y = el.box(#height, #content) - el.box(#height, #client); + el.scrollTo(0, y); +} + +function getNowStr() { + var now = new Date(); + return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second); +} + +/******************** start of chatbox ****************************************/ +class ChatBox: Reactor.Component { + this var msgs = []; + this var callback; + + function this(params) { + if (params) { + this.msgs = params.msgs || []; + this.callback = params.callback; + } + } + + function renderMsg(msg) { + var cls = msg.name == "me" ? "right-side msg" : "left-side msg"; + return
    + {msg.name == "me" ? +
    {msg.time + " "} me
    : +
    {msg.name} {" " + msg.time}
    + } +
    {msg.text}
    +
    ; + } + + function render() { + var me = this; + var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); }); + self.timer(1ms, function() { + scrollToBottom(me.msgs); + }); + return
    + + {msgs} + +
    + + {svg_send} +
    +
    ; + } + + function send() { + var el = this.$(input); + var value = (el.value || "").trim(); + el.value = ""; + if (!value) return; + if (this.callback) this.callback(value); + } + + event keydown $(input) (evt) { + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + this.send(); + } + } + } + + event click $(div.send span) { + this.send(); + view.focus = $(input); + } +} +/******************** end of chatbox ****************************************/ + +/******************** start of msgbox ****************************************/ +var remember_password = false; +function msgbox(type, title, content, callback=null, height=180, width=500, hasRetry=false, contentStyle="") { + $(body).scrollTo(0, 0); + if (!type) { + closeMsgbox(); + return; + } + var remember = false; + try { remember = handler.get_remember(); } catch(e) {} + width += is_xfce ? 50 : 0; + height += is_xfce ? 50 : 0; + + if (type.indexOf("input-password") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.password, res.remember); + if (!is_port_forward) handler.msgbox("connecting", "Connecting...", "Logging in..."); + }; + } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { + callback = function() { view.close(); } + } + $(#msgbox).content(); +} + +function connecting() { + handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait."); +} + +handler.msgbox = function(type, title, text, hasRetry=false) { + // crash somehow (when input wrong password), even with small time, for example, 1ms + self.timer(30ms, function() { msgbox(type, title, text, null, 180, 500, hasRetry); }); +} + +var reconnectTimeout = 1000; +handler.msgbox_retry = function(type, title, text, hasRetry) { + handler.msgbox(type, title, text, hasRetry); + if (hasRetry) { + self.timer(0, retryConnect); + self.timer(reconnectTimeout, retryConnect); + reconnectTimeout *= 2; + } else { + reconnectTimeout = 1000; + } +} + +function retryConnect(cancelTimer=false) { + if (cancelTimer) self.timer(0, retryConnect); + if (!is_port_forward) connecting(); + handler.reconnect(); +} +/******************** end of msgbox ****************************************/ + +function Progress() +{ + var _val; + var pos = -0.25; + + function step() { + if( _val !== undefined ) { this.refresh(); return false; } + pos += 0.02; + if( pos > 1.25) + pos = -0.25; + this.refresh(); + return true; + } + + function paintNoValue(gfx) + { + var (w,h) = this.box(#dimension,#inner); + var x = pos * w; + w = w * 0.25; + gfx.fillColor( this.style#color ) + .pushLayer(#inner-box) + .rectangle(x,0,w,h) + .popLayer(); + return true; + } + + this[#value] = property(v) { + get return _val; + set { + _val = undefined; + pos = -0.25; + this.paintContent = paintNoValue; + this.animate(step); + this.refresh(); + } + } + + this.value = ""; +} + +var svg_eye_cross = + + +; + +class PasswordComponent: Reactor.Component { + this var visible = false; + this var value = ''; + this var name = 'password'; + + function this(params) { + if (params && params.value) { + this.value = params.value; + } + if (params && params.name) { + this.name = params.name; + } + } + + function render() { + return
    + + {this.visible ? svg_eye_cross : svg_eye} +
    ; + } + + event click $(svg) { + var el = this.$(input); + var value = el.value; + var start = el.xcall(#selectionStart) || 0; + var end = el.xcall(#selectionEnd); + this.update({ visible: !this.visible }); + var me = this; + self.timer(30ms, function() { + var el = me.$(input); + view.focus = el; + el.value = value; + el.xcall(#setSelection, start, end); + }); + } +} + +function isReasonableSize(r) { + var x = r[0]; + var y = r[1]; + return !(x < -3200 || x > 3200 || y < -3200 || y > 3200); +} + diff --git a/rust-rdp/rust-desk/src/ui/file_transfer.css b/rust-rdp/rust-desk/src/ui/file_transfer.css new file mode 100644 index 0000000..d1e1a40 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/file_transfer.css @@ -0,0 +1,265 @@ +div#file-transfer-wrapper { + size:*; + display: none; +} + +div#file-transfer { + size: *; + margin: 0; + flow: horizontal; + background: color(gray-bg); + padding: 0.5em; +} + +table +{ + font: system; + border: 1px solid color(border); + flow: table-fixed; + prototype: Grid; + size: *; + padding:0; + border-spacing: 0; + overflow-x: auto; + overflow-y: hidden; +} + +table > thead { + behavior: column-resizer; + border-bottom: color(border) solid 1px; +} + +table > tbody { + behavior: select-multiple; + overflow-y: scroll-indicator; + size: *; + background: white; +} + +table th { + background-color: color(gray-bg); +} + +table th +{ + padding: 4px; + foreground-repeat: no-repeat; + foreground-position: 50% 3px auto auto; + border-left: color(border) solid 1px; +} + +table th.sortable[sort=asc] +{ + foreground-image: url(stock:arrow-down); +} + +table th.sortable[sort=desc] +{ + foreground-image: url(stock:arrow-up); +} + +table th:nth-child(1) { + width: 32px; +} + +table th:nth-child(2) { + width: *; +} + +table th:nth-child(3) { + width: *; +} + +table th:nth-child(4) { + width: 45px; +} + +table.has_current thead th:current { + font-weight: bold; +} + +table tr:nth-child(odd) { background-color: white; } /* each odd row */ +table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ + +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} + +table.has_current tbody tr:checked +{ + background-color: color(accent); +} + +table.has_current tbody tr:checked td { + color: highlighttext; +} + +table td +{ + padding: 4px; + text-align: left; + font-size: 1em; + height: 1.4em; + @ELLIPSIS; +} + +table.folder-view td:nth-child(1) { + behavior:shell-icon; +} + +table td:nth-child(3), table td:nth-child(4) { + color: color(lighter-text); + font-size: 0.9em; +} + +table.has_current tr:current td { + color: white; +} + +table td:nth-child(4) { + text-align: right; +} + +section { + size: *; + margin: 1em; + border-spacing: 0.5em; +} + +table td:nth-child(1) { + foreground-repeat: no-repeat; + foreground-position: 50% 50% +} + +div.toolbar { + flow: horizontal; +} + +div.toolbar svg { + size: 16px; +} + +div.toolbar .spacer { + width: *; +} + +div.toolbar > div.button { + padding: 4px 8px; + opacity: 0.66; +} + +div.toolbar > div.button:active { + opacity: 1; + background-color: #ddd; +} + +div.toolbar > div.button:hover { + opacity: 1; +} + +div.toolbar > div.send { + flow: horizontal; + border-spacing: 0.5em; +} + +div.remote > div.send svg { + transform: scale(-1, 1); +} + +div.navbar { + border: color(border) solid 1px; + padding: 4px 0; +} + +select.select-dir { + width: *; + padding: 0 4px; +} + +div.title { + flow: horizontal; + border-spacing: 1em; + position: relative; +} + +div.title svg.computer { + size: 48px; +} + +div.title div { + margin: * 0; + color: color(light-text); +} + +div.title div.platform { + position: absolute; + left: 12px; + top: 7px; +} + +div.title div.platform svg { + size: 24px; +} + +table.job-table tr td { + width: *; + padding: 0.5em 1em; + border-bottom: color(border) 1px solid; + flow: horizontal; + border-spacing: 1em; + height: 3em; + overflow-x: hidden; +} + +table.job-table tr svg { + size: 16px; +} + +table.job-table tr.is_remote svg { + transform: scale(-1, 1); +} + +table.job-table tr td div.text { + width: *; + overflow-x: hidden; +} + +table.job-table tr td div.path { + width: *; + color: color(light-text); + @ELLIPSIS; +} + +table.job-table tr:current td div.path { + color: white; +} + +table#port-forward thead tr th { + padding-left: 1em; + size: *; +} + +table#port-forward tr td { + height: 3em; + text-align: left; +} + +table#port-forward input[type=text], table#port-forward input[type=number] { + font-size: 1.2em; +} + +table#port-forward td.right-arrow svg { + size: 1.2em; + transform: rotate(180deg); +} + +table#port-forward td.remove svg { + size: 0.8em; +} + +table#port-forward tr.value td { + padding-left: 1em; + font-size: 1.5em; + color: black; +} diff --git a/rust-rdp/rust-desk/src/ui/file_transfer.tis b/rust-rdp/rust-desk/src/ui/file_transfer.tis new file mode 100644 index 0000000..0223e6d --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/file_transfer.tis @@ -0,0 +1,654 @@ +var remote_home_dir; + +var svg_add_folder = + + +; +var svg_trash = + + + +; +var svg_arrow = + +; +var svg_home = + +; +var svg_refresh = + +; +var svg_cancel = ; +var svg_computer = + + + +; + +function getSize(type, size) { + if (!size) { + if (type <= 3) return ""; + return "0B"; + } + size = size.toFloat(); + var toFixed = function(size) { + size = (size * 100).toInteger(); + var a = (size / 100).toInteger(); + if (size % 100 == 0) return a; + if (size % 10 == 0) return a + '.' + (size % 10); + var b = size % 100; + if (b < 10) b = '0' + b; + return a + '.' + b; + } + if (size < 1024) return size.toInteger() + "B"; + if (size < 1024 * 1024) return toFixed(size / 1024) + "K"; + if (size < 1024 * 1024 * 1024) return toFixed(size / (1024 * 1024)) + "M"; + return toFixed(size / (1024 * 1024 * 1024)) + "G"; +} + +function getParentPath(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + if (res <= 0) return "/"; + return path.substr(0, res); +} + +function getFileName(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + return path.substr(res + 1); +} + +function getExt(name) { + if (name.indexOf(".") == 0) { + return ""; + } + var i = name.lastIndexOf("."); + if (i > 0) return name.substr(i + 1); + return ""; +} + +var jobIdCounter = 1; + +class JobTable: Reactor.Component { + this var jobs = []; + this var job_map = {}; + + function render() { + var me = this; + var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); }); + return
    + + {rows} + +
    ; + } + + event click $(svg.cancel) (_, me) { + var job = this.jobs[me.parent.parent.index]; + var id = job.id; + handler.cancel_job(id); + delete this.job_map[id]; + var i = -1; + this.jobs.map(function(job, idx) { + if (job.id == id) i = idx; + }); + this.jobs.splice(i, 1); + this.update(); + var is_remote = job.is_remote; + if (job.type != "del-dir") is_remote = !is_remote; + refreshDir(is_remote); + } + + function send(path, is_remote) { + var to; + var show_hidden; + if (is_remote) { + to = file_transfer.local_folder_view.fd.path; + show_hidden = file_transfer.remote_folder_view.show_hidden; + } else { + to = file_transfer.remote_folder_view.fd.path; + show_hidden = file_transfer.local_folder_view.show_hidden; + } + if (!to) return; + to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); + var id = jobIdCounter; + jobIdCounter += 1; + this.jobs.push({ type: "transfer", + id: id, path: path, to: to, + include_hidden: show_hidden, + is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.send_files(id, path, to, show_hidden, is_remote); + this.update(); + } + + function addDelDir(path, is_remote) { + var id = jobIdCounter; + jobIdCounter += 1; + this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.remove_dir_all(id, path, is_remote); + this.update(); + } + + function getSvg(job) { + if (job.type == "transfer") { + return svg_send; + } else if (job.type == "del-dir") { + return svg_trash; + } + } + + function getStatus(job) { + if (!job.entries) return translate("Waiting"); + var i = job.file_num + 1; + var n = job.num_entries || job.entries.length; + if (i > n) i = n; + var res = i + ' / ' + n + " " + translate("files"); + if (job.total_size > 0) { + var s = getSize(0, job.finished_size); + if (s) s += " / "; + res += ", " + s + getSize(0, job.total_size); + } + // below has problem if some file skipped + var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); + if (job.finished) percent = '100'; + if (percent) res += ", " + percent + "%"; + if (job.finished) res = translate("Finished") + " " + res; + if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; + return res; + } + + function updateJob(job) { + var el = this.select("div[id=s" + job.id + "]"); + if (el) el.text = this.getStatus(job); + } + + function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) { + var job = this.job_map[id]; + if (!job) return; + if (file_num < job.file_num) return; + job.file_num = file_num; + var n = job.num_entries || job.entries.length; + job.finished = job.file_num >= n - 1 || err == "cancel"; + job.finished_size = finished_size; + job.speed = speed || 0; + this.updateJob(job); + if (job.type == "del-dir") { + if (job.finished) { + if (!err) { + handler.remove_dir(job.id, job.path, job.is_remote); + refreshDir(job.is_remote); + if (is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + else file_transfer.local_folder_view.table.resetCurrent(); + } + } else if (!job.no_confirm) { + handler.confirm_delete_files(id, job.file_num + 1); + } + } else if (job.finished || file_num == -1) { + refreshDir(!job.is_remote); + } + } + + function renderRow(job, i) { + var svg = this.getSvg(job); + return + {svg} +
    +
    {job.path}
    +
    {this.getStatus(job)}
    +
    + {svg_cancel} + ; + } +} + +class FolderView : Reactor.Component { + this var fd = {}; + this var history = []; + this var show_hidden = false; + + function sep() { + return handler.get_path_sep(this.is_remote); + } + + function this(params) { + this.is_remote = params.is_remote; + if (this.is_remote) { + this.show_hidden = !!handler.get_option("remote_show_hidden"); + } else { + this.show_hidden = !!handler.get_option("local_show_hidden"); + } + if (!this.is_remote) { + var dir = handler.get_option("local_dir"); + if (dir) { + this.fd = handler.read_dir(dir, this.show_hidden); + if (this.fd) return; + } + this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden); + } + } + + // sort predicate + function foldersFirst(a, b) { + if (a.type <= 3 && b.type > 3) return -1; + if (a.type > 3 && b.type <= 3) return +1; + if (a.name == b.name) return 0; + return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase()); + } + + function render() + { + return
    + {this.renderTitle()} + {this.renderNavBar()} + {this.renderOpBar()} + {this.renderTable()} +
    ; + } + + function renderTitle() { + return
    + {svg_computer} +
    {platformSvg(handler.get_platform(this.is_remote), "white")}
    +
    {translate(this.is_remote ? "Remote Computer" : "Local Computer")}
    +
    + } + + function renderNavBar() { + return
    +
    {svg_home}
    +
    {svg_arrow}
    +
    {svg_arrow}
    + {this.renderSelect()} +
    {svg_refresh}
    +
    ; + } + + function renderSelect() { + return ; + } + + function renderOpBar() { + if (this.is_remote) { + return
    +
    {svg_send}{translate('Receive')}
    +
    +
    {svg_add_folder}
    +
    {svg_trash}
    +
    ; + } + return
    +
    {svg_add_folder}
    +
    {svg_trash}
    +
    +
    {translate('Send')}{svg_send}
    +
    ; + } + + function get_updated() { + this.table.sortRows(false); + if (this.fd && this.fd.path) this.select_dir.value = this.fd.path; + } + + function renderTable() { + var fd = this.fd; + var entries = fd.entries || []; + var table = this.table; + if (!table || !table.sortBy) { + entries.sort(this.foldersFirst); + } + var me = this; + var path = fd.path; + if (path != "/" && path) { + entries = [{ name: "..", type: 1 }].concat(entries); + } + var rows = entries.map(function(e) { return me.renderRow(e); }); + var id = (this.is_remote ? "remote" : "local") + "-folder-view"; + return + + + + + {rows} + + + +
  • {svg_checkmark}{translate('Show Hidden Files')}
  • + +
    +
    {translate('Name')}{translate('Modified')}{translate('Size')}
    ; + } + + function joinPath(name) { + var path = this.fd.path; + if (path == "/") { + if (this.sep() == "/") return this.sep() + name; + else return name; + } + return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name; + } + + function attached() { + var me = this; + this.table.onRowDoubleClick = function (row) { + var type = row[0].attributes["type"]; + if (type > 3) return; + var name = row[1].text; + var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name); + me.table.resetCurrent(); + me.goto(path, true); + } + this.get_updated(); + } + + function goto(path, push) { + if (!path) return; + if (this.sep() == "\\" && path.length == 2) { // windows drive + path += "\\"; + } + if (push) this.pushHistory(); + if (this.is_remote) { + handler.read_remote_dir(path, this.show_hidden); + } else { + var fd = handler.read_dir(path, this.show_hidden); + this.refresh({ fd: fd }); + } + } + + function refresh(data) { + if (!data.fd || !data.fd.path) return; + if (this.is_remote && !remote_home_dir) { + remote_home_dir = data.fd.path; + } + this.update(data); + var me = this; + self.timer(1ms, function() { me.get_updated(); }); + } + + function renderRow(entry) { + var path; + if (this.is_remote) { + path = handler.get_icon_path(entry.type, getExt(entry.name)); + } else { + path = this.joinPath(entry.name); + } + var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0; + return + + {entry.name} + {tm || ""} + {getSize(entry.type, entry.size)} + ; + } + + event click $(#switch-hidden) { + this.show_hidden = !this.show_hidden; + this.refreshDir(); + } + + event click $(.goup) () { + var path = this.fd.path; + if (!path || path == "/") return; + path = getParentPath(this.is_remote, path); + this.goto(path, true); + } + + event click $(.goback) () { + var path = this.history.pop(); + if (!path) return; + this.goto(path, false); + } + + event click $(.trash) () { + var rows = this.getCurrentRows(); + if (!rows || rows.length == 0) return; + + var delete_dirs = new Array(); + + for (var i = 0; i < rows.length; ++i) { + var row = rows[i]; + + var path = row[0]; + var type = row[1]; + + var new_history = []; + for (var j = 0; j < this.history.length; ++j) { + var h = this.history[j]; + if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h); + } + this.history = new_history; + if (type == 1) { + delete_dirs.push(path); + } else { + confirmDelete(path, this.is_remote); + } + } + for (var i = 0; i < delete_dirs.length; ++i) { + file_transfer.job_table.addDelDir(delete_dirs[i], this.is_remote); + } + } + + event click $(.add-folder) () { + var me = this; + msgbox("custom", translate("Create Folder"), "
    \ +
    " + translate("Please enter the folder name") + ":
    \ +
    \ +
    ", function(res=null) { + if (!res) return; + if (!res.name) return; + var name = res.name.trim(); + if (!name) return; + if (name.indexOf(me.sep()) >= 0) { + handler.msgbox("custom-error", "Create Folder", "Invalid folder name"); + return; + } + var path = me.joinPath(name); + handler.create_dir(jobIdCounter, path, me.is_remote); + create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path }; + jobIdCounter += 1; + }); + } + + function refreshDir() { + this.goto(this.fd.path, false); + } + + event click $(.refresh) () { + this.refreshDir(); + } + + event click $(.home) () { + var path = this.is_remote ? remote_home_dir : handler.get_home_dir(); + if (!path) return; + if (path == this.fd.path) return; + this.goto(path, true); + } + + function getCurrentRow() { + var row = this.table.getCurrentRow(); + if (!row) return; + var name = row[1].text; + if (!name || name == "..") return; + var type = row[0].attributes["type"]; + return [this.joinPath(name), type]; + } + + function getCurrentRows() { + var rows = this.table.getCurrentRows(); + if (!rows || rows.length== 0) return; + + var records = new Array(); + + for (var i = 0; i < rows.length; ++i) { + var name = rows[i][1].text; + if (!name || name == "..") continue; + + var type = rows[i][0].attributes["type"]; + records.push([this.joinPath(name), type]); + } + return records; + } + + event click $(.send) () { + var rows = this.getCurrentRows(); + if (!rows || rows.length == 0) return; + for (var i = 0; i < rows.length; ++i) { + file_transfer.job_table.send(rows[i][0], this.is_remote); + } + } + + event change $(.select-dir) (_, el) { + var x = getTime() - last_key_time; + if (x < 1000) return; + if (this.fd.path != el.value) { + this.goto(el.value, true); + } + } + + event keydown $(.select-dir) (evt, me) { + if (isEnterKey(evt)) { + this.goto(me.value, true); + } + } + + function pushHistory() { + var path = this.fd.path; + if (!path) return; + if (path != this.history[this.history.length - 1]) this.history.push(path); + } +} + +var file_transfer; + +class FileTransfer: Reactor.Component { + function this() { + file_transfer = this; + } + + function render() { + return
    + + + +
    ; + } +} + +function initializeFileTransfer() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} + +handler.updateFolderFiles = function(fd) { + fd.entries = fd.entries || []; + if (fd.id > 0) { + var jt = file_transfer.job_table; + var job = jt.job_map[fd.id]; + if (job) { + job.file_num = -1; + job.total_size = fd.total_size; + job.entries = fd.entries; + job.num_entries = fd.num_entries; + file_transfer.job_table.updateJobStatus(job.id); + } + } else { + file_transfer.remote_folder_view.refresh({ fd: fd }); + } +} + +handler.jobProgress = function(id, file_num, speed, finished_size) { + file_transfer.job_table.updateJobStatus(id, file_num, null, speed, finished_size); +} + +handler.jobDone = function(id, file_num = -1) { + var job = deleting_single_file_jobs[id] || create_dir_jobs[id]; + if (job) { + refreshDir(job.is_remote); + return; + } + file_transfer.job_table.updateJobStatus(id, file_num); +} + +handler.jobError = function(id, err, file_num = -1) { + var job = deleting_single_file_jobs[id]; + if (job) { + msgbox("custom-error", "Delete File", err); + return; + } + job = create_dir_jobs[id]; + if (job) { + msgbox("custom-error", "Create Folder", err); + return; + } + if (file_num < 0) { + handler.msgbox("custom-error", "Failed", err); + } + file_transfer.job_table.updateJobStatus(id, file_num, err); +} + +function refreshDir(is_remote) { + if (is_remote) file_transfer.remote_folder_view.refreshDir(); + else file_transfer.local_folder_view.refreshDir(); +} + +var deleting_single_file_jobs = {}; +var create_dir_jobs = {} + +function confirmDelete(path, is_remote) { + msgbox("custom-skip", "Confirm Delete", "
    \ +
    " + translate('Are you sure you want to delete this file?') + "
    \ + " + path + "
    \ +
    ", function(res=null) { + if (res) { + handler.remove_file(jobIdCounter, path, 0, is_remote); + if (is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + else file_transfer.local_folder_view.table.resetCurrent(); + deleting_single_file_jobs[jobIdCounter] = { is_remote: is_remote, path: path }; + jobIdCounter += 1; + } + }); +} + +handler.confirmDeleteFiles = function(id, i, name) { + var jt = file_transfer.job_table; + var job = jt.job_map[id]; + if (!job) return; + var n = job.num_entries; + if (i >= n) return; + var file_path = job.path; + if (name) file_path += handler.get_path_sep(job.is_remote) + name; + msgbox("custom-skip", "Confirm Delete", "
    \ +
    " + translate('Deleting') + " #" + (i + 1) + " / " + n + " " + translate('files') + ".
    \ +
    " + translate('Are you sure you want to delete this file?') + "
    \ + " + name + "
    \ +
    " + translate('Do this for all conflicts') + "
    \ +
    ", function(res=null) { + if (!res) { + jt.updateJobStatus(id, i - 1, "cancel"); + } else if (res.skip) { + if (res.remember) jt.updateJobStatus(id, i, "cancel"); + else handler.jobDone(id, i); + } else { + job.no_confirm = res.remember; + if (job.no_confirm) handler.set_no_confirm(id); + handler.remove_file(id, file_path, i, job.is_remote); + } + }); +} + +function save_file_transfer_close_state() { + var local_dir = file_transfer.local_folder_view.fd.path || ""; + var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : ""; + var remote_dir = file_transfer.remote_folder_view.fd.path || ""; + var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : ""; + handler.save_close_state("local_dir", local_dir); + handler.save_close_state("local_show_hidden", local_show_hidden); + handler.save_close_state("remote_dir", remote_dir); + handler.save_close_state("remote_show_hidden", remote_show_hidden); +} diff --git a/rust-rdp/rust-desk/src/ui/grid.tis b/rust-rdp/rust-desk/src/ui/grid.tis new file mode 100644 index 0000000..82d3857 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/grid.tis @@ -0,0 +1,258 @@ +var last_key_time = 0; +var keymap = {}; +for (var (k, v) in Event) { + k = k + "" + if (k[0] == "V" && k[1] == "K") { + keymap[v] = k; + } +} + +class Grid: Behavior { + const TABLE_HEADER_CLICK = 0x81; + const TABLE_ROW_CLICK = 0x82; + const TABLE_ROW_DBL_CLICK = 0x83; + function onHeaderClick(headerCell) + { + this.postEvent(TABLE_HEADER_CLICK, headerCell.index, headerCell); + return true; + } + + function onRowClick(row , reason) + { + this.postEvent(TABLE_ROW_CLICK, row.index, row); + return true; + } + + function onRowDoubleClick(row) + { + this.postEvent(TABLE_ROW_DBL_CLICK, row.index, row); + return true; + } + + function getCurrentRow() + { + return this.$(tbody>tr:current); + } + + function getCurrentRows() + { + return this.$$(tbody>tr:checked); + } + + function getCurrentColumn() + { + return this.$(thead>:current); // return current cell in header row + } + + function resetCurrent() { + var prev = this.getCurrentRow(); + if (prev) + { + prev.state.current = false; // drop state flag + prev.state.checked = false; // drop state flag + } + } + + function setCurrentRow(row, reason = #by_code, doubleClick = false) + { + if (!row) return; + // get previously selected row: + var prev = this.getCurrentRow(); + if (prev) + { + if (prev === row && !doubleClick) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + prev.state.checked = false; // drop state flag + } + row.state.current = true; + row.state.checked = true; + row.scrollToView(); + + if (doubleClick) + this.onRowDoubleClick(row,reason); + else + this.onRowClick(row,reason); + } + + function setCurrentColumn(col) + { + // get previously selected column: + var prev = this.getCurrentColumn(); + if (prev) + { + if (prev === col) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + } + col.state.current = true; // set state flag + col.scrollToView(); + this.onHeaderClick(col); + } + + function sortRows(sortClicked) + { + var col = this.sortBy; + if (!col) return; + var byColumn = col.index; + var nowDesc = (col.attributes["sort"] || "desc") == "desc"; + if (sortClicked) (this.$(thead [sort]) || col).attributes["sort"] = undefined; // drop any other sort order. + var getValue = function(x) { + var value = x.attributes["value"]; + if (value == undefined) return x.text.toLowerCase(); + return value.toFloat(); + } + var sort = function(r1, r2, asc) { + if (r1[1].text == "..") { + return -1; + } + if (r2[1].text == "..") { + return 1; + } + if (!asc) + return getValue(r1[byColumn]) < getValue(r2[byColumn]) ? -1 : 1; + else + return getValue(r1[byColumn]) > getValue(r2[byColumn]) ? -1 : 1; + } + if (nowDesc) + { + if (sortClicked) col.attributes["sort"] = "asc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? true : false)); + } else { + if (sortClicked) col.attributes["sort"] = "desc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? false : true)); + } + } + + function attached() + { + assert this.tag == "table" : "wrong element type for grid, table expected"; + this.body = this.$(:root>tbody); + assert this.body : "Grid require element"; + } + + function onMouse(evt) + { + if ((evt.type != Event.MOUSE_DOWN) && (evt.type != Event.MOUSE_DCLICK)) + return false; + + if (!evt.mainButton) + return false; + + // auxiliary function, returns row this target element belongs to + function targetRow(target) { return target.$p(tbody>tr); } + + // auxiliary function, returns row this target element belongs to + function targetHeaderCell(target) { return target.$p(thead>tr>th); } + + if (var row = targetRow(evt.target)) // click on the row + this.setCurrentRow(row, #by_mouse, evt.type == Event.MOUSE_DCLICK); + else if (var headerCell = targetHeaderCell(evt.target)) + { + this.setCurrentColumn(headerCell); // click on the header cell + if (evt.type != Event.MOUSE_DCLICK && headerCell.$is(.sortable)) { + this.sortBy = headerCell; + this.sortRows(true); + } + } + + //return true; // as it is always ours then stop event bubbling + } + + function onFocus(evt) + { + return (evt.type == Event.GOT_FOCUS || evt.type == Event.LOST_FOCUS); + } + + function onKey(evt) + { + last_key_time = getTime(); + if (evt.type != Event.KEY_DOWN) + return false; + + switch(evt.keyCode) + { + case Event.VK_DOWN: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + if (idx < this.body.length) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_UP: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index - 1 : this.length - 1; + if (idx >= 0) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_PRIOR: + { + var y = this.body.scroll(#top) - this.body.scroll(#height); + var r; + for(var i = this.body.length - 1; i >= 0; --i) + { + var pr = r; r = this.body[i]; + if (r.box(#top, #inner, #content) < y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + case Event.VK_NEXT: + { + var y = this.body.scroll(#top) + 2 * this.body.scroll(#height); + var lastScrollable = this.body.length - 1; + var r; + for(var i = 0; i <= lastScrollable; ++i) + { + var pr = r; r = this.body[i]; + if (r.box(#bottom, #inner, #content) > y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + + case Event.VK_HOME: + { + if (this.body.length) + this.setCurrentRow(this.body.first,#by_key); + } + return true; + + case Event.VK_END: + { + if (this.body.length) + this.setCurrentRow(this.body.last,#by_key); + } + return true; + } + var char = handler.get_char(keymap[evt.keyCode] || "", evt.keyCode); + if (char) { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + while (idx < this.body.length) { + var el = this.body[idx]; + var text = el[1].text; + if (text && text[0].toLowerCase() == char) { + this.setCurrentRow(el, #by_key); + return true; + } + idx += 1; + } + } + if (isEnterKey(evt)) { + this.onRowDoubleClick(this.getCurrentRow()); + } + return false; + } +} diff --git a/rust-rdp/rust-desk/src/ui/header.css b/rust-rdp/rust-desk/src/ui/header.css new file mode 100644 index 0000000..f0960b9 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/header.css @@ -0,0 +1,95 @@ +header div { + word-wrap: normal; +} + +header #screens { + background: white; + border: #A9A9A9 1px solid; + height: 22px; + border-radius: 4px; + flow: horizontal; + border-spacing: 0.5em; + padding-right: 1em; + position: relative; +} + +header #screen { + text-align: center; + margin: 3px 0; + width: 18px; + height: 14px; + border: color(border) solid 1px; + font-size: 11px; + color: color(light-text); +} + +@media platform == "OSX" { + header #screen { + line-height: 11px; + } +} + +header #secure { + position: absolute; + left: -10px; + top: -2px; +} + +header #secure svg { + size: 18px; +} + +header .remote-id { + width: *; + padding-left: 30px; + padding-right: 4em; + margin: * 0; +} + +header span:hover { + background: #f7f7f7; +} + +@media platform != "OSX" { +header span:hover { + background: #d9d9d9; +} +} + +header #screen:hover { + background: #d9d9d9; +} + +header #secure:hover { + background: unset; +} + +header span:active, header #screen:active { + color: black; + background: color(gray-bg); +} + +div#global-screens { + position: relative; + margin: 2px 0; +} + +div#global-screens > div { + position: absolute; + border: color(border) solid 1px; + text-align: center; + color: color(light-text); +} + +header #screen.current, div#global-screens > div.current { + background: #666; + color: white; +} + +span#fullscreen.active { + border: color(border) solid 1px; +} + +button:disabled { + opacity: 0.3; +} diff --git a/rust-rdp/rust-desk/src/ui/header.tis b/rust-rdp/rust-desk/src/ui/header.tis new file mode 100644 index 0000000..c04e7d9 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/header.tis @@ -0,0 +1,419 @@ +var pi = handler.get_default_pi(); // peer information +var chat_msgs = []; + +var svg_fullscreen = + +; +var svg_action = ; +var svg_display = + +; +var svg_secure = + +; +var svg_insecure = ; +var svg_insecure_relay = ; +var svg_secure_relay = ; + +var cur_window_state = view.windowState; +function check_state_change() { + if (view.windowState != cur_window_state) { + stateChanged(); + } + self.timer(30ms, check_state_change); +} + +if (is_linux) { + check_state_change(); +} else { + view << event statechange { + stateChanged(); + } +} + +function get_id() { + return handler.get_option('alias') || handler.get_id() +} + +function stateChanged() { + stdout.println('state changed from ' + cur_window_state + ' -> ' + view.windowState); + cur_window_state = view.windowState; + adjustBorder(); + adaptDisplay(); + if (cur_window_state != View.WINDOW_MINIMIZED) { + view.focus = handler; // to make focus away from restore/maximize button, so that enter key work + } + var fs = view.windowState == View.WINDOW_FULL_SCREEN; + var el = $(#fullscreen); + if (el) el.attributes.toggleClass("active", fs); + el = $(#maximize); + if (el) { + el.state.disabled = fs; + } + if (fs) { + $(header).style.set { + display: "none", + }; + } +} + +var header; +var old_window_state = View.WINDOW_SHOWN; + +class Header: Reactor.Component { + function this() { + header = this; + } + + function render() { + var icon_conn; + var title_conn; + if (this.secure_connection && this.direct_connection) { + icon_conn = svg_secure; + title_conn = translate("Direct and encrypted connection"); + } else if (this.secure_connection && !this.direct_connection) { + icon_conn = svg_secure_relay; + title_conn = translate("Relayed and encrypted connection"); + } else if (!this.secure_connection && this.direct_connection) { + icon_conn = svg_insecure; + title_conn = translate("Direct and unencrypted connection"); + } else { + icon_conn = svg_insecure_relay; + title_conn = translate("Relayed and unencrypted connection"); + } + var title = get_id(); + if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; + if ((pi.displays || []).length == 0) { + return
    {title}
    ; + } + var screens = pi.displays.map(function(d, i) { + return
    + {i+1} +
    ; + }); + updateWindowToolbarPosition(); + var style = "flow:horizontal;"; + if (is_osx) style += "margin:*"; + self.timer(1ms, toggleMenuState); + return
    + {is_osx || is_xfce ? "" : {svg_fullscreen}} +
    + {icon_conn} +
    {get_id()}
    +
    {screens}
    + {this.renderGlobalScreens()} +
    + {svg_chat} + {svg_action} + {svg_display} + {this.renderDisplayPop()} + {this.renderActionPop()} +
    ; + } + + function renderDisplayPop() { + return + +
  • {translate('Adjust Window')}
  • +
    +
  • {svg_checkmark}{translate('Original')}
  • +
  • {svg_checkmark}{translate('Shrink')}
  • +
  • {svg_checkmark}{translate('Stretch')}
  • +
    +
  • {svg_checkmark}{translate('Good image quality')}
  • +
  • {svg_checkmark}{translate('Balanced')}
  • +
  • {svg_checkmark}{translate('Optimize reaction time')}
  • +
  • {svg_checkmark}{translate('Custom')}
  • +
    +
  • {svg_checkmark}{translate('Show remote cursor')}
  • + {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} + {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} + {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + + ; + } + + function renderActionPop() { + return + +
  • {translate('Transfer File')}
  • +
  • {translate('TCP Tunneling')}
  • +
    + {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} +
    + {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" ?
  • {translate("Block user input")}
  • : ""} +
  • {translate('Refresh')}
  • + + ; + } + + function renderGlobalScreens() { + if (pi.displays.length < 3) return ""; + var x0 = 9999999; + var y0 = 9999999; + var x = -9999999; + var y = -9999999; + pi.displays.map(function(d, i) { + if (d.x < x0) x0 = d.x; + if (d.y < y0) y0 = d.y; + var dx = d.x + d.width; + if (dx > x) x = dx; + var dy = d.y + d.height; + if (dy > y) y = dy; + }); + var w = x - x0; + var h = y - y0; + var scale = 16. / h; + var screens = pi.displays.map(function(d, i) { + var min_wh = d.width > d.height ? d.height : d.width; + var fs = min_wh * 0.9 * scale; + var style = "width:" + (d.width * scale) + "px;" + + "height:" + (d.height * scale) + "px;" + + "left:" + ((d.x - x0) * scale) + "px;" + + "top:" + ((d.y - y0) * scale) + "px;" + + "font-size:" + fs + "px;"; + if (is_osx) { + style += "line-height:" + fs + "px;"; + } + return
    {i+1}
    ; + }); + + var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;"; + return
    + {screens} +
    ; + } + + event click $(#fullscreen) (_, el) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + if (old_window_state == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = old_window_state; + } else { + old_window_state = view.windowState; + if (view.windowState == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = View.WINDOW_FULL_SCREEN; + if (is_linux) { self.timer(150ms, function() { view.windowState = View.WINDOW_FULL_SCREEN; }); } + } + } + + event click $(#chat) { + startChat(); + } + + event click $(#action) (_, me) { + var menu = $(menu#action-options); + me.popup(menu); + } + + event click $(#display) (_, me) { + var menu = $(menu#display-options); + me.popup(menu); + } + + event click $(#screen) (_, me) { + if (pi.current_display == me.index) return; + handler.switch_display(me.index); + } + + event click $(#transfer-file) { + handler.transfer_file(); + } + + event click $(#tunnel) { + handler.tunnel(); + } + + event click $(#ctrl-alt-del) { + handler.ctrl_alt_del(); + } + + event click $(#lock-screen) { + handler.lock_screen(); + } + + event click $(#refresh) { + handler.refresh_video(); + } + + event click $(#block-input) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = translate("Unblock user input"); + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = translate("Block user input"); + } + } + + event click $(menu#display-options>li) (_, me) { + if (me.id == "custom") { + handle_custom_image_quality(); + } else if (me.attributes.hasClass("toggle-option")) { + handler.toggle_option(me.id); + toggleMenuState(); + } else if (!me.attributes.hasClass("selected")) { + var type = me.attributes["type"]; + if (type == "image-quality") { + handler.save_image_quality(me.id); + } else if (type == "view-style") { + handler.save_view_style(me.id); + adaptDisplay(); + } + toggleMenuState(); + } + } +} + +function handle_custom_image_quality() { + var tmp = handler.get_custom_image_quality(); + var bitrate0 = tmp[0] || 50; + var quantizer0 = tmp.length > 1 ? tmp[1] : 100; + msgbox("custom", "Custom Image Quality", "
    \ +
    x% bitrate
    \ +
    x% quantizer
    \ +
    ", function(res=null) { + if (!res) return; + if (!res.bitrate) return; + handler.save_custom_image_quality(res.bitrate, res.quantizer); + toggleMenuState(); + }); +} + +function toggleMenuState() { + var values = []; + var q = handler.get_image_quality(); + if (!q) q = "balanced"; + values.push(q); + var s = handler.get_view_style(); + if (!s) s = "original"; + values.push(s); + for (var el in $$(menu#display-options>li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } + for (var id in ["show-remote-cursor", "disable-audio", "disable-clipboard", "lock-after-session-end", "privacy-mode"]) { + var el = self.select('#' + id); + if (el) { + var value = handler.get_toggle_option(id); + el.attributes.toggleClass("selected", value); + if (id == "privacy-mode") { + var el = $(li#block-input); + if (el) { + el.state.disabled = value; + } + } + } + } +} + +if (is_osx) { + $(header).content(
    ); + $(header).attributes["role"] = "window-caption"; +} else { + if (is_file_transfer || is_port_forward) { + $(caption).content(
    ); + } else { + $(div.window-toolbar).content(
    ); + } + setWindowButontsAndIcon(); +} + +if (!(is_file_transfer || is_port_forward)) { + $(header).style.set { + height: "32px", + }; + if (!is_osx) { + $(div.window-icon).style.set { + size: "32px", + }; + } +} + +handler.updatePi = function(v) { + pi = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.switchDisplay = function(i) { + pi.current_display = i; + header.update(); +} + +function updateWindowToolbarPosition() { + if (is_osx) return; + self.timer(1ms, function() { + var el = $(div.window-toolbar); + var w1 = el.box(#width, #border); + var w2 = $(header).box(#width, #border); + var x = (w2 - w1) / 2; + el.style.set { + left: x + "px", + display: "block", + }; + }); +} + +view.on("size", function() { + // ensure size is done, so add timer + self.timer(1ms, function() { + updateWindowToolbarPosition(); + adaptDisplay(); + }); +}); + +handler.newMessage = function(text) { + chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()}); + startChat(); +} + +function sendMsg(text) { + chat_msgs.push({text: text, name: "me", time: getNowStr()}); + handler.send_chat(text); + if (chatbox) chatbox.refresh(); +} + +var chatbox; +function startChat() { + if (chatbox) { + chatbox.windowState = View.WINDOW_SHOWN; + chatbox.refresh(); + return; + } + var icon = handler.get_icon(); + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + var w = 300; + var h = 400; + var x = (sx + sw - w) / 2; + var y = sy + 80; + var params = { + type: View.FRAME_WINDOW, + x: x, + y: y, + width: w, + height: h, + client: true, + parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon }, + caption: get_id(), + }; + var html = handler.get_chatbox(); + if (html) params.html = html; + else params.url = self.url("chatbox.html"); + chatbox = view.window(params); +} + +handler.setConnectionType = function(secured, direct) { + header.update({ + secure_connection: secured, + direct_connection: direct, + }); +} diff --git a/rust-rdp/rust-desk/src/ui/index.css b/rust-rdp/rust-desk/src/ui/index.css new file mode 100644 index 0000000..dc7b1c1 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/index.css @@ -0,0 +1,380 @@ +html { + background-color: transparent; + var(gray-bg-osx): rgba(238, 238, 238, 0.75); +} + +body { + overflow: hidden; +} + +@media platform != "OSX" { + body { + border-top: color(border) solid 1px; + } +} + +.title { + font-size: 1.4em; +} + +.app { + flow: horizontal; + size: *; +} + +.lighter-text { + color: color(lighter-text); + font-size: 0.9em; +} + +.left-pane { + width: 200px; + height: *; + background: color(bg); + border-right: color(border) 1px solid; +} + +.left-pane > div:nth-child(1) { + border-spacing: 1em; + padding: 20px; +} + +.left-pane div { + word-wrap: break-word; +} + +div.sessions-bar { + color: color(light-text); + padding-top: 0.5em; + border-top: color(border) solid 1px; + margin-bottom: 1em; + position: relative; + flow: horizontal; +} + +div.sessions-tab span { + display: inline-block; + padding: 6px 8px; + cursor: pointer; + @ELLIPSIS; +} + +div.sessions-tab svg { + size: 14px; +} + +div.sessions-tab span.active { + cursor: default; + border-radius: 3px; + background: white; + color: black; +} + +div.search-id { + width: 120px; + padding: 0; + position: relative; + display: inline-block; +} + +div.search-id input { + font-size: 1em; + height: 20px; + border: none; + padding-left: 26px; +} + +div.search-id span { + position: absolute; + top: 0px; + padding: 6px; + color: color(border); +} + +div.search-id svg { + size: 14px; +} + +span.search-icon { + left: 0px; +} + +span.clear-input { + display: none; + right: 0px; +} + +div.search-id:hover span.clear-input { + display: inline-block; +} + +span.clear-input:hover { + color: black; +} + +.your-desktop { + border-spacing: 0.5em; + border-left: color(accent) solid 2px; + padding-left: 0.5em; +} + +.your-desktop input[type=text] { + padding: 0; + border: none; + height: 1.5em; +} + +.your-desktop > div { + color: color(light-text); +} + +.right-pane { + size: *; + background: color(gray-bg); +} + +.right-content { + overflow: scroll-indicator; + padding: 1.6em; + border-spacing: 1.6em; + size: *; + flow: vertical; +} + +@media platform == "OSX" { + .right-pane { + background: color(gray-bg-osx); + } +} + +@mixin CARD { + padding: 1.6em; + border-spacing: 1em; + background: color(bg); + border-radius: 1em; +} + +.card-connect { + @CARD; + width: 320px; +} + +.right-buttons { + text-align: right; +} + +.right-buttons>button { + margin-left: 1.6em; +} + +div.connect-status { + left: 240px; + border-top: color(border) solid 1px; + width: 100%; + background: color(gray-bg); + padding: 1em; +} + +div.connect-status > span.connect-status-icon { + border-radius: 4px; + width: 8px; + height: 8px; + display: inline-block; + margin-right: 1em; +} + +div.connect-status > span.link { + margin-left: 1em; + display: inline-block; +} + +span.connect-status-1 { + background: #e04f5f; +} + +span.connect-status1 { + background: #32bea6; +} + +span.connect-status0 { + background: #F5853B; +} + +div.recent-sessions-content { + border-spacing: 1em; + flow: horizontal-flow; +} + +div.remote-session { + border-radius: 1em; + height: 140px; + width: 220px; + padding: 0; + position: relative; + border: none; +} + +div.remote-session:hover, div.remote-session-list:hover { + outline: color(button) solid 2px -2px; +} + +div.remote-session .platform { + width: *; + height: 120px; + padding: *; + position: relative; +} + +div.remote-session .platform .username{ + left: 0; + color: #eee; + position: absolute; + bottom: 38px; + font-size: 0.8em; + width: 220px; + text-align: center; +} + +div.remote-session .platform svg { + width: 60px; + height: 60px; + background: none; +} + +div.remote-session-list { + background: white; + width: 220px; + flow: horizontal; +} + +div.remote-session-list .platform { + size: 42px; +} + +div.remote-session-list .platform svg { + width: 30px; + height: 30px; + background: none; + padding: 6px; +} + +div.remote-session-list .name { + size: *; + padding-left: 1em; +} + +div.remote-session-list .name >div { + margin-top: *; + margin-bottom: *; + width: *; +} + +div.remote-session-list .name .username { + margin-top: 3px; + font-size: 0.8em; + color: color(lighter-text); +} + +div.remote-session .text { + background: white; + position: absolute; + height: 3em; + width: 100%; + border-radius: 0 0 1em 1em; + bottom: 0; + flow: horizontal; +} + +div.remote-session .text > div { + padding-top: 1em; + padding-left: 1em; + width: *; +} + +svg#menu { + size: 1em; + background: none; + padding: 0.5em; + margin: 0.5em; + color: color(light-text); +} + +.remote-session-list svg#menu { + margin-right: 0; +} + +svg#menu:hover { + color: black; + border-radius: 1em; + background: color(gray-bg); +} + +svg#edit:hover { + color: black; +} + +svg#edit { + display: inline-block; +} + +div.install-me, div.trust-me { + margin-top: 0.5em; + padding: 20px; + color: white; + background: linear-gradient(left,#e242bc,#f4727c); +} + +div.trust-me > div:nth-child(1), +div.install-me > div:nth-child(1) { + font-size: 1.2em; + font-weight: bold; + text-align: center; + margin-bottom: 0.5em; +} + +div.install-me > div:nth-child(2) { + line-height: 1.4em; +} + +#install-me.link { + margin-top: 0.5em; +} + +div.trust-me > div:nth-child(2) { + font-size: 0.9em; + margin-bottom: 1em; +} + +div.install-me > div:nth-child(3), +div.trust-me > div:nth-child(3) { + text-align: center; + font-size: 1.5em; + font-weight: bold; +} + +div#myid { + position: relative; +} + +div#myid svg#menu { + position: absolute; + right: -1em; +} + +div.remote-session svg#menu { + position: absolute; + right: 0; + top: 0; +} + +.install-me .button { + height: 2em; + line-height: 2em; + text-align: center; + font-weight: bold; + font-size: 1em; + margin-top: 1em; + border-color: white; + border: 1px; + background: none; +} diff --git a/rust-rdp/rust-desk/src/ui/index.html b/rust-rdp/rust-desk/src/ui/index.html new file mode 100644 index 0000000..88c1722 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/rust-rdp/rust-desk/src/ui/index.tis b/rust-rdp/rust-desk/src/ui/index.tis new file mode 100644 index 0000000..8c2fb06 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/index.tis @@ -0,0 +1,740 @@ +if (is_osx) view.windowBlurbehind = #light; +stdout.println("current platform:", OS); + +// html min-width, min-height not working on mac, below works for all +view.windowMinSize = (500, 300); + +var app; +var tmp = handler.get_connect_status(); +var connect_status = tmp[0]; +var service_stopped = false; +var software_update_url = ""; +var key_confirmed = tmp[1]; +var system_error = ""; + +var svg_menu = + + + +; + +var my_id = ""; +function get_id() { + my_id = handler.get_id(); + return my_id; +} + +class ConnectStatus: Reactor.Component { + function render() { + return +
    + + {this.getConnectStatusStr()} + {service_stopped ? {translate('Start Service')} : ""} +
    ; + } + + function getConnectStatusStr() { + if (service_stopped) { + return translate("Service is not running"); + } else if (connect_status == -1) { + return translate('not_ready_status'); + } else if (connect_status == 0) { + return translate('connecting_status'); + } + return translate("Ready"); + } + + event click $(#start-service) () { + handler.set_option("stop-service", ""); + } +} + +function createNewConnect(id, type) { + id = id.replace(/\s/g, ""); + app.remote_id.value = formatId(id); + if (!id) return; + if (id == my_id) { + msgbox("custom-error", "Error", "You cannot connect to your own computer"); + return; + } + handler.set_remote_id(id); + handler.new_remote(id, type); +} + +var direct_server; +class DirectServer: Reactor.Component { + function this() { + direct_server = this; + } + + function render() { + var text = translate("Enable Direct IP Access"); + var cls = handler.get_option("direct-server") == "Y" ? "selected" : "line-through"; + return
  • {svg_checkmark}{text}
  • ; + } + + function onClick() { + handler.set_option("direct-server", handler.get_option("direct-server") == "Y" ? "" : "Y"); + this.update(); + } +} + +var myIdMenu; +var audioInputMenu; +class AudioInputs: Reactor.Component { + function this() { + audioInputMenu = this; + } + + function render() { + if (!this.show) return
  • ; + var inputs = handler.get_sound_inputs(); + if (is_win) inputs = ["System Sound"].concat(inputs); + if (!inputs.length) return
  • ; + inputs = ["Mute"].concat(inputs); + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Audio Input')} + + {inputs.map(function(name) { + return
  • {svg_checkmark}{translate(name)}
  • ; + })} +
    +
  • ; + } + + function get_default() { + if (is_win) return "System Sound"; + return ""; + } + + function get_value() { + return handler.get_option("audio-input") || this.get_default(); + } + + function toggleMenuState() { + var v = this.get_value(); + for (var el in $$(menu#audio-input>li)) { + var selected = el.id == v; + el.attributes.toggleClass("selected", selected); + } + } + + event click $(menu#audio-input>li) (_, me) { + var v = me.id; + if (v == this.get_value()) return; + if (v == this.get_default()) v = ""; + handler.set_option("audio-input", v); + this.toggleMenuState(); + } +} + +class MyIdMenu: Reactor.Component { + function this() { + myIdMenu = this; + } + + function render() { + return
    + {this.renderPop()} + ID{svg_menu} +
    ; + } + + function renderPop() { + return + +
  • {svg_checkmark}{translate('Enable Keyboard/Mouse')}
  • +
  • {svg_checkmark}{translate('Enable Clipboard')}
  • +
  • {svg_checkmark}{translate('Enable File Transfer')}
  • +
  • {svg_checkmark}{translate('Enable TCP Tunneling')}
  • + +
    +
  • {translate('ID/Relay Server')}
  • +
  • {translate('IP Whitelisting')}
  • +
  • {translate('Socks5 Proxy')}
  • +
    +
  • {svg_checkmark}{translate("Enable Service")}
  • + +
    +
  • {translate('About')} {" "} {handler.get_app_name()}
  • + + ; + } + + event click $(svg#menu) (_, me) { + audioInputMenu.update({ show: true }); + this.toggleMenuState(); + if (direct_server) direct_server.update(); + var menu = $(menu#config-options); + me.popup(menu); + } + + function toggleMenuState() { + for (var el in $$(menu#config-options>li)) { + if (el.id && el.id.indexOf("enable-") == 0) { + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", enabled); + el.attributes.toggleClass("line-through", !enabled); + } + } + } + + event click $(menu#config-options>li) (_, me) { + if (me.id && me.id.indexOf("enable-") == 0) { + handler.set_option(me.id, handler.get_option(me.id) == "N" ? "" : "N"); + } + if (me.id == "whitelist") { + var old_value = handler.get_option("whitelist").split(",").join("\n"); + msgbox("custom-whitelist", translate("IP Whitelisting"), "
    \ +
    " + translate("whitelist_sep") + "
    \ + \ +
    \ + ", function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + if (value) { + var values = value.split(/[\s,;\n]+/g); + for (var ip in values) { + if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) { + return translate("Invalid IP") + ": " + ip; + } + } + value = values.join("\n"); + } + if (value == old_value) return; + stdout.println("whitelist updated"); + handler.set_option("whitelist", value.replace("\n", ",")); + }, 300); + } else if (me.id == "custom-server") { + var configOptions = handler.get_options(); + var old_relay = configOptions["relay-server"] || ""; + var old_id = configOptions["custom-rendezvous-server"] || ""; + msgbox("custom-server", "ID/Relay Server", "
    \ +
    " + translate("ID Server") + ":
    \ +
    " + translate("Relay Server") + ":
    \ +
    \ + ", function(res=null) { + if (!res) return; + var id = (res.id || "").trim(); + var relay = (res.relay || "").trim(); + if (id == old_id && relay == old_relay) return; + if (id) { + var err = handler.test_if_valid_server(id); + if (err) return translate("ID Server") + ": " + err; + } + if (relay) { + var err = handler.test_if_valid_server(relay); + if (err) return translate("Relay Server") + ": " + err; + } + configOptions["custom-rendezvous-server"] = id; + configOptions["relay-server"] = relay; + handler.set_options(configOptions); + }, 240); + } else if (me.id == "socks5-server") { + var socks5 = handler.get_socks() || {}; + var old_proxy = socks5[0] || ""; + var old_username = socks5[1] || ""; + var old_password = socks5[2] || ""; + msgbox("custom-server", "Socks5 Proxy",
    +
    {translate("Hostname")}
    +
    {translate("Username")}
    +
    {translate("Password")}
    +
    + , function(res=null) { + if (!res) return; + var proxy = (res.proxy || "").trim(); + var username = (res.username || "").trim(); + var password = (res.password || "").trim(); + if (proxy == old_proxy && username == old_username && password == old_password) return; + if (proxy) { + var err = handler.test_if_valid_server(proxy); + if (err) return translate("Server") + ": " + err; + } + handler.set_socks(proxy, username, password); + }, 240); + } else if (me.id == "stop-service") { + handler.set_option("stop-service", service_stopped ? "" : "Y"); + } else if (me.id == "about") { + var name = handler.get_app_name(); + msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \ +
    Version: " + handler.get_version() + " \ +
    Privacy Statement
    \ +
    Website
    \ +
    Copyright © 2020 CarrieZ Studio \ +
    Author: Carrie \ +

    Made with heart in this chaotic world!

    \ +
    \ +
    ", function(el) { + if (el && el.attributes) { + handler.open_url(el.attributes['url']); + }; + }, 400); + } + } +} + +class App: Reactor.Component +{ + function this() { + app = this; + } + + function render() { + var is_can_screen_recording = handler.is_can_screen_recording(false); + return +
    + +
  • {translate('Refresh random password')}
  • +
  • {translate('Set your own password')}
  • +
    +
    +
    +
    {translate('Your Desktop')}
    +
    {translate('desk_tip')}
    +
    + + {key_confirmed ? : translate("Generating ...")} +
    +
    +
    {translate('Password')}
    + +
    +
    + {!is_win || handler.is_installed() ? "": } + {software_update_url ? : ""} + {is_win && handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? : ""} + {is_can_screen_recording ? "": } + {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} + {is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} + {system_error ? : ""} + {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} + {!system_error && handler.current_is_wayland() ? : ""} +
    +
    +
    +
    +
    {translate('Control Remote Desktop')}
    + +
    + + +
    +
    + +
    + +
    +
    +
    ; + } + + event click $(button#connect) { + this.newRemote("connect"); + } + + event click $(button#file-transfer) { + this.newRemote("file-transfer"); + } + + function newRemote(type) { + createNewConnect(this.remote_id.value, type); + } +} + +class InstallMe: Reactor.Component { + function render() { + return
    + +
    {translate('install_tip')}
    +
    +
    ; + } + + event click $(#install-me) { + handler.goto_install(); + } +} + +const http = function() { + + function makeRequest(httpverb) { + return function( params ) { + params.type = httpverb; + view.request(params); + }; + } + + function download(from, to, args..) + { + var rqp = { type:#get, url: from, toFile: to }; + var fn = 0; + var on = 0; + for( var p in args ) + if( p instanceof Function ) + { + switch(++fn) { + case 1: rqp.success = p; break; + case 2: rqp.error = p; break; + case 3: rqp.progress = p; break; + } + } else if( p instanceof Object ) + { + switch(++on) { + case 1: rqp.params = p; break; + case 2: rqp.headers = p; break; + } + } + view.request(rqp); + } + + return { + get: makeRequest(#get), + post: makeRequest(#post), + put: makeRequest(#put), + del: makeRequest(#delete), + download: download + }; + +}(); + +class UpgradeMe: Reactor.Component { + function render() { + var update_or_download = is_osx ? "download" : "update"; + return
    +
    {translate('Status')}
    +
    {translate('Your installation is lower version.')}
    +
    {translate('Click to upgrade')}
    +
    ; + } + + event click $(#install-me) { + handler.update_me(""); + } +} + +class UpdateMe: Reactor.Component { + function render() { + var update_or_download = "download"; // !is_win ? "download" : "update"; + return
    +
    {translate('Status')}
    +
    There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.
    +
    {translate('Click to ' + update_or_download)}
    +
    +
    ; + } + + event click $(#install-me) { + handler.open_url("https://rustdesk.com"); + return; + if (!is_win) { + handler.open_url("https://rustdesk.com"); + return; + } + var url = software_update_url + '.' + handler.get_software_ext(); + var path = handler.get_software_store_path(); + var onsuccess = function(md5) { + $(#download-percent).content(translate("Installing ...")); + handler.update_me(path); + }; + var onerror = function(err) { + msgbox("custom-error", "Download Error", "Failed to download"); + }; + var onprogress = function(loaded, total) { + if (!total) total = 5 * 1024 * 1024; + var el = $(#download-percent); + el.style.set{display: "block"}; + el.content("Downloading %" + (loaded * 100 / total)); + }; + stdout.println("Downloading " + url + " to " + path); + http.download( + url, + self.url(path), + onsuccess, onerror, onprogress); + } +} + +class SystemError: Reactor.Component { + function render() { + return
    +
    {system_error}
    +
    ; + } +} + +class TrustMe: Reactor.Component { + function render() { + return
    +
    {translate('Configuration Permissions')}
    +
    {translate('config_acc')}
    +
    {translate('Configure')}
    +
    ; + } + + event click $(#trust-me) { + handler.is_process_trusted(true); + watch_trust(); + } +} + +class CanScreenRecording: Reactor.Component { + function render() { + return
    +
    {translate('Configuration Permissions')}
    +
    {translate('config_screen')}
    +
    {translate('Configure')}
    +
    ; + } + + event click $(#screen-recording) { + handler.is_can_screen_recording(true); + watch_trust(); + } +} + +class InstallDaemon: Reactor.Component { + function render() { + return
    + +
    {translate('install_daemon_tip')}
    +
    {translate('Install')}
    +
    ; + } + + event click $(#install-me) { + handler.is_installed_daemon(true); + } +} + +class FixWayland: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Login screen using Wayland is not supported')}
    +
    {translate('Fix it')}
    +
    ({translate('Reboot required')})
    +
    ; + } + + event click $(#fix-wayland) { + handler.fix_login_wayland(); + app.update(); + } +} + +class ModifyDefaultLogin: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Current Wayland display server is not supported')}
    +
    {translate('Fix it')}
    +
    ({translate('Reboot required')})
    +
    ; + } + + event click $(#modify-default-login) { + if (var r = handler.modify_default_login()) { + msgbox("custom-error", "Error", r); + } + app.update(); + } +} + +function watch_trust() { + // not use TrustMe::update, because it is buggy + var trusted = handler.is_process_trusted(false); + var el = $(div.trust-me); + if (el) { + el.style.set { + display: trusted ? "none" : "block", + }; + } + if (trusted) { + app.update(); + return; + } + self.timer(1s, watch_trust); +} + +class PasswordEyeArea : Reactor.Component { + render() { + return +
    + + {svg_eye} +
    ; + } + + event mouseenter { + var me = this; + me.leaved = false; + me.timer(300ms, function() { + if (me.leaved) return; + me.input.value = handler.get_password(); + }); + } + + event mouseleave { + this.leaved = true; + this.input.value = "******"; + } +} + +class Password: Reactor.Component { + function render() { + return
    + + {svg_edit} +
    ; + } + + event click $(svg#edit) (_, me) { + var menu = $(menu#edit-password-context); + me.popup(menu); + } + + event click $(li#refresh-password) { + handler.update_password(""); + this.update(); + } + + event click $(li#set-password) { + var me = this; + msgbox("custom-password", translate("Set Password"), "
    \ +
    " + translate('Password') + ":
    \ +
    " + translate('Confirmation') + ":
    \ +
    \ + ", function(res=null) { + if (!res) return; + var p0 = (res.password || "").trim(); + var p1 = (res.confirmation || "").trim(); + if (p0.length < 6) { + return translate("Too short, at least 6 characters."); + } + if (p0 != p1) { + return translate("The confirmation is not identical."); + } + handler.update_password(p0); + me.update(); + }); + } +} + +class ID: Reactor.Component { + function render() { + return ; + } + + // https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm + event change { + var fid = formatId(this.value); + var d = this.value.length - (this.old_value || "").length; + this.old_value = this.value; + var start = this.xcall(#selectionStart) || 0; + var end = this.xcall(#selectionEnd); + if (fid == this.value || d <= 0 || start != end) { + return; + } + // fix Caret position + this.value = fid; + var text_after_caret = this.old_value.substr(start); + var n = fid.length - formatId(text_after_caret).length; + this.xcall(#setSelection, n, n); + } +} + +var reg = /^\d+$/; +function formatId(id) { + id = id.replace(/\s/g, ""); + if (reg.test(id) && id.length > 3) { + var n = id.length; + var a = n % 3 || 3; + var new_id = id.substr(0, a); + for (var i = a; i < n; i += 3) { + new_id += " " + id.substr(i, 3); + } + return new_id; + } + return id; +} + +event keydown (evt) { + if (view.focus && view.focus.id != 'remote_id') { + return; + } + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + var el = $(button#connect); + view.focus = el; + el.sendEvent("click"); + // simulate button click effect, windows does not have this issue + el.attributes.toggleClass("active", true); + self.timer(0.3s, function() { + el.attributes.toggleClass("active", false); + }); + } + } +} + +$(body).content(); + +function self.closing() { + // return false; // can prevent window close + var (x, y, w, h) = view.box(#rectw, #border, #screen); + handler.save_size(x, y, w, h); +} + +function self.ready() { + var r = handler.get_size(); + if (isReasonableSize(r) && r[2] > 0) { + view.move(r[0], r[1], r[2], r[3]); + } else { + centerize(800, 600); + } + if (!handler.get_remote_id()) { + view.focus = $(#remote_id); + } +} + +function checkConnectStatus() { + self.timer(1s, function() { + var tmp = !!handler.get_option("stop-service"); + if (tmp != service_stopped) { + service_stopped = tmp; + app.connect_status.update(); + myIdMenu.update(); + } + tmp = handler.get_connect_status(); + if (tmp[0] != connect_status) { + connect_status = tmp[0]; + app.connect_status.update(); + } + if (tmp[1] != key_confirmed) { + key_confirmed = tmp[1]; + app.update(); + } + if (tmp[2] && tmp[2] != my_id) { + stdout.println("id updated"); + app.update(); + } + tmp = handler.get_error(); + if (system_error != tmp) { + system_error = tmp; + app.update(); + } + tmp = handler.get_software_update_url(); + if (tmp != software_update_url) { + software_update_url = tmp; + app.update(); + } + if (handler.recent_sessions_updated()) { + stdout.println("recent sessions updated"); + app.update(); + } + checkConnectStatus(); + }); +} + +checkConnectStatus(); diff --git a/rust-rdp/rust-desk/src/ui/install.html b/rust-rdp/rust-desk/src/ui/install.html new file mode 100644 index 0000000..c86861b --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/install.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/rust-rdp/rust-desk/src/ui/install.tis b/rust-rdp/rust-desk/src/ui/install.tis new file mode 100644 index 0000000..59c1043 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/install.tis @@ -0,0 +1,45 @@ +function self.ready() { + centerize(800, 600); +} + +class Install: Reactor.Component { + function render() { + return
    +
    {translate('Installation')}
    +
    {translate('Installation Path')} {": "}
    +
    {translate('Create start menu shortcuts')}
    +
    {translate('Create desktop icon')}
    +
    {translate('End-user license agreement')}
    +
    {translate('agreement_tip')}
    +
    +
    + + + +
    +
    ; + } + + event click $(#cancel) { + view.close(); + } + + event click $(#aggrement) { + view.open_url("http://rustdesk.com/privacy"); + } + + event click $(#submit) { + for (var el in $$(button)) el.state.disabled = true; + $(progress).style.set{ display: "inline-block" }; + var args = ""; + if ($(#startmenu).value) { + args += "startmenu "; + } + if ($(#desktopicon).value) { + args += "desktopicon "; + } + view.install_me(args); + } +} + +$(body).content(); diff --git a/rust-rdp/rust-desk/src/ui/macos.rs b/rust-rdp/rust-desk/src/ui/macos.rs new file mode 100644 index 0000000..b722f38 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/macos.rs @@ -0,0 +1,145 @@ +#[cfg(target_os = "macos")] +use cocoa::{ + appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, + base::{id, nil, YES}, + foundation::{NSAutoreleasePool, NSString}, +}; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Object, Sel, BOOL}, + sel, sel_impl, +}; +use std::{ + ffi::c_void, + sync::{Arc, Mutex}, +}; + +static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; + +lazy_static::lazy_static! { + pub static ref SHOULD_OPEN_UNTITLED_FILE_CALLBACK: Arc>>> = Default::default(); +} + +trait AppHandler { + fn command(&mut self, cmd: u32); +} + +struct DelegateState { + handler: Option>, +} + +impl DelegateState { + fn command(&mut self, command: u32) { + if command == 0 { + unsafe { + let () = msg_send!(NSApp(), terminate: nil); + } + } else if let Some(inner) = self.handler.as_mut() { + inner.command(command) + } + } +} + +// https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs +unsafe fn set_delegate(handler: Option>) { + let mut decl = + ClassDecl::new("AppDelegate", class!(NSObject)).expect("App Delegate definition failed"); + decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR); + + decl.add_method( + sel!(applicationDidFinishLaunching:), + application_did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + + decl.add_method( + sel!(applicationShouldOpenUntitledFile:), + application_should_handle_open_untitled_file as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(handleMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + let decl = decl.register(); + let delegate: id = msg_send![decl, alloc]; + let () = msg_send![delegate, init]; + let state = DelegateState { handler }; + let handler_ptr = Box::into_raw(Box::new(state)); + (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + let () = msg_send![NSApp(), setDelegate: delegate]; +} + +extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) { + unsafe { + let () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; + } +} + +extern "C" fn application_should_handle_open_untitled_file( + _this: &mut Object, + _: Sel, + _sender: id, +) -> BOOL { + if let Some(callback) = SHOULD_OPEN_UNTITLED_FILE_CALLBACK.lock().unwrap().as_ref() { + callback(); + } + YES +} + +/// This handles menu items in the case that all windows are closed. +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let tag: isize = msg_send![item, tag]; + if tag == 0 { + let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); + let inner = &mut *(inner as *mut DelegateState); + (*inner).command(tag as u32); + } else if tag == 1 { + crate::run_me(Vec::::new()).ok(); + } + } +} + +pub fn make_menubar() { + unsafe { + let _pool = NSAutoreleasePool::new(nil); + set_delegate(None); + let menubar = NSMenu::new(nil).autorelease(); + let app_menu_item = NSMenuItem::new(nil).autorelease(); + menubar.addItem_(app_menu_item); + let app_menu = NSMenu::new(nil).autorelease(); + let quit_title = + NSString::alloc(nil).init_str(&format!("Quit {}", hbb_common::config::APP_NAME)); + let quit_action = sel!(handleMenuItem:); + let quit_key = NSString::alloc(nil).init_str("q"); + let quit_item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_(quit_title, quit_action, quit_key) + .autorelease(); + let () = msg_send![quit_item, setTag: 0]; + /* + if !enabled { + let () = msg_send![quit_item, setEnabled: NO]; + } + + if selected { + let () = msg_send![quit_item, setState: 1_isize]; + } + let () = msg_send![item, setTag: id as isize]; + */ + app_menu.addItem_(quit_item); + if std::env::args().len() > 1 { + let new_title = NSString::alloc(nil).init_str("New Window"); + let new_action = sel!(handleMenuItem:); + let new_key = NSString::alloc(nil).init_str("n"); + let new_item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_(new_title, new_action, new_key) + .autorelease(); + let () = msg_send![new_item, setTag: 1]; + app_menu.addItem_(new_item); + } + app_menu_item.setSubmenu_(app_menu); + NSApp().setMainMenu_(menubar); + } +} diff --git a/rust-rdp/rust-desk/src/ui/msgbox.tis b/rust-rdp/rust-desk/src/ui/msgbox.tis new file mode 100644 index 0000000..e1c2d8e --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/msgbox.tis @@ -0,0 +1,234 @@ +function translate_text(text) { + if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) { + var fds = text.split(': '); + for (var i = 0; i < fds.length; ++i) { + fds[i] = translate(fds[i]); + } + text = fds.join(': '); + } + return text; +} + +var msgboxTimerFunc = function() {} +function closeMsgbox() { + self.timer(0, msgboxTimerFunc); + $(#msgbox).content(); +} + +class MsgboxComponent: Reactor.Component { + function this(params) { + this.width = params.width; + this.height = params.height; + this.type = params.type; + this.title = params.title; + this.content = params.content; + this.remember = params.remember; + this.callback = params.callback; + this.hasRetry = params.hasRetry; + this.contentStyle = params.contentStyle; + try { this.content = translate_text(this.content); } catch (e) {} + } + + function getIcon(color) { + if (this.type == "input-password") { + return ; + } + if (this.type == "connecting") { + return ; + } + if (this.type == "success") { + return ; + } + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") { + return ; + } + return null; + } + + function getInputPasswordContent() { + var ts = this.remember ? { checked: true } : {}; + return
    +
    {translate('Please enter your password')}
    + +
    {translate('Remember password')}
    +
    ; + } + + function getContent() { + if (this.type == "input-password") { + return this.getInputPasswordContent(); + } + return this.content; + } + + function getColor() { + if (this.type == "input-password") { + return "#AD448E"; + } + if (this.type == "success") { + return "#32bea6"; + } + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") { + return "#e04f5f"; + } + return "#2C8CFF"; + } + + function hasSkip() { + return this.type.indexOf("skip") >= 0; + } + + function render() { + this.set_outline_focus(); + var color = this.getColor(); + var icon = this.getIcon(color); + var content = this.getContent(); + var hasCancel = this.type.indexOf("error") < 0 && this.type != "success" && this.type.indexOf("nocancel") < 0; + var hasOk = this.type != "connecting" && this.type.indexOf("nook") < 0; + var hasClose = this.type.indexOf("hasclose") >= 0; + var show_progress = this.type == "connecting"; + var me = this; + self.timer(0, msgboxTimerFunc); + msgboxTimerFunc = function() { + if (typeof content == "string") + me.$(#content).html = translate(content); + else + me.$(#content).content(content); + }; + self.timer(3ms, msgboxTimerFunc); + return (
    +
    +
    +
    + {translate(this.title)} +
    +
    +
    + {icon &&
    {icon}
    } +
    +
    +
    + + + {hasCancel || this.hasRetry ? : ""} + {this.hasSkip() ? : ""} + {hasOk || this.hasRetry ? : ""} + {hasClose ? : ""} +
    +
    +
    +
    ); + } + + event click $(.custom-event) (_, me) { + if (this.callback) this.callback(me); + } + + function submit() { + if (this.$(button#submit)) { + this.$(button#submit).sendEvent("click"); + } + } + + function cancel() { + if (this.$(button#cancel)) { + this.$(button#cancel).sendEvent("click"); + } + } + + event click $(button#cancel) { + this.close(); + if (this.callback) this.callback(null); + } + + event click $(button#skip) { + var values = this.getValues(); + values.skip = true; + if (this.callback) this.callback(values); + } + + event click $(button#submit) { + if (this.type == "error") { + if (this.hasRetry) { + retryConnect(true); + return; + } + } + if (this.type == "re-input-password") { + this.type = "input-password"; + this.update(); + return; + } + var values = this.getValues(); + if (this.callback) { + var err = (this.callback(values, this.show_progress) || '').trim(); + if (!err) { + this.close(); + return; + } + if (err) this.show_progress(false, err); + } else { + this.close(); + } + } + + event keydown (evt) { + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + this.submit(); + } + if (evt.keyCode == Event.VK_ESCAPE) { + this.cancel(); + } + } + } + + function show_progress(show=1, err="") { + if (show == -1) { + this.close() + return; + } + this.$(#progress).style.set { + display: show ? "inline-block" : "none" + }; + this.$(#error).text = err; + } + + function getValues() { + var values = { type: this.type }; + for (var el in this.$$(.form input)) { + values[el.attributes["name"]] = el.value; + } + for (var el in this.$$(.form textarea)) { + values[el.attributes["name"]] = el.value; + } + for (var el in this.$$(.form button)) { + values[el.attributes["name"]] = el.value; + } + if (this.type == "input-password") { + values.password = (values.password || "").trim(); + if (!values.password) { + return; + } + } + return values; + } + + function set_outline_focus() { + var me = this; + self.timer(30ms, function() { + var el = me.$(.outline-focus); + if (el) view.focus = el; + else { + el = me.$(#submit); + if (el) { + view.focus = el; + } + } + }); + } + + function close() { + closeMsgbox(); + } +} diff --git a/rust-rdp/rust-desk/src/ui/port_forward.tis b/rust-rdp/rust-desk/src/ui/port_forward.tis new file mode 100644 index 0000000..a30f698 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/port_forward.tis @@ -0,0 +1,77 @@ +class PortForward: Reactor.Component { + function render() { + var args = handler.get_args(); + var is_rdp = handler.is_rdp(); + if (is_rdp) { + this.pfs = [["", "", "RDP"]]; + args = ["rdp"]; + } else if (args.length) { + this.pfs = [args]; + } else { + this.pfs = handler.get_port_forwards(); + } + var pfs = this.pfs.map(function(pf, i) { + return + {is_rdp ? : pf[0]} + {args.length ? svg_arrow : ""} + {pf[1] || "localhost"} + {pf[2]} + {args.length ? "" : {svg_cancel}} + ; + }); + return
    + {pfs.length ?
    + {translate('Listening ...')}
    + {translate('not_close_tcp_tip')} +
    : ""} + + + + + + + {args.length ? "" : } + + + + {args.length ? "" : + + + + + + + + } + {pfs} + +
    {translate('Local Port')} + {translate('Remote Host')}{translate('Remote Port')}{translate('Action')}
    {svg_arrow}
    ; + } + + event click $(#add) () { + var port = ($(#port).value || "").toInteger() || 0; + var remote_host = $(#remote-host).value || ""; + var remote_port = ($(#remote-port).value || "").toInteger() || 0; + if (port <= 0 || remote_port <= 0) return; + handler.add_port_forward(port, remote_host, remote_port); + this.update(); + } + + event click $(#new-rdp) { + handler.new_rdp(); + } + + event click $(.remove svg) (_, me) { + var pf = this.pfs[me.parent.parent.index - 1]; + handler.remove_port_forward(pf[0]); + this.update(); + } +} + +function initializePortForward() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} diff --git a/rust-rdp/rust-desk/src/ui/remote.css b/rust-rdp/rust-desk/src/ui/remote.css new file mode 100644 index 0000000..617285e --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/remote.css @@ -0,0 +1,36 @@ +body { + margin: 0; + color: black; + overflow: scroll-indicator; +} + +div#video-wrapper { + size: *; + background: #212121; +} + +video#handler { + behavior: native-remote video; + size: *; + margin: *; + foreground-size: contain; +} + +img#cursor { + position: absolute; + display: none; + //opacity: 0.66, + //transform: scale(0.8); +} + +.goup { + transform: rotate(90deg); +} + +table#remote-folder-view { + context-menu: selector(menu#remote-folder-view); +} + +table#local-folder-view { + context-menu: selector(menu#local-folder-view); +} diff --git a/rust-rdp/rust-desk/src/ui/remote.html b/rust-rdp/rust-desk/src/ui/remote.html new file mode 100644 index 0000000..32c1409 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/remote.html @@ -0,0 +1,37 @@ + + + + + +
    +
    + + + +
    + +
    + +
    +
    +
    +
    + + diff --git a/rust-rdp/rust-desk/src/ui/remote.rs b/rust-rdp/rust-desk/src/ui/remote.rs new file mode 100644 index 0000000..b40e32a --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/remote.rs @@ -0,0 +1,1861 @@ +use crate::client::*; +use crate::common::{ + self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, +}; +use enigo::{self, Enigo, KeyboardControllable}; +use hbb_common::{ + allow_err, + config::{self, Config, PeerConfig}, + fs, log, + message_proto::{permission_info::Permission, *}, + protobuf::Message as _, + rendezvous_proto::ConnType, + sleep, + tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, + }, + Stream, +}; +use sciter::{ + dom::{ + event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, + Element, HELEMENT, + }, + make_args, + video::{video_destination, AssetPtr, COLOR_SPACE}, + Value, +}; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, Mutex, RwLock}, +}; + +type Video = AssetPtr; + +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); + static ref VIDEO: Arc>> = Default::default(); +} + +fn get_key_state(key: enigo::Key) -> bool { + #[cfg(target_os = "macos")] + if key == enigo::Key::NumLock { + return true; + } + ENIGO.lock().unwrap().get_key_state(key) +} + +static mut IS_IN: bool = false; +static mut KEYBOARD_HOOKED: bool = false; +static mut SERVER_KEYBOARD_ENABLED: bool = true; +static mut SERVER_CLIPBOARD_ENABLED: bool = true; + +#[derive(Default)] +pub struct HandlerInner { + element: Option, + sender: Option>, + thread: Option>, + close_state: HashMap, +} + +#[derive(Clone, Default)] +pub struct Handler { + inner: Arc>, + cmd: String, + id: String, + args: Vec, + lc: Arc>, +} + +impl Deref for Handler { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl sciter::EventHandler for Handler { + fn get_subscription(&mut self) -> Option { + Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) + } + + fn attached(&mut self, root: HELEMENT) { + self.write().unwrap().element = Some(Element::from(root)); + } + + fn detached(&mut self, _root: HELEMENT) { + self.write().unwrap().element = None; + self.write().unwrap().sender.take().map(|sender| { + sender.send(Data::Close).ok(); + }); + } + + // https://github.com/sciter-sdk/rust-sciter/blob/master/examples/video.rs + fn on_event( + &mut self, + _root: HELEMENT, + source: HELEMENT, + _target: HELEMENT, + code: BEHAVIOR_EVENTS, + phase: PHASE_MASK, + reason: EventReason, + ) -> bool { + if phase != PHASE_MASK::BUBBLING { + return false; + } + match code { + BEHAVIOR_EVENTS::VIDEO_BIND_RQ => { + let source = Element::from(source); + log::debug!("[video] {:?} {} ({:?})", code, source, reason); + if let EventReason::VideoBind(ptr) = reason { + if ptr.is_null() { + return true; + } + let site = AssetPtr::adopt(ptr as *mut video_destination); + log::debug!("[video] start video"); + *VIDEO.lock().unwrap() = Some(site); + self.reconnect(); + } + } + BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { + log::debug!("[video] {:?}", code); + } + BEHAVIOR_EVENTS::VIDEO_STARTED => { + log::debug!("[video] {:?}", code); + let source = Element::from(source); + use sciter::dom::ELEMENT_AREAS; + let flags = ELEMENT_AREAS::CONTENT_BOX as u32 | ELEMENT_AREAS::SELF_RELATIVE as u32; + let rc = source.get_location(flags).unwrap(); + log::debug!( + "[video] start video thread on <{}> which is about {:?} pixels", + source, + rc.size() + ); + } + BEHAVIOR_EVENTS::VIDEO_STOPPED => { + log::debug!("[video] {:?}", code); + } + _ => return false, + }; + return true; + } + + sciter::dispatch_script_call! { + fn is_xfce(); + fn get_id(); + fn get_default_pi(); + fn get_option(String); + fn t(String); + fn set_option(String, String); + fn save_close_state(String, String); + fn is_file_transfer(); + fn is_port_forward(); + fn is_rdp(); + fn login(String, bool); + fn new_rdp(); + fn send_mouse(i32, i32, i32, bool, bool, bool, bool); + fn enter(); + fn leave(); + fn ctrl_alt_del(); + fn transfer_file(); + fn tunnel(); + fn lock_screen(); + fn reconnect(); + fn get_chatbox(); + fn get_icon(); + fn get_home_dir(); + fn read_dir(String, bool); + fn remove_dir(i32, String, bool); + fn create_dir(i32, String, bool); + fn remove_file(i32, String, i32, bool); + fn read_remote_dir(String, bool); + fn send_chat(String); + fn switch_display(i32); + fn remove_dir_all(i32, String, bool); + fn confirm_delete_files(i32, i32); + fn set_no_confirm(i32); + fn cancel_job(i32); + fn send_files(i32, String, String, bool, bool); + fn get_platform(bool); + fn get_path_sep(bool); + fn get_icon_path(i32, String); + fn get_char(String, i32); + fn get_size(); + fn get_port_forwards(); + fn remove_port_forward(i32); + fn get_args(); + fn add_port_forward(i32, String, i32); + fn save_size(i32, i32, i32, i32); + fn get_view_style(); + fn get_image_quality(); + fn get_custom_image_quality(); + fn save_view_style(String); + fn save_image_quality(String); + fn save_custom_image_quality(i32, i32); + fn refresh_video(); + fn get_toggle_option(String); + fn toggle_option(String); + fn get_remember(); + } +} + +impl Handler { + pub fn new(cmd: String, id: String, args: Vec) -> Self { + let me = Self { + cmd, + id: id.clone(), + args, + ..Default::default() + }; + me.lc + .write() + .unwrap() + .initialize(id, me.is_file_transfer(), me.is_port_forward()); + me + } + + fn start_keyboard_hook(&self) { + if self.is_port_forward() || self.is_file_transfer() { + return; + } + if unsafe { KEYBOARD_HOOKED } { + return; + } + unsafe { + KEYBOARD_HOOKED = true; + } + log::info!("keyboard hooked"); + let mut me = self.clone(); + let peer = self.peer_platform(); + let is_win = peer == "Windows"; + std::thread::spawn(move || { + // This will block. + std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev + use rdev::{EventType::*, *}; + let func = move |evt: Event| { + if unsafe { !IS_IN || !SERVER_KEYBOARD_ENABLED } { + return; + } + let (key, down) = match evt.event_type { + KeyPress(k) => (k, 1), + KeyRelease(k) => (k, 0), + _ => return, + }; + let alt = get_key_state(enigo::Key::Alt); + let ctrl = get_key_state(enigo::Key::Control); + let shift = get_key_state(enigo::Key::Shift); + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => Some(ControlKey::Control), + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + me.ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Subtract), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + let mut key_event = KeyEvent::new(); + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let chr = match evt.name { + Some(ref s) => s.chars().next().unwrap_or('\0'), + _ => '\0', + }; + if chr != '\0' { + if chr == 'l' && is_win && command { + me.lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", evt); + return; + } + } + me.key_down_or_up(down, key_event, alt, ctrl, shift, command); + }; + if let Err(error) = rdev::listen(func) { + log::error!("rdev: {:?}", error); + } + }); + } + + fn get_view_style(&mut self) -> String { + return self.lc.read().unwrap().view_style.clone(); + } + + fn get_image_quality(&mut self) -> String { + return self.lc.read().unwrap().image_quality.clone(); + } + + fn get_custom_image_quality(&mut self) -> Value { + let mut v = Value::array(0); + for x in self.lc.read().unwrap().custom_image_quality.iter() { + v.push(x); + } + v + } + + #[inline] + fn save_config(&self, config: PeerConfig) { + self.lc.write().unwrap().save_config(config); + } + + fn save_view_style(&mut self, value: String) { + self.lc.write().unwrap().save_view_style(value); + } + + #[inline] + fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + fn toggle_option(&mut self, name: String) { + let msg = self.lc.write().unwrap().toggle_option(name); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + fn get_toggle_option(&mut self, name: String) -> bool { + self.lc.read().unwrap().get_toggle_option(&name) + } + + fn refresh_video(&mut self) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + + fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) { + let msg = self + .lc + .write() + .unwrap() + .save_custom_image_quality(bitrate, quantizer); + self.send(Data::Message(msg)); + } + + fn save_image_quality(&mut self, value: String) { + let msg = self.lc.write().unwrap().save_image_quality(value); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + fn get_remember(&mut self) -> bool { + self.lc.read().unwrap().remember + } + + fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + fn is_xfce(&self) -> bool { + crate::platform::is_xfce() + } + + fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { + let size = (x, y, w, h); + let mut config = self.load_config(); + if self.is_file_transfer() { + let close_state = self.read().unwrap().close_state.clone(); + let mut has_change = false; + for (k, v) in close_state { + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.options.get(&k) { + has_change = true; + if v2.is_none() { + config.options.remove(&k); + } else { + config.options.insert(k, v); + } + } + } + if size == config.size_ft && !has_change { + return; + } + config.size_ft = size; + } else if self.is_port_forward() { + if size == config.size_pf { + return; + } + config.size_pf = size; + } else { + if size == config.size { + return; + } + config.size = size; + } + self.save_config(config); + log::info!("size saved"); + } + + fn get_port_forwards(&mut self) -> Value { + let port_forwards = self.lc.read().unwrap().port_forwards.clone(); + let mut v = Value::array(0); + for (port, remote_host, remote_port) in port_forwards { + let mut v2 = Value::array(0); + v2.push(port); + v2.push(remote_host); + v2.push(remote_port); + v.push(v2); + } + v + } + + fn get_args(&mut self) -> Value { + let mut v = Value::array(0); + for x in self.args.iter() { + v.push(x); + } + v + } + + fn remove_port_forward(&mut self, port: i32) { + let mut config = self.load_config(); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != port) + .collect(); + self.save_config(config); + self.send(Data::RemovePortForward(port)); + } + + fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + let mut config = self.load_config(); + if config + .port_forwards + .iter() + .filter(|x| x.0 == port) + .next() + .is_some() + { + return; + } + let pf = (port, remote_host, remote_port); + config.port_forwards.push(pf.clone()); + self.save_config(config); + self.send(Data::AddPortForward(pf)); + } + + fn get_size(&mut self) -> Value { + let s = if self.is_file_transfer() { + self.lc.read().unwrap().size_ft + } else if self.is_port_forward() { + self.lc.read().unwrap().size_pf + } else { + self.lc.read().unwrap().size + }; + let mut v = Value::array(0); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + v + } + + fn get_id(&mut self) -> String { + self.id.clone() + } + + fn get_default_pi(&mut self) -> Value { + let mut pi = Value::map(); + let info = self.lc.read().unwrap().info.clone(); + pi.set_item("username", info.username.clone()); + pi.set_item("hostname", info.hostname.clone()); + pi.set_item("platform", info.platform.clone()); + pi + } + + fn get_option(&self, k: String) -> String { + self.lc.read().unwrap().get_option(&k) + } + + fn set_option(&self, k: String, v: String) { + self.lc.write().unwrap().set_option(k, v); + } + + fn save_close_state(&self, k: String, v: String) { + self.write().unwrap().close_state.insert(k, v); + } + + fn get_chatbox(&mut self) -> String { + #[cfg(feature = "inline")] + return super::inline::get_chatbox(); + #[cfg(not(feature = "inline"))] + return "".to_owned(); + } + + fn get_icon(&mut self) -> String { + config::ICON.to_owned() + } + + fn get_home_dir(&mut self) -> String { + fs::get_home_as_string() + } + + fn read_dir(&mut self, path: String, include_hidden: bool) -> Value { + match fs::read_dir(&fs::get_path(&path), include_hidden) { + Err(_) => Value::null(), + Ok(fd) => { + let mut m = make_fd(0, &fd.entries.to_vec(), false); + m.set_item("path", path); + m + } + } + } + + fn cancel_job(&mut self, id: i32) { + self.send(Data::CancelJob(id)); + } + + fn read_remote_dir(&mut self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_dir(ReadDir { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + + fn send_chat(&mut self, text: String) { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + fn switch_display(&mut self, display: i32) { + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + fn remove_file(&mut self, id: i32, path: String, file_num: i32, is_remote: bool) { + self.send(Data::RemoveFile((id, path, file_num, is_remote))); + } + + fn remove_dir_all(&mut self, id: i32, path: String, is_remote: bool) { + self.send(Data::RemoveDirAll((id, path, is_remote))); + } + + fn confirm_delete_files(&mut self, id: i32, file_num: i32) { + self.send(Data::ConfirmDeleteFiles((id, file_num))); + } + + fn set_no_confirm(&mut self, id: i32) { + self.send(Data::SetNoConfirm(id)); + } + + fn remove_dir(&mut self, id: i32, path: String, is_remote: bool) { + if is_remote { + self.send(Data::RemoveDir((id, path))); + } else { + fs::remove_all_empty_dir(&fs::get_path(&path)).ok(); + } + } + + fn create_dir(&mut self, id: i32, path: String, is_remote: bool) { + self.send(Data::CreateDir((id, path, is_remote))); + } + + fn send_files( + &mut self, + id: i32, + path: String, + to: String, + include_hidden: bool, + is_remote: bool, + ) { + self.send(Data::SendFiles((id, path, to, include_hidden, is_remote))); + } + + fn is_file_transfer(&self) -> bool { + self.cmd == "--file-transfer" + } + + fn is_port_forward(&self) -> bool { + self.cmd == "--port-forward" || self.is_rdp() + } + + fn is_rdp(&self) -> bool { + self.cmd == "--rdp" + } + + fn reconnect(&mut self) { + let cloned = self.clone(); + let mut lock = self.write().unwrap(); + lock.thread.take().map(|t| t.join()); + lock.thread = Some(std::thread::spawn(move || { + io_loop(cloned); + })); + } + + #[inline] + fn peer_platform(&self) -> String { + self.lc.read().unwrap().info.platform.clone() + } + + fn get_platform(&mut self, is_remote: bool) -> String { + if is_remote { + self.peer_platform() + } else { + whoami::platform().to_string() + } + } + + fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + let p = self.get_platform(is_remote); + if &p == "Windows" { + return "\\"; + } else { + return "/"; + } + } + + fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { + let mut path = Config::icon_path(); + if file_type == FileType::DirLink as i32 { + let new_path = path.join("dir_link"); + if !std::fs::metadata(&new_path).is_ok() { + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::File as i32 { + if !ext.is_empty() { + path = path.join(format!("file.{}", ext)); + } else { + path = path.join("file"); + } + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + } else if file_type == FileType::FileLink as i32 { + let new_path = path.join("file_link"); + if !std::fs::metadata(&new_path).is_ok() { + path = path.join("file"); + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::DirDrive as i32 { + if cfg!(windows) { + path = fs::get_path("C:"); + } else if cfg!(target_os = "macos") { + if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { + for entry in entries { + if let Ok(entry) = entry { + path = entry.path(); + break; + } + } + } + } + } + fs::get_string(&path) + } + + #[inline] + fn send(&mut self, data: Data) { + if let Some(ref sender) = self.read().unwrap().sender { + sender.send(data).ok(); + } + } + + fn login(&mut self, password: String, remember: bool) { + self.send(Data::Login((password, remember))); + } + + fn new_rdp(&mut self) { + self.send(Data::NewRDP); + } + + fn enter(&mut self) { + unsafe { + IS_IN = true; + } + } + + fn leave(&mut self) { + unsafe { + IS_IN = false; + } + } + + fn send_mouse( + &mut self, + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let mut msg_out = Message::new(); + let mut mouse_event = MouseEvent { + mask, + x, + y, + ..Default::default() + }; + if alt { + mouse_event.modifiers.push(ControlKey::Alt.into()); + } + if shift { + mouse_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl { + mouse_event.modifiers.push(ControlKey::Control.into()); + } + if command { + mouse_event.modifiers.push(ControlKey::Meta.into()); + } + msg_out.set_mouse_event(mouse_event); + self.send(Data::Message(msg_out)); + // on macos, ctrl + left = right, up wont emit, so we need to + // emit up myself if peer is not macos + // to-do: how about ctrl + left from win to macos + if cfg!(target_os = "macos") { + let buttons = mask >> 3; + let evt_type = mask & 0x7; + if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { + self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); + } + } + } + + fn set_cursor_data(&mut self, cd: CursorData) { + let colors = hbb_common::compress::decompress(&cd.colors); + let mut png = Vec::new(); + if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { + self.call( + "setCursorData", + &make_args!( + cd.id.to_string(), + cd.hotx, + cd.hoty, + cd.width, + cd.height, + &png[..] + ), + ); + } + } + + fn get_key_event(&self, down_or_up: i32, name: &str, code: i32) -> Option { + let mut key_event = KeyEvent::new(); + if down_or_up == 2 { + /* windows send both keyup/keydown and keychar, so here we avoid keychar + for <= 0xFF, best practice should only avoid those not on keyboard, but + for now, we have no way to test, so avoid <= 0xFF totally + */ + if code <= 0xFF { + return None; + } + key_event.set_unicode(code.clone() as _); + } else if let Some(key) = KEY_MAP.get(name) { + match key { + Key::Chr(chr) => { + key_event.set_chr(chr.clone()); + } + Key::ControlKey(key) => { + key_event.set_control_key(key.clone()); + } + _ => {} + } + } else { + if cfg!(target_os = "macos") { + match code { + 0x4C => key_event.set_control_key(ControlKey::NumpadEnter), // numpad enter + 0x69 => key_event.set_control_key(ControlKey::Snapshot), + 0x72 => key_event.set_control_key(ControlKey::Help), + 0x6E => key_event.set_control_key(ControlKey::Apps), + 0x47 => { + key_event.set_control_key(if self.peer_platform() == "Mac OS" { + ControlKey::Clear + } else { + ControlKey::NumLock + }); + } + 0x51 => key_event.set_control_key(ControlKey::Equals), + 0x2F => key_event.set_chr('.' as _), + 0x32 => key_event.set_chr('`' as _), + _ => { + log::error!("Unknown key code {}", code); + return None; + } + } + } else if cfg!(windows) { + match code { + 0x2C => key_event.set_control_key(ControlKey::Snapshot), + 0x91 => key_event.set_control_key(ControlKey::Scroll), + 0x90 => key_event.set_control_key(ControlKey::NumLock), + 0x5C => key_event.set_control_key(ControlKey::RWin), + 0x5B => key_event.set_control_key(ControlKey::Meta), + 0x5D => key_event.set_control_key(ControlKey::Apps), + 0xBE => key_event.set_chr('.' as _), + 0xC0 => key_event.set_chr('`' as _), + _ => { + log::error!("Unknown key code {}", code); + return None; + } + } + } else if cfg!(target_os = "linux") { + match code { + 65300 => key_event.set_control_key(ControlKey::Scroll), + 65421 => key_event.set_control_key(ControlKey::NumpadEnter), // numpad enter + 65407 => key_event.set_control_key(ControlKey::NumLock), + 65515 => key_event.set_control_key(ControlKey::Meta), + 65516 => key_event.set_control_key(ControlKey::RWin), + 65513 => key_event.set_control_key(ControlKey::Alt), + 65514 => key_event.set_control_key(ControlKey::RAlt), + 65508 => key_event.set_control_key(ControlKey::RControl), + 65506 => key_event.set_control_key(ControlKey::RShift), + 96 => key_event.set_chr('`' as _), + 46 => key_event.set_chr('.' as _), + 126 => key_event.set_chr('`' as _), + 33 => key_event.set_chr('1' as _), + 64 => key_event.set_chr('2' as _), + 35 => key_event.set_chr('3' as _), + 36 => key_event.set_chr('4' as _), + 37 => key_event.set_chr('5' as _), + 94 => key_event.set_chr('6' as _), + 38 => key_event.set_chr('7' as _), + 42 => key_event.set_chr('8' as _), + 40 => key_event.set_chr('9' as _), + 41 => key_event.set_chr('0' as _), + 95 => key_event.set_chr('-' as _), + 43 => key_event.set_chr('=' as _), + 123 => key_event.set_chr('[' as _), + 125 => key_event.set_chr(']' as _), + 124 => key_event.set_chr('\\' as _), + 58 => key_event.set_chr(';' as _), + 34 => key_event.set_chr('\'' as _), + 60 => key_event.set_chr(',' as _), + 62 => key_event.set_chr('.' as _), + 63 => key_event.set_chr('/' as _), + _ => { + log::error!("Unknown key code {}", code); + return None; + } + } + } else { + log::error!("Unknown key code {}", code); + return None; + } + } + Some(key_event) + } + + fn get_char(&mut self, name: String, code: i32) -> String { + if let Some(key_event) = self.get_key_event(1, &name, code) { + match key_event.union { + Some(key_event::Union::chr(chr)) => { + if let Some(chr) = std::char::from_u32(chr as _) { + return chr.to_string(); + } + } + _ => {} + } + } + "".to_owned() + } + + fn ctrl_alt_del(&mut self) { + if self.peer_platform() == "Windows" { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::CtrlAltDel); + self.key_down_or_up(1, key_event, false, false, false, false); + } else { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::Delete); + self.key_down_or_up(3, key_event, true, true, false, false); + } + } + + fn lock_screen(&mut self) { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + self.key_down_or_up(1, key_event, false, false, false, false); + } + + fn transfer_file(&mut self) { + let id = self.get_id(); + let args = vec!["--file-transfer", &id]; + if let Err(err) = crate::run_me(args) { + log::error!("Failed to spawn file transfer: {}", err); + } + } + + fn tunnel(&mut self) { + let id = self.get_id(); + let args = vec!["--port-forward", &id]; + if let Err(err) = crate::run_me(args) { + log::error!("Failed to spawn IP tunneling: {}", err); + } + } + + fn key_down_or_up( + &mut self, + down_or_up: i32, + evt: KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let mut key_event = evt; + + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if self.peer_platform() != "Mac OS" { + if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + } + if down_or_up == 1 { + key_event.down = true; + } else if down_or_up == 3 { + key_event.press = true; + } + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + log::debug!("{:?}", msg_out); + self.send(Data::Message(msg_out)); + } + + #[inline] + fn set_cursor_id(&mut self, id: String) { + self.call("setCursorId", &make_args!(id)); + } + + #[inline] + fn set_cursor_position(&mut self, cd: CursorPosition) { + self.call("setCursorPosition", &make_args!(cd.x, cd.y)); + } + + #[inline] + fn call(&self, func: &str, args: &[Value]) { + let r = self.read().unwrap(); + if let Some(ref e) = r.element { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } + } + + #[inline] + fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { + self.call("setDisplay", &make_args!(x, y, w, h)); + } +} + +const MILLI1: Duration = Duration::from_millis(1); + +async fn start_one_port_forward( + handler: Handler, + port: i32, + remote_host: String, + remote_port: i32, + receiver: mpsc::UnboundedReceiver, +) { + handler.lc.write().unwrap().port_forward = (remote_host, remote_port); + if let Err(err) = + crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver).await + { + handler.on_error(&format!("Failed to listen on {}: {}", port, err)); + } + log::info!("port forward (:{}) exit", port); +} + +#[tokio::main(flavor = "current_thread")] +async fn io_loop(handler: Handler) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + handler.write().unwrap().sender = Some(sender.clone()); + if handler.is_port_forward() { + if handler.is_rdp() { + start_one_port_forward(handler, 0, "".to_owned(), 3389, receiver).await; + } else if handler.args.len() == 0 { + let pfs = handler.lc.read().unwrap().port_forwards.clone(); + let mut queues = HashMap::>::new(); + for d in pfs { + sender.send(Data::AddPortForward(d)).ok(); + } + loop { + match receiver.recv().await { + Some(Data::AddPortForward((port, remote_host, remote_port))) => { + if port <= 0 || remote_port <= 0 { + continue; + } + let (sender, receiver) = mpsc::unbounded_channel::(); + queues.insert(port, sender); + let handler = handler.clone(); + tokio::spawn(async move { + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + ) + .await; + }); + } + Some(Data::RemovePortForward(port)) => { + if let Some(s) = queues.remove(&port) { + s.send(Data::Close).ok(); + } + } + Some(Data::Close) => { + break; + } + Some(d) => { + for (_, s) in queues.iter() { + s.send(d.clone()).ok(); + } + } + _ => {} + } + } + } else { + let port = handler.args[0].parse::().unwrap_or(0); + if handler.args.len() != 3 + || handler.args[2].parse::().unwrap_or(0) <= 0 + || port <= 0 + { + handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); + } + let remote_host = handler.args[1].clone(); + let remote_port = handler.args[2].parse::().unwrap_or(0); + start_one_port_forward(handler, port, remote_host, remote_port, receiver).await; + } + return; + } + let (video_sender, audio_sender) = start_video_audio_threads(|data: &[u8]| { + VIDEO + .lock() + .unwrap() + .as_mut() + .map(|v| v.render_frame(data).ok()); + }); + let mut remote = Remote { + handler, + video_sender, + audio_sender, + receiver, + sender, + old_clipboard: Default::default(), + read_jobs: Vec::new(), + write_jobs: Vec::new(), + remove_jobs: Default::default(), + timer: time::interval(SEC30), + last_update_jobs_status: (Instant::now(), Default::default()), + first_frame: false, + }; + remote.io_loop().await; +} + +struct RemoveJob { + files: Vec, + path: String, + sep: &'static str, + is_remote: bool, + no_confirm: bool, + last_update_job_status: Instant, +} + +impl RemoveJob { + fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { + Self { + files, + path, + sep, + is_remote, + no_confirm: false, + last_update_job_status: Instant::now(), + } + } +} + +struct Remote { + handler: Handler, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + old_clipboard: Arc>, + read_jobs: Vec, + write_jobs: Vec, + remove_jobs: HashMap, + timer: Interval, + last_update_jobs_status: (Instant, HashMap), + first_frame: bool, +} + +impl Remote { + async fn io_loop(&mut self) { + let stop_clipboard = self.start_clipboard(); + let mut last_recv_time = Instant::now(); + let conn_type = if self.handler.is_file_transfer() { + ConnType::FILE_TRANSFER + } else { + ConnType::default() + }; + match Client::start(&self.handler.id, conn_type).await { + Ok((mut peer, direct)) => { + unsafe { + SERVER_KEYBOARD_ENABLED = true; + SERVER_CLIPBOARD_ENABLED = true; + } + self.handler + .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + log::error!("Connection closed: {}", err); + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + if !self.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + log::info!("Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer"); + break; + } + } + d = self.receiver.recv() => { + if let Some(d) = d { + if !self.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + self.handler.msgbox("error", "Connection Error", "Timeout"); + break; + } + if !self.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + self.update_jobs_status(); + } else { + self.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + } + } + log::debug!("Exit io_loop of id={}", self.handler.id); + } + Err(err) => { + self.handler + .msgbox("error", "Connection Error", &err.to_string()); + } + } + if let Some(stop) = stop_clipboard { + stop.send(()).ok(); + } + unsafe { + SERVER_KEYBOARD_ENABLED = false; + SERVER_CLIPBOARD_ENABLED = false; + } + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(job) = self.remove_jobs.get_mut(&id) { + if job.no_confirm { + let file_num = (file_num + 1) as usize; + if file_num < job.files.len() { + let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); + self.sender + .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) + .ok(); + let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; + if elapsed >= 1000 { + job.last_update_job_status = Instant::now(); + } else { + return; + } + } else { + self.remove_jobs.remove(&id); + } + } + } + if let Some(err) = err { + self.handler + .call("jobError", &make_args!(id, err, file_num)); + } else { + self.handler.call("jobDone", &make_args!(id, file_num)); + } + } + + fn start_clipboard(&mut self) -> Option> { + if self.handler.is_file_transfer() || self.handler.is_port_forward() { + return None; + } + let (tx, rx) = std::sync::mpsc::channel(); + let old_clipboard = self.old_clipboard.clone(); + let tx_protobuf = self.sender.clone(); + let lc = self.handler.lc.clone(); + match ClipboardContext::new() { + Ok(mut ctx) => { + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&old_clipboard)); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit clipboard service of client"); + break; + } + _ => {} + } + if !unsafe { SERVER_CLIPBOARD_ENABLED } + || !unsafe { SERVER_KEYBOARD_ENABLED } + || lc.read().unwrap().disable_clipboard + { + continue; + } + if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { + tx_protobuf.send(Data::Message(msg)).ok(); + } + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + Some(tx) + } + + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { + match data { + Data::Close => { + return false; + } + Data::Login((password, remember)) => { + self.handler + .handle_login_from_ui(password, remember, peer) + .await; + } + Data::Message(msg) => { + allow_err!(peer.send(&msg).await); + } + Data::SendFiles((id, path, to, include_hidden, is_remote)) => { + if is_remote { + log::debug!("New job {}, write to {} from remote {}", id, to, path); + self.write_jobs + .push(fs::TransferJob::new_write(id, to, Vec::new())); + allow_err!(peer.send(&fs::new_send(id, path, include_hidden)).await); + } else { + match fs::TransferJob::new_read(id, path.clone(), include_hidden) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(job) => { + log::debug!( + "New job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + let m = make_fd(job.id(), job.files(), true); + self.handler.call("updateFolderFiles", &make_args!(m)); + let files = job.files().clone(); + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + allow_err!(peer.send(&fs::new_receive(id, to, files)).await); + } + } + } + } + Data::SetNoConfirm(id) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + job.no_confirm = true; + } + } + Data::ConfirmDeleteFiles((id, file_num)) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + let i = file_num as usize; + if i < job.files.len() { + self.handler.call( + "confirmDeleteFiles", + &make_args!(id, file_num, job.files[i].name.clone()), + ); + } + } + } + Data::RemoveDirAll((id, path, is_remote)) => { + let sep = self.handler.get_path_sep(is_remote); + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_all_files(ReadAllFiles { + id, + path: path.clone(), + include_hidden: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + self.remove_jobs + .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); + } else { + match fs::get_recursive_files(&path, true) { + Ok(entries) => { + let m = make_fd(id, &entries, true); + self.handler.call("updateFolderFiles", &make_args!(m)); + self.remove_jobs + .insert(id, RemoveJob::new(entries, path, sep, is_remote)); + } + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + } + } + } + Data::CancelJob(id) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id: id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.remove_download_file(); + fs::remove_job(id, &mut self.write_jobs); + } + fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + Data::RemoveDir((id, path)) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_dir(FileRemoveDir { + id, + path, + recursive: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } + Data::RemoveFile((id, path, file_num, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_file(FileRemoveFile { + id, + path, + file_num, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::remove_file(&path) { + Err(err) => { + self.handle_job_status(id, file_num, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, file_num, None); + } + } + } + } + Data::CreateDir((id, path, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_create(FileDirCreate { + id, + path, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::create_dir(&path) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, -1, None); + } + } + } + } + _ => {} + } + true + } + + #[inline] + fn update_job_status( + job: &fs::TransferJob, + elapsed: i32, + last_update_jobs_status: &mut (Instant, HashMap), + handler: &mut Handler, + ) { + if elapsed <= 0 { + return; + } + let transferred = job.transferred(); + let last_transferred = { + if let Some(v) = last_update_jobs_status.1.get(&job.id()) { + v.to_owned() + } else { + 0 + } + }; + last_update_jobs_status.1.insert(job.id(), transferred); + let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + handler.call( + "jobProgress", + &make_args!(job.id(), file_num, speed, job.finished_size() as f64), + ); + } + + fn update_jobs_status(&mut self) { + let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; + if elapsed >= 1000 { + for job in self.read_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { + if let Ok(msg_in) = Message::parse_from_bytes(&data) { + match msg_in.union { + Some(message::Union::video_frame(vf)) => { + if !self.first_frame { + self.first_frame = true; + self.handler.call("closeSuccess", &make_args!()); + self.handler.call("adaptSize", &make_args!()); + } + self.video_sender.send(MediaData::VideoFrame(vf)).ok(); + } + Some(message::Union::hash(hash)) => { + self.handler.handle_hash(hash, peer).await; + } + Some(message::Union::login_response(lr)) => match lr.union { + Some(login_response::Union::error(err)) => { + if !self.handler.handle_login_error(&err) { + return false; + } + } + Some(login_response::Union::peer_info(pi)) => { + self.handler.handle_peer_info(pi); + if !(self.handler.is_file_transfer() + || self.handler.is_port_forward() + || !unsafe { SERVER_CLIPBOARD_ENABLED } + || !unsafe { SERVER_KEYBOARD_ENABLED } + || self.handler.lc.read().unwrap().disable_clipboard) + { + let txt = self.old_clipboard.lock().unwrap().clone(); + if !txt.is_empty() { + let msg_out = crate::create_clipboard_msg(txt); + let sender = self.sender.clone(); + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + sender.send(Data::Message(msg_out)).ok(); + }); + } + } + } + _ => {} + }, + Some(message::Union::cursor_data(cd)) => { + self.handler.set_cursor_data(cd); + } + Some(message::Union::cursor_id(id)) => { + self.handler.set_cursor_id(id.to_string()); + } + Some(message::Union::cursor_position(cp)) => { + self.handler.set_cursor_position(cp); + } + Some(message::Union::clipboard(cb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + update_clipboard(cb, Some(&self.old_clipboard)); + } + } + Some(message::Union::file_response(fr)) => match fr.union { + Some(file_response::Union::dir(fd)) => { + let entries = fd.entries.to_vec(); + let mut m = make_fd(fd.id, &entries, fd.id > 0); + if fd.id <= 0 { + m.set_item("path", fd.path); + } + self.handler.call("updateFolderFiles", &make_args!(m)); + if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { + job.set_files(entries); + } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + job.files = entries; + } + } + Some(file_response::Union::block(block)) => { + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } + } + Some(file_response::Union::done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + _ => {} + }, + Some(message::Union::misc(misc)) => match misc.union { + Some(misc::Union::audio_format(f)) => { + self.audio_sender.send(MediaData::AudioFormat(f)).ok(); + } + Some(misc::Union::chat_message(c)) => { + self.handler.call("newMessage", &make_args!(c.text)); + } + Some(misc::Union::permission_info(p)) => { + log::info!("Change permission {:?} -> {}", p.permission, p.enabled); + match p.permission.enum_value_or_default() { + Permission::Keyboard => { + unsafe { + SERVER_KEYBOARD_ENABLED = p.enabled; + } + self.handler + .call("setPermission", &make_args!("keyboard", p.enabled)); + } + Permission::Clipboard => { + unsafe { + SERVER_CLIPBOARD_ENABLED = p.enabled; + } + self.handler + .call("setPermission", &make_args!("clipboard", p.enabled)); + } + Permission::Audio => { + self.handler + .call("setPermission", &make_args!("audio", p.enabled)); + } + } + } + Some(misc::Union::switch_display(s)) => { + self.handler.call("switchDisplay", &make_args!(s.display)); + self.video_sender.send(MediaData::Reset).ok(); + if s.width > 0 && s.height > 0 { + VIDEO.lock().unwrap().as_mut().map(|v| { + v.stop_streaming().ok(); + let ok = v.start_streaming( + (s.width, s.height), + COLOR_SPACE::Rgb32, + None, + ); + log::info!("[video] reinitialized: {:?}", ok); + }); + self.handler.set_display(s.x, s.y, s.width, s.height); + } + } + Some(misc::Union::close_reason(c)) => { + self.handler.msgbox("error", "Connection Error", &c); + return false; + } + Some(misc::Union::option_response(resp)) => { + self.handler.msgbox("warn", "Option Error", &resp.error); + } + _ => {} + }, + Some(message::Union::test_delay(t)) => { + self.handler.handle_test_delay(t, peer).await; + } + Some(message::Union::audio_frame(frame)) => { + if !self.handler.lc.read().unwrap().disable_audio { + self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); + } + } + _ => {} + } + } + true + } +} + +fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { + let mut m = Value::map(); + m.set_item("id", id); + let mut a = Value::array(0); + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = Value::map(); + e.set_item("name", entry.name.to_owned()); + let tmp = entry.entry_type.value(); + e.set_item("type", if tmp == 0 { 1 } else { tmp }); + e.set_item("time", entry.modified_time as f64); + e.set_item("size", entry.size as f64); + a.push(e); + } + if only_count { + m.set_item("num_entries", entries.len() as i32); + } else { + m.set_item("entries", a); + } + m.set_item("total_size", n as f64); + m +} + +#[async_trait] +impl Interface for Handler { + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + let retry = check_if_retry(msgtype, title, text); + self.call("msgbox_retry", &make_args!(msgtype, title, text, retry)); + } + + fn handle_login_error(&mut self, err: &str) -> bool { + self.lc.write().unwrap().handle_login_error(err, self) + } + + fn handle_peer_info(&mut self, pi: PeerInfo) { + let mut pi_sciter = Value::map(); + let username = self.lc.read().unwrap().get_username(&pi); + pi_sciter.set_item("username", username.clone()); + pi_sciter.set_item("hostname", pi.hostname.clone()); + pi_sciter.set_item("platform", pi.platform.clone()); + pi_sciter.set_item("sas_enabled", pi.sas_enabled); + if self.is_file_transfer() { + if pi.username.is_empty() { + self.on_error("No active console user logged on, please connect and logon first."); + return; + } + } else if !self.is_port_forward() { + if pi.displays.is_empty() { + self.lc.write().unwrap().handle_peer_info(username, pi); + self.msgbox("error", "Remote Error", "No Display"); + return; + } + let mut displays = Value::array(0); + for ref d in pi.displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + displays.push(display); + } + pi_sciter.set_item("displays", displays); + let mut current = pi.current_display as usize; + if current >= pi.displays.len() { + current = 0; + } + pi_sciter.set_item("current_display", current as i32); + let current = &pi.displays[current]; + self.set_display(current.x, current.y, current.width, current.height); + // https://sciter.com/forums/topic/color_spaceiyuv-crash + // Nothing spectacular in decoder – done on CPU side. + // So if you can do BGRA translation on your side – the better. + // BGRA is used as internal image format so it will not require additional transformations. + VIDEO.lock().unwrap().as_mut().map(|v| { + let ok = v.start_streaming( + (current.width as _, current.height as _), + COLOR_SPACE::Rgb32, + None, + ); + log::info!("[video] initialized: {:?}", ok); + }); + } + self.lc.write().unwrap().handle_peer_info(username, pi); + self.call("updatePi", &make_args!(pi_sciter)); + if self.is_file_transfer() { + self.call("closeSuccess", &make_args!()); + } else if !self.is_port_forward() { + self.msgbox("success", "Successful", "Connected, waiting for image..."); + } + #[cfg(windows)] + { + let mut path = std::env::temp_dir(); + path.push(&self.id); + let path = path.with_extension(config::APP_NAME.to_lowercase()); + std::fs::File::create(&path).ok(); + if let Some(path) = path.to_str() { + crate::platform::windows::add_recent_document(&path); + } + } + self.start_keyboard_hook(); + } + + async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + handle_test_delay(t, peer).await; + } +} + +impl Handler { + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err); + } +} diff --git a/rust-rdp/rust-desk/src/ui/remote.tis b/rust-rdp/rust-desk/src/ui/remote.tis new file mode 100644 index 0000000..c7d0740 --- /dev/null +++ b/rust-rdp/rust-desk/src/ui/remote.tis @@ -0,0 +1,435 @@ +var cursor_img = $(img#cursor); +is_file_transfer = handler.is_file_transfer(); +var is_port_forward = handler.is_port_forward(); +var input_blocked = false; +var display_width = 0; +var display_height = 0; +var display_origin_x = 0; +var display_origin_y = 0; +var display_scale = 1; +var keyboard_enabled = true; // server side +var clipboard_enabled = true; // server side +var audio_enabled = true; // server side +var scroll_body = $(body); + +handler.setDisplay = function(x, y, w, h) { + display_width = w; + display_height = h; + display_origin_x = x; + display_origin_y = y; + adaptDisplay(); +} + +function adaptDisplay() { + var w = display_width; + var h = display_height; + if (!w || !h) return; + var style = handler.get_view_style(); + display_scale = 1.; + var (sx, sy, sw, sh) = view.screenBox(view.windowState == View.WINDOW_FULL_SCREEN ? #frame : #workarea, #rectw); + if (sw >= w && sh > h) { + var hh = $(header).box(#height, #border); + var el = $(div#adjust-window); + if (sh > h + hh && el) { + el.style.set{ display: "block" }; + el = $(li#adjust-window); + el.style.set{ display: "block" }; + el.onClick = function() { + view.windowState == View.WINDOW_SHOWN; + var (x, y) = view.box(#position, #border, #screen); + // extra for border + var extra = 2; + view.move(x, y, w + extra, h + hh + extra); + } + } + } + if (style != "original") { + var bw = $(body).box(#width, #border); + var bh = $(body).box(#height, #border); + if (view.windowState == View.WINDOW_FULL_SCREEN) { + bw = sw; + bh = sh; + } + if (bw > 0 && bh > 0) { + var scale_x = bw.toFloat() / w; + var scale_y = bh.toFloat() / h; + var scale = scale_x < scale_y ? scale_x : scale_y; + if ((scale > 1 && style == "stretch") || + (scale < 1 && style == "shrink")) { + display_scale = scale; + w = w * scale; + h = h * scale; + } + } + } + handler.style.set { + width: w + "px", + height: h + "px", + }; +} + +// https://sciter.com/event-handling/ +// https://sciter.com/docs/content/sciter/Event.htm + +var entered = false; +if (!is_file_transfer && !is_port_forward) { + self.onKey = function(evt) { + if (!entered) return false; + // so that arrow key not move scrollbar + return true; + } +} + +var wait_window_toolbar = false; +var last_mouse_mask; +var acc_wheel_delta_x = 0; +var acc_wheel_delta_y = 0; +var last_wheel_time = 0; +var inertia_velocity_x = 0; +var inertia_velocity_y = 0; +var acc_wheel_delta_x0 = 0; +var acc_wheel_delta_y0 = 0; +var total_wheel_time = 0; +var wheeling = false; +var dragging = false; + +// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum +function resetWheel() { + acc_wheel_delta_x = 0; + acc_wheel_delta_y = 0; + last_wheel_time = 0; + inertia_velocity_x = 0; + inertia_velocity_y = 0; + acc_wheel_delta_x0 = 0; + acc_wheel_delta_y0 = 0; + total_wheel_time = 0; + wheeling = false; +} + +var INERTIA_ACCELERATION = 30; + +// not good, precision not enough to simulate accelation effect, +// seems have to use pixel based rather line based delta +function accWheel(v, is_x) { + if (wheeling) return; + var abs_v = Math.abs(v); + var max_t = abs_v / INERTIA_ACCELERATION; + for (var t = 0.1; t < max_t; t += 0.1) { + var d = Math.round((abs_v - t * INERTIA_ACCELERATION / 2) * t).toInteger(); + if (d >= 1) { + abs_v -= t * INERTIA_ACCELERATION; + if (v < 0) { + d = -d; + v = -abs_v; + } else { + v = abs_v; + } + handler.send_mouse(3, is_x ? d : 0, !is_x ? d : 0, false, false, false, false); + accWheel(v, is_x); + break; + } + } +} + +function handler.onMouse(evt) +{ + if (is_file_transfer || is_port_forward) return false; + if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { + var dy = evt.y - scroll_body.scroll(#top); + if (dy <= 1) { + if (!wait_window_toolbar) { + wait_window_toolbar = true; + self.timer(300ms, function() { + if (!wait_window_toolbar) return; + var extra = 0; + // workaround for stupid Sciter, without this, click + // event not triggered on top part of buttons on toolbar + if (is_osx) extra = 10; + if (view.windowState == View.WINDOW_FULL_SCREEN) { + $(header).style.set { + display: "block", + padding: (2 * workarea_offset + extra) + "px 0 0 0", + }; + } + wait_window_toolbar = false; + }); + } + } else { + wait_window_toolbar = false; + var h = $(header).style; + if (dy > 20 && h#display != "none") { + h.set { + display: "none", + }; + } + } + } + if (!got_mouse_control) { + if (Math.abs(evt.x - cur_local_x) > 12 || Math.abs(evt.y - cur_local_y) > 12) { + got_mouse_control = true; + } else { + return; + } + } + var mask = 0; + var wheel_delta_x; + var wheel_delta_y; + switch(evt.type) { + case Event.MOUSE_DOWN: + mask = 1; + dragging = true; + break; + case Event.MOUSE_UP: + mask = 2; + dragging = false; + break; + case Event.MOUSE_MOVE: + if (cursor_img.style#display != "none" && keyboard_enabled) { + cursor_img.style#display = "none"; + handler.style#cursor = ''; + } + break; + case Event.MOUSE_WHEEL: + // mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"]; + mask = 3; + { + var (dx, dy) = evt.wheelDeltas; + if (dx > 0) dx = 1; + else if (dx < 0) dx = -1; + if (dy > 0) dy = 1; + else if (dy < 0) dy = -1; + if (Math.abs(dx) > Math.abs(dy)) { + dy = 0; + } else { + dx = 0; + } + acc_wheel_delta_x += dx; + acc_wheel_delta_y += dy; + wheel_delta_x = acc_wheel_delta_x.toInteger(); + wheel_delta_y = acc_wheel_delta_y.toInteger(); + acc_wheel_delta_x -= wheel_delta_x; + acc_wheel_delta_y -= wheel_delta_y; + var now = getTime(); + var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0; + if (dt > 0) { + var vx = dx / dt; + var vy = dy / dt; + if (vx != 0 || vy != 0) { + inertia_velocity_x = vx; + inertia_velocity_y = vy; + } + } + acc_wheel_delta_x0 += dx; + acc_wheel_delta_y0 += dy; + total_wheel_time += dt; + if (dx == 0 && dy == 0) { + wheeling = false; + if (dt < 0.1 && total_wheel_time > 0) { + var v2 = (acc_wheel_delta_y0 / total_wheel_time) * inertia_velocity_y; + if (v2 > 0) { + v2 = Math.sqrt(v2); + inertia_velocity_y = inertia_velocity_y < 0 ? -v2 : v2; + accWheel(inertia_velocity_y, false); + } + v2 = (acc_wheel_delta_x0 / total_wheel_time) * inertia_velocity_x; + if (v2 > 0) { + v2 = Math.sqrt(v2); + inertia_velocity_x = inertia_velocity_x < 0 ? -v2 : v2; + accWheel(inertia_velocity_x, true); + } + } + resetWheel(); + } else { + wheeling = true; + } + last_wheel_time = now; + if (wheel_delta_x == 0 && wheel_delta_y == 0) return keyboard_enabled; + } + break; + case Event.MOUSE_DCLICK: // seq: down, up, dclick, up + mask = 1; + break; + case Event.MOUSE_ENTER: + entered = true; + stdout.println("enter"); + handler.enter(); + return keyboard_enabled; + case Event.MOUSE_LEAVE: + entered = false; + stdout.println("leave"); + handler.leave(); + return keyboard_enabled; + default: + return false; + } + var x = evt.x; + var y = evt.y; + if (mask != 0) { + // to gain control of the mouse, user must move mouse + if (cur_x != x || cur_y != y) { + return keyboard_enabled; + } + // save bandwidth + x = 0; + y = 0; + } else { + cur_local_x = cur_x = x; + cur_local_y = cur_y = y; + } + if (mask != 3) { + resetWheel(); + } + if (!keyboard_enabled) return false; + x = (x / display_scale).toInteger(); + y = (y / display_scale).toInteger(); + // insert down between two up, osx has this behavior for triple click + if (last_mouse_mask == 2 && mask == 2) { + handler.send_mouse((evt.buttons << 3) | 1, x + display_origin_x, y + display_origin_y, evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey); + } + last_mouse_mask = mask; + // to-do: altKey, ctrlKey etc + handler.send_mouse((evt.buttons << 3) | mask, + mask == 3 ? wheel_delta_x : x + display_origin_x, + mask == 3 ? wheel_delta_y : y + display_origin_y, + evt.altKey, + evt.ctrlKey, evt.shiftKey, evt.commandKey); + return true; +}; + +var cur_hotx = 0; +var cur_hoty = 0; +var cur_img = null; +var cur_x = 0; +var cur_y = 0; +var cur_local_x = 0; +var cur_local_y = 0; +var cursors = {}; +var image_binded; + +handler.setCursorData = function(id, hotx, hoty, width, height, colors) { + cur_hotx = hotx; + cur_hoty = hoty; + cursor_img.style.set { + width: width + "px", + height: height + "px", + }; + var img = Image.fromBytes(colors); + if (img) { + image_binded = true; + cursors[id] = [img, hotx, hoty, width, height]; + this.bindImage("in-memory:cursor", img); + if (cursor_img.style#display == 'none') { + self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); }); + } + cur_img = img; + } +} + +handler.setCursorId = function(id) { + var img = cursors[id]; + if (img) { + image_binded = true; + cur_hotx = img[1]; + cur_hoty = img[2]; + cursor_img.style.set { + width: img[3] + "px", + height: img[4] + "px", + }; + img = img[0]; + this.bindImage("in-memory:cursor", img); + if (cursor_img.style#display == 'none') { + self.timer(1ms, function() { handler.style.cursor(cur_img, cur_hotx, cur_hoty); }); + } + cur_img = img; + } +} + +var got_mouse_control = true; +handler.setCursorPosition = function(x, y) { + if (!image_binded) return; + got_mouse_control = false; + cur_x = x - display_origin_x; + cur_y = y - display_origin_y; + var x = cur_x - cur_hotx; + var y = cur_y - cur_hoty; + x *= display_scale; + y *= display_scale; + cursor_img.style.set { + left: x + "px", + top: y + "px", + }; + if (cursor_img.style#display == 'none') { + handler.style#cursor = 'none'; + cursor_img.style#display = "block"; + } +} + +function self.ready() { + var w = 960; + var h = 640; + if (is_file_transfer || is_port_forward) { + var r = handler.get_size(); + if (isReasonableSize(r) && r[2] > 0) { + view.move(r[0], r[1], r[2], r[3]); + } else { + centerize(w, h); + } + } else { + centerize(w, h); + } + if (!is_port_forward) connecting(); + if (is_file_transfer) initializeFileTransfer(); + if (is_port_forward) initializePortForward(); +} + +var workarea_offset = 0; +var size_adapted; +handler.adaptSize = function() { + if (size_adapted) return; + size_adapted = true; + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + var (fx, fy, fw, fh) = view.screenBox(#frame, #rectw); + if (is_osx) workarea_offset = sy; + var r = handler.get_size(); + if (isReasonableSize(r) && r[2] > 0) { + if (r[2] >= fw && r[3] >= fh && !is_linux) { + view.windowState = View.WINDOW_FULL_SCREEN; + stdout.println("Initialize to full screen"); + } else if (r[2] >= sw && r[3] >= sh) { + view.windowState = View.WINDOW_MAXIMIZED; + stdout.println("Initialize to full screen"); + } else { + view.move(r[0], r[1], r[2], r[3]); + } + } else { + var w = handler.box(#width, #border) + var h = handler.box(#height, #border) + if (w >= sw || h >= sh) { + view.windowState = View.WINDOW_MAXIMIZED; + return; + } + // extra for border + var extra = 2; + centerize(w + extra, handler.box(#height, #border) + h + extra); + } +} + +function self.closing() { + var (x, y, w, h) = view.box(#rectw, #border, #screen); + if (is_file_transfer) save_file_transfer_close_state(); + if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); +} + +handler.setPermission = function(name, enabled) { + if (name == "keyboard") keyboard_enabled = enabled; + if (name == "audio") audio_enabled = enabled; + if (name == "clipboard") clipboard_enabled = enabled; + input_blocked = false; + header.update(); +} + +handler.closeSuccess = function() { + // handler.msgbox("success", "Successful", "Ready to go."); + handler.msgbox("", "", ""); +} diff --git a/rust-rdp/rust-desk/src/windows.cc b/rust-rdp/rust-desk/src/windows.cc new file mode 100644 index 0000000..162c8a0 --- /dev/null +++ b/rust-rdp/rust-desk/src/windows.cc @@ -0,0 +1,392 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include // NOLINT(build/include_order) +#include + +void flog(char const *fmt, ...) +{ + FILE *h = fopen("C:\\Windows\\temp\\test_rustdesk.log", "at"); + if (!h) + return; + va_list arg; + va_start(arg, fmt); + vfprintf(h, fmt, arg); + va_end(arg); + fclose(h); +} + +// ultravnc has rdp support +// https://github.com/veyon/ultravnc/blob/master/winvnc/winvnc/service.cpp +// https://github.com/TigerVNC/tigervnc/blob/master/win/winvnc/VNCServerService.cxx +// https://blog.csdn.net/MA540213/article/details/84638264 + +DWORD GetLogonPid(DWORD dwSessionId, BOOL as_user) +{ + DWORD dwLogonPid = 0; + HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnap != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32W procEntry; + procEntry.dwSize = sizeof procEntry; + + if (Process32FirstW(hSnap, &procEntry)) + do + { + DWORD dwLogonSessionId = 0; + if (_wcsicmp(procEntry.szExeFile, as_user ? L"explorer.exe" : L"winlogon.exe") == 0 && + ProcessIdToSessionId(procEntry.th32ProcessID, &dwLogonSessionId) && + dwLogonSessionId == dwSessionId) + { + dwLogonPid = procEntry.th32ProcessID; + break; + } + } while (Process32NextW(hSnap, &procEntry)); + CloseHandle(hSnap); + } + return dwLogonPid; +} + +// if should try WTSQueryUserToken? +// https://stackoverflow.com/questions/7285666/example-code-a-service-calls-createprocessasuser-i-want-the-process-to-run-in +BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL as_user) +{ + BOOL bResult = FALSE; + DWORD Id = GetLogonPid(dwSessionId, as_user); + if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id)) + { + bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken); + CloseHandle(hProcess); + } + return bResult; +} + +// START the app as system +extern "C" +{ + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user) + { + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + if (GetSessionUserTokenWin(&hToken, dwSessionId, as_user)) + { + STARTUPINFOW si; + ZeroMemory(&si, sizeof si); + si.cb = sizeof si; + si.dwFlags = STARTF_USESHOWWINDOW; + wchar_t buf[MAX_PATH]; + wcscpy_s(buf, sizeof(buf), cmd); + PROCESS_INFORMATION pi; + LPVOID lpEnvironment = NULL; + DWORD dwCreationFlags = DETACHED_PROCESS; + if (as_user) + { + + CreateEnvironmentBlock(&lpEnvironment, // Environment block + hToken, // New token + TRUE); // Inheritance + } + if (lpEnvironment) + { + dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT; + } + if (CreateProcessAsUserW(hToken, NULL, buf, NULL, NULL, FALSE, dwCreationFlags, lpEnvironment, NULL, &si, &pi)) + { + CloseHandle(pi.hThread); + hProcess = pi.hProcess; + } + CloseHandle(hToken); + if (lpEnvironment) + DestroyEnvironmentBlock(lpEnvironment); + } + return hProcess; + } + + // Switch the current thread to the specified desktop + static bool + switchToDesktop(HDESK desktop) + { + HDESK old_desktop = GetThreadDesktop(GetCurrentThreadId()); + if (!SetThreadDesktop(desktop)) + { + return false; + } + if (!CloseDesktop(old_desktop)) + { + // + } + return true; + } + + // https://github.com/TigerVNC/tigervnc/blob/8c6c584377feba0e3b99eecb3ef33b28cee318cb/win/rfb_win32/Service.cxx + + // Determine whether the thread's current desktop is the input one + BOOL + inputDesktopSelected() + { + HDESK current = GetThreadDesktop(GetCurrentThreadId()); + HDESK input = OpenInputDesktop(0, FALSE, + DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW | + DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | + DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | GENERIC_WRITE); + if (!input) + { + return FALSE; + } + + DWORD size; + char currentname[256]; + char inputname[256]; + + if (!GetUserObjectInformation(current, UOI_NAME, currentname, sizeof(currentname), &size)) + { + CloseDesktop(input); + return FALSE; + } + if (!GetUserObjectInformation(input, UOI_NAME, inputname, sizeof(inputname), &size)) + { + CloseDesktop(input); + return FALSE; + } + CloseDesktop(input); + // flog("%s %s\n", currentname, inputname); + return strcmp(currentname, inputname) == 0 ? TRUE : FALSE; + } + + // Switch the current thread into the input desktop + bool + selectInputDesktop() + { + // - Open the input desktop + HDESK desktop = OpenInputDesktop(0, FALSE, + DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW | + DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | + DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | GENERIC_WRITE); + if (!desktop) + { + return false; + } + + // - Switch into it + if (!switchToDesktop(desktop)) + { + CloseDesktop(desktop); + return false; + } + + // *** + DWORD size = 256; + char currentname[256]; + if (GetUserObjectInformation(desktop, UOI_NAME, currentname, 256, &size)) + { + // + } + + return true; + } + + int handleMask(uint8_t *rwbuffer, const uint8_t *mask, int width, int height, int bmWidthBytes, int bmHeight) + { + auto andMask = mask; + auto andMaskSize = bmWidthBytes * bmHeight; + auto offset = height * bmWidthBytes; + auto xorMask = mask + offset; + auto xorMaskSize = andMaskSize - offset; + int doOutline = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int byte = y * bmWidthBytes + x / 8; + int bit = 7 - x % 8; + + if (byte < andMaskSize && !(andMask[byte] & (1 << bit))) + { + // Valid pixel, so make it opaque + rwbuffer[3] = 0xff; + + // Black or white? + if (xorMask[byte] & (1 << bit)) + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0xff; + else + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0; + } + else if (byte < xorMaskSize && xorMask[byte] & (1 << bit)) + { + // Replace any XORed pixels with black, because RFB doesn't support + // XORing of cursors. XORing is used for the I-beam cursor, which is most + // often used over a white background, but also sometimes over a black + // background. We set the XOR'd pixels to black, then draw a white outline + // around the whole cursor. + + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0; + rwbuffer[3] = 0xff; + + doOutline = 1; + } + else + { + // Transparent pixel + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = rwbuffer[3] = 0; + } + + rwbuffer += 4; + } + } + return doOutline; + } + + void drawOutline(uint8_t *out0, const uint8_t *in0, int width, int height, int out0_size) + { + auto in = in0; + auto out0_end = out0 + out0_size; + auto offset = width * 4 + 4; + auto out = out0 + offset; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Visible pixel? + if (in[3] > 0) + { + auto n = 4 * 3; + auto p = out - (width + 2) * 4 - 4; + // Outline above... + if (p >= out0 && p + n <= out0_end) memset(p, 0xff, n); + // ...besides... + p = out - 4; + if (p + n <= out0_end) memset(p, 0xff, n); + // ...and above + p = out + (width + 2) * 4 - 4; + if (p + n <= out0_end) memset(p, 0xff, n); + } + in += 4; + out += 4; + } + // outline is slightly larger + out += 2 * 4; + } + + // Pass 2, overwrite with actual cursor + in = in0; + out = out0 + offset; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (in[3] > 0 && out + 4 <= out0_end) + memcpy(out, in, 4); + in += 4; + out += 4; + } + out += 2 * 4; + } + } + + int ffi(unsigned v) + { + static const int MultiplyDeBruijnBitPosition[32] = + { + 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + return MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27]; + } + + int get_di_bits(uint8_t *out, HDC dc, HBITMAP hbmColor, int width, int height) + { + BITMAPV5HEADER bi; + memset(&bi, 0, sizeof(BITMAPV5HEADER)); + + bi.bV5Size = sizeof(BITMAPV5HEADER); + bi.bV5Width = width; + bi.bV5Height = -height; // Negative for top-down + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x000000FF; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x00FF0000; + bi.bV5AlphaMask = 0xFF000000; + + if (!GetDIBits(dc, hbmColor, 0, height, + out, (LPBITMAPINFO)&bi, DIB_RGB_COLORS)) + return 1; + + // We may not get the RGBA order we want, so shuffle things around + int ridx, gidx, bidx, aidx; + + ridx = ffi(bi.bV5RedMask) / 8; + gidx = ffi(bi.bV5GreenMask) / 8; + bidx = ffi(bi.bV5BlueMask) / 8; + // Usually not set properly + aidx = 6 - ridx - gidx - bidx; + + if ((bi.bV5RedMask != ((unsigned)0xff << ridx * 8)) || + (bi.bV5GreenMask != ((unsigned)0xff << gidx * 8)) || + (bi.bV5BlueMask != ((unsigned)0xff << bidx * 8))) + return 1; + + auto rwbuffer = out; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint8_t r, g, b, a; + + r = rwbuffer[ridx]; + g = rwbuffer[gidx]; + b = rwbuffer[bidx]; + a = rwbuffer[aidx]; + + rwbuffer[0] = r; + rwbuffer[1] = g; + rwbuffer[2] = b; + rwbuffer[3] = a; + + rwbuffer += 4; + } + } + return 0; + } + + void blank_screen(BOOL set) + { + if (set) + { + SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2); + } + else + { + SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)-1); + } + } + + void AddRecentDocument(PCWSTR path) + { + SHAddToRecentDocs(SHARD_PATHW, path); + } + + uint32_t get_active_user(PWSTR bufin, uint32_t nin) + { + uint32_t nout = 0; + auto id = WTSGetActiveConsoleSessionId(); + PWSTR buf = NULL; + DWORD n = 0; + if (WTSQuerySessionInformationW(NULL, id, WTSUserName, &buf, &n)) + { + if (buf) { + nout = min(nin, n); + memcpy(bufin, buf, nout); + WTSFreeMemory(buf); + } + } + return nout; + } +} // end of extern "C" diff --git a/rust-rdp/rust-server/.gitignore b/rust-rdp/rust-server/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/rust-rdp/rust-server/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/rust-rdp/rust-server/Cargo.toml b/rust-rdp/rust-server/Cargo.toml new file mode 100644 index 0000000..6fdc6c7 --- /dev/null +++ b/rust-rdp/rust-server/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rustdesk-server" +version = "1.0.0" +authors = ["rustdesk "] +edition = "2018" +description = "A remote control software." + +[dependencies] +hbb_common = { path = "libs/hbb_common" } + diff --git a/rust-rdp/rust-server/LICENSE b/rust-rdp/rust-server/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/rust-rdp/rust-server/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/rust-rdp/rust-server/README.md b/rust-rdp/rust-server/README.md new file mode 100644 index 0000000..8346961 --- /dev/null +++ b/rust-rdp/rust-server/README.md @@ -0,0 +1,24 @@ +# A working demo of RustDesk server implementation + +This is a super simple working demo implementation with only one relay connection allowed, without NAT traversal, persistence, encryption and any other advanced features. But it can be your good starting point to write your own RustDesk server program. + +## How to run + +```bash +# install rustup first, https://rustup.rs/ +IP= cargo run + +sudo docker image pull rustdesk/rustdesk-server +sudo docker run --name hbbr -p 21117:21117 -v `pwd`:/root -it --rm rustdesk/rustdesk-server hbbr -m +sudo docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -v `pwd`:/root -it --rm rustdesk/rustdesk-server hbbs -r -m +``` + +https://rustdesk.com/blog/id-relay-set/ + +https://github.com/rustdesk/rustdesk/issues/115 + +If you still need my private help, [buy me a good hotpot](https://github.com/sponsors/rustdesk/sponsorships?sponsor=rustdesk&tier_id=84000&preview=false) please! + +# Acknowledgements + +- [realm](https://github.com/zhboner/realm) diff --git a/rust-rdp/rust-server/libs/hbb_common/.gitignore b/rust-rdp/rust-server/libs/hbb_common/.gitignore new file mode 100644 index 0000000..b1cf151 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +src/protos/ diff --git a/rust-rdp/rust-server/libs/hbb_common/Cargo.toml b/rust-rdp/rust-server/libs/hbb_common/Cargo.toml new file mode 100644 index 0000000..45f03d4 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "hbb_common" +version = "0.1.0" +authors = ["rustdesk"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +protobuf = { version = "3.0.0-alpha.2" } +tokio = { version = "0.2", features = ["full"] } +tokio-util = { version = "0.3", features = ["full"] } +futures = "0.3" +bytes = "0.5" +log = "0.4" +env_logger = "0.8" +socket2 = { version = "0.3", features = ["reuseport"] } +zstd = "0.5" +quinn = {version = "0.6", optional = true } +anyhow = "1.0" +futures-util = "0.3" +directories-next = "2.0" +rand = "0.7" +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" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +mac_address = "1.1" + +[features] +quic = ["quinn"] + +[build-dependencies] +protobuf-codegen-pure = { version = "3.0.0-alpha.2" } + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser"] } + +[dev-dependencies] +toml = "0.5" +serde_json = "1.0" diff --git a/rust-rdp/rust-server/libs/hbb_common/build.rs b/rust-rdp/rust-server/libs/hbb_common/build.rs new file mode 100644 index 0000000..99dacb7 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/build.rs @@ -0,0 +1,9 @@ +fn main() { + std::fs::create_dir_all("src/protos").unwrap(); + protobuf_codegen_pure::Codegen::new() + .out_dir("src/protos") + .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .include("protos") + .run() + .expect("Codegen failed."); +} diff --git a/rust-rdp/rust-server/libs/hbb_common/protos/message.proto b/rust-rdp/rust-server/libs/hbb_common/protos/message.proto new file mode 100644 index 0000000..a8e8d34 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/protos/message.proto @@ -0,0 +1,404 @@ +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; + } +} + +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; + } +} + +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; +} + +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 { + 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; + 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 = 1; + 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; +} + +message SwitchDisplay { + int32 display = 1; + sint32 x = 2; + sint32 y = 3; + int32 width = 4; + int32 height = 5; +} + +enum Permission { + Keyboard = 1; + Clipboard = 2; + Audio = 3; +} + +message PermissionInfo { + Permission permission = 1; + bool enabled = 2; +} + +enum ImageQuality { + NotSet = 0; + Low = 2; + Balanced = 3; + Best = 4; +} + +enum BoolOption { + NotSet = 0; + No = 1; + Yes = 2; +} + +message OptionMessage { + 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; +} + +message TestDelay { + int64 time = 1; + bool from_client = 2; +} + +message PublicKey { + bytes asymmetric_value = 1; + bytes symmetric_value = 2; +} + +message SignedId { + bytes id = 1; + bytes pk = 2; +} + +message AudioFormat { + uint32 sample_rate = 1; + uint32 channels = 2; +} + +message AudioFrame { bytes data = 1; } + +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; + } +} + +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; + } +} diff --git a/rust-rdp/rust-server/libs/hbb_common/protos/rendezvous.proto b/rust-rdp/rust-server/libs/hbb_common/protos/rendezvous.proto new file mode 100644 index 0000000..c710847 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/protos/rendezvous.proto @@ -0,0 +1,134 @@ +syntax = "proto3"; +package hbb; + +message RegisterPeer { + string id = 1; + int32 serial = 2; +} + +message RegisterPeerResponse { bool request_pk = 2; } + +message PunchHoleRequest { + string id = 1; + NatType nat_type = 2; +} + +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; +} + +message RegisterPk { + string id = 1; + bytes uuid = 2; + bytes pk = 3; +} + +message RegisterPkResponse { + enum Result { + OK = 1; + UUID_MISMATCH = 2; + } + Result result = 1; +} + +message PunchHoleResponse { + bytes socket_addr = 1; + bytes pk = 2; + enum Failure { + ID_NOT_EXIST = 1; + OFFLINE = 2; + } + Failure failure = 3; + string relay_server = 4; + oneof union { + NatType nat_type = 5; + bool is_local = 6; + } +} + +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; +} + +message RelayResponse { + bytes socket_addr = 1; + string uuid = 2; + string relay_server = 3; + oneof union { + string id = 4; + bytes pk = 5; + } +} + +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; +} + +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; + } +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/bytes_codec.rs b/rust-rdp/rust-server/libs/hbb_common/src/bytes_codec.rs new file mode 100644 index 0000000..e029f1c --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/bytes_codec.rs @@ -0,0 +1,274 @@ +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> { + 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> { + 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, 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 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 = 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 = 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 = 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 = 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 = 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 = 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); + } + } +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/compress.rs b/rust-rdp/rust-server/libs/hbb_common/src/compress.rs new file mode 100644 index 0000000..a969ccf --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/compress.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use zstd::block::{Compressor, Decompressor}; + +thread_local! { + static COMPRESSOR: RefCell = RefCell::new(Compressor::new()); + static DECOMPRESSOR: RefCell = 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 { + 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 { + 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 +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/config.rs b/rust-rdp/rust-server/libs/hbb_common/src/config.rs new file mode 100644 index 0000000..72eee93 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/config.rs @@ -0,0 +1,698 @@ +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::SocketAddr, + path::{Path, PathBuf}, + sync::{Arc, Mutex, RwLock}, + time::SystemTime, +}; + +pub const APP_NAME: &str = "RustDesk"; +pub const BIND_INTERFACE: &str = "0.0.0.0"; +pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; +pub const CONNECT_TIMEOUT: u64 = 18_000; +pub const COMPRESS_LEVEL: i32 = 3; +const SERIAL: i32 = 0; +// 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(windows)] // windows, 32x32, bigger very ugly after shrink +pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAolBMVEUAAAAAcf8Acf8Acf8AcP8Acf8Acf8Acf8AcP8Acf/////9/v/7/f+gyv9wr/8Ld/8GdP/A3P+nzv+Qwf9mqv8lhv8UfP+Kvv88k/8zjv/x+P/Y6f/U5//J4f/F3/+x1P+pz/+izP+Cuv9Zov9Qnv9FmP8gg//f7f+42P96tv9fpv8ui//1+f/q8//o8v/Q5f+ax/+Zxv+VxP+Fu/9rrf8rif+x1o3FAAAACXRSTlMAv/RPTPKHioRsIqhAAAABNklEQVQ4y4WT6XaCQAyFB7S2GUD2RaxSKGDV7sv7v1on4djEluL9Ncn5biaZk1FKzSwbRmVbM2V0DRNaGD9Maq6sacBS9jRwpUTw1Ww7SEOvkwQDaeVofQeJ1nrXjgD3pTaqCDCg/xs4OBrlwUGTHvxzoHAx69Y9+Hk5oGdAsCNXSoHfEN1JYEme4KcfvO9WAmtMBNx6jIaCgSPGLbAyvKRhAEcrAxAKTSZiACu+gNSbyWwZeKdQKj91yRXWIOWZTMgATun6EtjgczNQaKMEWD0+xJ6B4AnvECVqdKwYoJ50hIPyw25AANkzpsLB00cYOIUEoHU0uVaQVcMxBwlgVZJ3Orz+3ahH6gP2tBgfYzsZmdo1fGIzRx5Irn2WxKaHeJnKtb/4cS5/PbWACd0oo/n/39/4vwGFYSxtSYV4OAAAAABJRU5ErkJggg== +"; +#[cfg(target_os = "linux")] // 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")] +pub const ORG: &str = "com.carriez"; + +type Size = (i32, i32, i32, i32); + +lazy_static::lazy_static! { + static ref CONFIG: Arc> = Arc::new(RwLock::new(Config::load())); + static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); + pub static ref ONLINE: Arc>> = Default::default(); +} +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref APP_DIR: Arc> = 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-sg.rustdesk.com", + "rs-cn.rustdesk.com", +]; +pub const RENDEZVOUS_PORT: i32 = 21116; +pub const RELAY_PORT: i32 = 21117; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Config { + #[serde(default)] + id: String, + #[serde(default)] + password: String, + #[serde(default)] + salt: String, + #[serde(default)] + key_pair: (Vec, Vec), // sk, pk + #[serde(default)] + key_confirmed: bool, + #[serde(default)] + keys_confirmed: HashMap, +} + +// more variable configs +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Config2 { + #[serde(default)] + remote_id: String, // latest used one + #[serde(default)] + size: Size, + #[serde(default)] + rendezvous_server: String, + #[serde(default)] + nat_type: i32, + #[serde(default)] + serial: i32, + + // the other scalar value must before this + #[serde(default)] + pub options: HashMap, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct PeerConfig { + #[serde(default)] + pub password: Vec, + #[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, + #[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, + + // the other scalar value must before this + #[serde(default)] + pub options: HashMap, + #[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_::("2") + } + + fn store(&self) { + Config::store_(self, "2"); + } +} + +impl Config { + fn load_( + suffix: &str, + ) -> T { + let file = Self::file_(suffix); + log::debug!("Configuration path: {}", file.display()); + let cfg = match confy::load_path(&file) { + Ok(config) => config, + Err(err) => { + log::error!("Failed to load config: {}", err); + T::default() + } + }; + if suffix.is_empty() { + log::debug!("{:?}", cfg); + } + cfg + } + + fn store_(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_::("") + } + + fn store(&self) { + Config::store_(self, ""); + } + + pub fn file() -> PathBuf { + Self::file_("") + } + + pub fn import(from: &str) { + log::info!("import {}", from); + // load first to create path + Self::load(); + crate::allow_err!(std::fs::copy(from, Self::file())); + crate::allow_err!(std::fs::copy( + from.replace(".toml", "2.toml"), + Self::file_("2") + )); + } + + pub fn save_tmp() -> String { + let _lock = CONFIG.read().unwrap(); // do not use let _, which will be dropped immediately + let path = Self::file_("2").to_str().unwrap_or("").to_owned(); + let path2 = format!("{}_tmp", path); + crate::allow_err!(std::fs::copy(&path, &path2)); + let path = Self::file().to_str().unwrap_or("").to_owned(); + let path2 = format!("{}_tmp", path); + crate::allow_err!(std::fs::copy(&path, &path2)); + path2 + } + + fn file_(suffix: &str) -> PathBuf { + let name = format!("{}{}", APP_NAME, suffix); + Self::path(name).with_extension("toml") + } + + pub fn get_home() -> PathBuf { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Self::path(""); + 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() + } + } + + fn 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; + // /var/root for root + if let Some(project) = ProjectDirs::from("", org, APP_NAME) { + let mut path = patch(project.config_dir().to_path_buf()); + path.push(p); + return path; + } + return "".into(); + } + + 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)); + return path.clone(); + } + } + #[cfg(target_os = "linux")] + { + let mut path = Self::get_home(); + path.push(format!(".local/share/logs/{}", APP_NAME)); + 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, postfix) + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + let mut path: PathBuf = format!("/tmp/{}", APP_NAME).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 { + format!("{}:0", BIND_INTERFACE).parse().unwrap() + } + + pub fn get_rendezvous_server() -> SocketAddr { + let mut rendezvous_server = Self::get_option("custom-rendezvous-server"); + if rendezvous_server.is_empty() { + rendezvous_server = CONFIG2.write().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); + } + if let Ok(addr) = crate::to_socket_addr(&rendezvous_server) { + addr + } else { + Self::get_any_listen_addr() + } + } + + pub fn get_rendezvous_servers() -> Vec { + let s = Self::get_option("custom-rendezvous-server"); + if !s.is_empty() { + return vec![s]; + } + let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; + if serial_obsolute { + let ss: Vec = 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 { + #[cfg(any(target_os = "android", target_os = "ios"))] + return None; + 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::() % 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, Vec)) { + let mut config = CONFIG.write().unwrap(); + config.key_pair = pair; + config.store(); + } + + pub fn get_key_pair() -> (Vec, Vec) { + // 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_options() -> HashMap { + CONFIG2.read().unwrap().options.clone() + } + + pub fn set_options(v: HashMap) { + let mut config = CONFIG2.write().unwrap(); + 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(); + if k == "custom-rendezvous-server" { + config.rendezvous_server = "".to_owned(); + } + 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 get_size() -> Size { + CONFIG2.read().unwrap().size + } + + pub fn set_size(x: i32, y: i32, w: i32, h: i32) { + let mut config = CONFIG2.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 = CONFIG2.write().unwrap(); + if remote_id == config.remote_id { + return; + } + config.remote_id = remote_id.into(); + config.store(); + } + + pub fn get_remote_id() -> String { + CONFIG2.read().unwrap().remote_id.clone() + } +} + +const PEERS: &str = "peers"; + +impl PeerConfig { + pub fn load(id: &str) -> PeerConfig { + let _ = 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 _ = 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::path(path).with_extension("toml") + } + + pub fn peers() -> Vec<(String, SystemTime, PeerInfoSerde)> { + if let Ok(peers) = Config::path(PEERS).read_dir() { + if let Ok(peers) = peers + .map(|res| res.map(|e| e.path())) + .collect::, _>>() + { + 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 = fs::metadata(p) + .map(|m| m.modified().unwrap_or(SystemTime::UNIX_EPOCH)) + .unwrap_or(SystemTime::UNIX_EPOCH); + let id = p + .file_stem() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + let info = PeerConfig::load(&id).info; + if info.platform.is_empty() { + fs::remove_file(&p).ok(); + } + (id, t, info) + }) + .filter(|p| !p.2.platform.is_empty()) + .collect(); + peers.sort_unstable_by(|a, b| b.1.cmp(&a.1)); + return peers; + } + } + Default::default() + } +} + +#[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()); + } +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/fs.rs b/rust-rdp/rust-server/libs/hbb_common/src/fs.rs new file mode 100644 index 0000000..1aad666 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/fs.rs @@ -0,0 +1,554 @@ +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, prelude::*}; + +pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType { + 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> { + 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> { + read_dir_recursive(&get_path(path), &get_path(""), include_hidden) +} + +#[derive(Default)] +pub struct TransferJob { + id: i32, + path: PathBuf, + files: Vec, + file_num: i32, + file: Option, + total_size: u64, + finished_size: u64, + transfered: 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) -> 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 { + 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 { + &self.files + } + + #[inline] + pub fn set_files(&mut self, files: Vec) { + 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 transfered(&self) -> u64 { + self.transfered + } + + #[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) -> 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?); + } + if block.compressed { + let tmp = decompress(&block.data); + self.file.as_mut().unwrap().write_all(&tmp).await?; + self.finished_size += tmp.len() as u64; + } else { + self.file.as_mut().unwrap().write_all(&block.data).await?; + self.finished_size += block.data.len() as u64; + } + self.transfered += block.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> { + 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 = 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.transfered += 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(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, files: Vec) -> Message { + let mut resp = FileResponse::new(); + resp.set_dir(FileDirectory { + id, + 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) -> 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) { + *jobs = jobs.drain(0..).filter(|x| x.id() != id).collect(); +} + +#[inline] +pub fn get_job(id: i32, jobs: &mut Vec) -> Option<&mut TransferJob> { + jobs.iter_mut().filter(|x| x.id() == id).next() +} + +pub async fn handle_read_jobs( + jobs: &mut Vec, + 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(()) +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/lib.rs b/rust-rdp/rust-server/libs/hbb_common/src/lib.rs new file mode 100644 index 0000000..4eb1db2 --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/lib.rs @@ -0,0 +1,215 @@ +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 socket2::{Domain, Socket, Type}; +use std::{ + fs::File, + io::{self, BufRead}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs}, + path::Path, + time::{self, SystemTime, UNIX_EPOCH}, +}; +pub use tokio; +pub use tokio_util; +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; +pub use sodiumoxide; + +#[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::delay_for(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(ms: u64, future: T) -> tokio::time::Timeout { + tokio::time::timeout(std::time::Duration::from_millis(ms), future) +} + +fn new_socket(addr: SocketAddr, tcp: bool, reuse: bool) -> Result { + let stype = { + if tcp { + Type::stream() + } else { + Type::dgram() + } + }; + let socket = match addr { + SocketAddr::V4(..) => Socket::new(Domain::ipv4(), stype, None), + SocketAddr::V6(..) => Socket::new(Domain::ipv6(), stype, 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 bahavior + #[cfg(unix)] + socket.set_reuse_port(true)?; + socket.set_reuse_address(true)?; + } + socket.bind(&addr.into())?; + Ok(socket) +} + +pub type ResultType = anyhow::Result; + +/// 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 { + 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_ne_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_ne_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_ne_bytes(padded); + let tm = (number >> 17) & (u32::max_value() as u128); + let ip = (((number >> 49) - tm) as u32).to_ne_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::() + .parse::() + .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 to_socket_addr(host: &str) -> ResultType { + let addrs: Vec = host.to_socket_addrs()?.collect(); + if addrs.is_empty() { + bail!("Failed to solve {}", host); + } + Ok(addrs[0]) +} + +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

    (filename: P) -> io::Result>> +where + P: AsRef, +{ + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + +#[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))); + } +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/quic.rs b/rust-rdp/rust-server/libs/hbb_common/src/quic.rs new file mode 100644 index 0000000..ada2acd --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/quic.rs @@ -0,0 +1,135 @@ +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; +type Receiver = mpsc::UnboundedReceiver; + +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 { + 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> { + Connection::new_for_server(&mut self.incoming).await + } +} + +pub struct Connection { + conn: quinn::Connection, + tx: quinn::SendStream, + rx: Receiver, +} + +type Value = ResultType>; + +impl Connection { + async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType> { + 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::(); + 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 { + let (tx, rx_quic) = conn.open_bi().await?; + let (tx_mpsc, rx) = mpsc::unbounded_channel::(); + 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 { + // 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(()) +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/tcp.rs b/rust-rdp/rust-server/libs/hbb_common/src/tcp.rs new file mode 100644 index 0000000..cc8432e --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/tcp.rs @@ -0,0 +1,146 @@ +use crate::{bail, bytes_codec::BytesCodec, ResultType}; +use bytes::{BufMut, Bytes, BytesMut}; +use futures::SinkExt; +use protobuf::Message; +use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; +use std::{ + io::{Error, ErrorKind}, + ops::{Deref, DerefMut}, +}; +use tokio::{ + net::{TcpListener, TcpStream, ToSocketAddrs}, + stream::StreamExt, +}; +use tokio_util::codec::Framed; + +pub struct FramedStream(Framed, Option<(Key, u64, u64)>); + +impl Deref for FramedStream { + type Target = Framed; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FramedStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FramedStream { + pub async fn new( + remote_addr: T, + local_addr: T2, + ms_timeout: u64, + ) -> ResultType { + for local_addr in local_addr.to_socket_addrs().await? { + for remote_addr in remote_addr.to_socket_addrs().await? { + if let Ok(stream) = super::timeout( + ms_timeout, + TcpStream::connect_std( + super::new_socket(local_addr, true, true)?.into_tcp_stream(), + &remote_addr, + ), + ) + .await? + { + return Ok(Self(Framed::new(stream, BytesCodec::new()), None)); + } + } + } + bail!("could not resolve to any address"); + } + + pub fn from(stream: TcpStream) -> Self { + Self(Framed::new(stream, BytesCodec::new()), None) + } + + pub fn set_raw(&mut self) { + self.0.codec_mut().set_raw(); + self.1 = None; + } + + pub fn is_secured(&self) -> bool { + self.1.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) -> ResultType<()> { + let mut msg = msg; + if let Some(key) = self.1.as_mut() { + key.1 += 1; + let nonce = Self::get_nonce(key.1); + msg = secretbox::seal(&msg, &nonce, &key.0); + } + self.0.send(bytes::Bytes::from(msg)).await?; + Ok(()) + } + + pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { + self.0.send(bytes).await?; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + let mut res = self.0.next().await; + if let Some(key) = self.1.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> { + if let Ok(res) = super::timeout(ms, self.next()).await { + res + } else { + None + } + } + + pub fn set_key(&mut self, key: Key) { + self.1 = 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_ne_bytes()); + nonce + } +} + +const DEFAULT_BACKLOG: i32 = 128; + +#[allow(clippy::never_loop)] +pub async fn new_listener(addr: T, reuse: bool) -> ResultType { + if !reuse { + Ok(TcpListener::bind(addr).await?) + } else { + for addr in addr.to_socket_addrs().await? { + let socket = super::new_socket(addr, true, true)?; + socket.listen(DEFAULT_BACKLOG)?; + return Ok(TcpListener::from_std(socket.into_tcp_listener())?); + } + bail!("could not resolve to any address"); + } +} diff --git a/rust-rdp/rust-server/libs/hbb_common/src/udp.rs b/rust-rdp/rust-server/libs/hbb_common/src/udp.rs new file mode 100644 index 0000000..0a60c7a --- /dev/null +++ b/rust-rdp/rust-server/libs/hbb_common/src/udp.rs @@ -0,0 +1,75 @@ +use crate::{bail, ResultType}; +use bytes::BytesMut; +use futures::SinkExt; +use protobuf::Message; +use std::{ + io::Error, + net::SocketAddr, + ops::{Deref, DerefMut}, +}; +use tokio::{net::ToSocketAddrs, net::UdpSocket, stream::StreamExt}; +use tokio_util::{codec::BytesCodec, udp::UdpFramed}; + +pub struct FramedSocket(UdpFramed); + +impl Deref for FramedSocket { + type Target = UdpFramed; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FramedSocket { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FramedSocket { + pub async fn new(addr: T) -> ResultType { + let socket = UdpSocket::bind(addr).await?; + Ok(Self(UdpFramed::new(socket, BytesCodec::new()))) + } + + #[allow(clippy::never_loop)] + pub async fn new_reuse(addr: T) -> ResultType { + for addr in addr.to_socket_addrs().await? { + return Ok(Self(UdpFramed::new( + UdpSocket::from_std(super::new_socket(addr, false, true)?.into_udp_socket())?, + BytesCodec::new(), + ))); + } + bail!("could not resolve to any address"); + } + + #[inline] + pub async fn send(&mut self, msg: &impl Message, addr: SocketAddr) -> ResultType<()> { + self.0 + .send((bytes::Bytes::from(msg.write_to_bytes().unwrap()), addr)) + .await?; + Ok(()) + } + + #[inline] + pub async fn send_raw(&mut self, msg: &'static [u8], addr: SocketAddr) -> ResultType<()> { + self.0.send((bytes::Bytes::from(msg), addr)).await?; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + self.0.next().await + } + + #[inline] + pub async fn next_timeout(&mut self, ms: u64) -> Option> { + if let Ok(res) = + tokio::time::timeout(std::time::Duration::from_millis(ms), self.0.next()).await + { + res + } else { + None + } + } +} diff --git a/rust-rdp/rust-server/src/main.rs b/rust-rdp/rust-server/src/main.rs new file mode 100644 index 0000000..694aa02 --- /dev/null +++ b/rust-rdp/rust-server/src/main.rs @@ -0,0 +1,129 @@ +use hbb_common::{ + bytes::BytesMut, + protobuf::Message as _, + rendezvous_proto::*, + tcp::{new_listener, FramedStream}, + tokio, + udp::FramedSocket, +}; + +#[tokio::main(basic_scheduler)] +async fn main() { + let mut socket = FramedSocket::new("0.0.0.0:21116").await.unwrap(); + let mut listener = new_listener("0.0.0.0:21116", false).await.unwrap(); + let mut rlistener = new_listener("0.0.0.0:21117", false).await.unwrap(); + let mut id_map = std::collections::HashMap::new(); + let relay_server = std::env::var("IP").unwrap(); + let mut saved_stream = None; + loop { + tokio::select! { + Some(Ok((bytes, addr))) = socket.next() => { + handle_udp(&mut socket, bytes, addr, &mut id_map).await; + } + Ok((stream, addr)) = listener.accept() => { + let mut stream = FramedStream::from(stream); + if let Some(Ok(bytes)) = stream.next_timeout(3000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::punch_hole_request(ph)) => { + println!("punch_hole_request {:?}", addr); + if let Some(addr) = id_map.get(&ph.id) { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_request_relay(RequestRelay { + relay_server: relay_server.clone(), + ..Default::default() + }); + socket.send(&msg_out, addr.clone()).await.ok(); + saved_stream = Some(stream); + } + } + Some(rendezvous_message::Union::relay_response(_)) => { + println!("relay_response {:?}", addr); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_relay_response(RelayResponse { + relay_server: relay_server.clone(), + ..Default::default() + }); + if let Some(mut stream) = saved_stream.take() { + stream.send(&msg_out).await.ok(); + if let Ok((stream_a, _)) = rlistener.accept().await { + let mut stream_a = FramedStream::from(stream_a); + stream_a.next_timeout(3_000).await; + if let Ok((stream_b, _)) = rlistener.accept().await { + let mut stream_b = FramedStream::from(stream_b); + stream_b.next_timeout(3_000).await; + relay(stream_a, stream_b, &mut socket, &mut id_map).await; + } + } + } + } + _ => {} + } + } + } + } + } + } +} + +async fn relay( + stream: FramedStream, + peer: FramedStream, + socket: &mut FramedSocket, + id_map: &mut std::collections::HashMap, +) { + let mut peer = peer; + let mut stream = stream; + peer.set_raw(); + stream.set_raw(); + loop { + tokio::select! { + Some(Ok((bytes, addr))) = socket.next() => { + handle_udp(socket, bytes, addr, id_map).await; + } + res = peer.next() => { + if let Some(Ok(bytes)) = res { + stream.send_bytes(bytes.into()).await.ok(); + } else { + break; + } + }, + res = stream.next() => { + if let Some(Ok(bytes)) = res { + peer.send_bytes(bytes.into()).await.ok(); + } else { + break; + } + }, + } + } +} + +async fn handle_udp( + socket: &mut FramedSocket, + bytes: BytesMut, + addr: std::net::SocketAddr, + id_map: &mut std::collections::HashMap, +) { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::register_peer(rp)) => { + println!("register_peer {:?}", addr); + id_map.insert(rp.id, addr); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_register_peer_response(RegisterPeerResponse::new()); + socket.send(&msg_out, addr).await.ok(); + } + Some(rendezvous_message::Union::register_pk(_)) => { + println!("register_pk {:?}", addr); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_register_pk_response(RegisterPkResponse { + result: register_pk_response::Result::OK.into(), + ..Default::default() + }); + socket.send(&msg_out, addr).await.ok(); + } + _ => {} + } + } +}