Compare commits

..

127 Commits

Author SHA1 Message Date
Ferdinand Schober
0119f8870e update screenshots 2024-11-09 14:03:25 +01:00
Ferdinand Schober
8b0a797d75 update connection instructions 2024-11-09 13:40:04 +01:00
Ferdinand Schober
3913fa8a38 update README 2024-11-09 12:35:45 +01:00
Ferdinand Schober
682a7b65bd cleanup 2024-11-09 12:03:50 +01:00
Ferdinand Schober
ae58714681 fix initial activation 2024-11-08 23:03:12 +01:00
Ferdinand Schober
875a31907a cleanup 2024-11-08 22:45:11 +01:00
Ferdinand Schober
6cff691a38 reduce visibility of functions 2024-11-08 21:27:51 +01:00
Ferdinand Schober
f9217fe268 simplify service 2024-11-08 18:45:38 +01:00
Ferdinand Schober
809fbbf453 simplify Service 2024-11-08 18:40:10 +01:00
Ferdinand Schober
6dfd5d97a4 remove dependency on service 2024-11-08 18:33:47 +01:00
Ferdinand Schober
156be7a79d remove unnecessary struct 2024-11-08 18:25:26 +01:00
Ferdinand Schober
7d6a08141c fix initial capture creation 2024-11-08 18:04:29 +01:00
Ferdinand Schober
37d35d3eea remove inactive emulation handles 2024-11-08 17:53:48 +01:00
Ferdinand Schober
2be9fbe2a4 cleanup emulation 2024-11-08 17:49:47 +01:00
Ferdinand Schober
9837e3d9d2 cleanup 2024-11-08 17:33:28 +01:00
Ferdinand Schober
d30bd81fa9 simplify 2024-11-08 17:20:32 +01:00
Ferdinand Schober
ef2b2a773e remove dependency on service from capture 2024-11-08 17:18:51 +01:00
Ferdinand Schober
46044d0796 no need for a Cell 2024-11-08 14:07:34 +01:00
Ferdinand Schober
ff2a2cb1df move enter hook logic to service (where it belongs) 2024-11-08 13:59:38 +01:00
Ferdinand Schober
88693093c6 revert to 1px 2024-11-08 13:59:20 +01:00
Ferdinand Schober
1f1d3f2ccf properly terminate input capture 2024-11-07 21:21:06 +01:00
Ferdinand Schober
60fab82423 fix update pos 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4d835a5190 fix potential unterminated emulation session 2024-11-07 21:21:06 +01:00
Ferdinand Schober
707cef154e cleanup 2024-11-07 21:21:06 +01:00
Ferdinand Schober
6bc00bd84d terminate capture in all instances 2024-11-07 21:21:06 +01:00
Ferdinand Schober
bad8c5a81b update pos when entered from a different side 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b73c5991ad ignore port change if same port 2024-11-07 21:21:06 +01:00
Ferdinand Schober
39d5d1c03b clippy 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b94a453703 use a guard struct to send enable / disable events 2024-11-07 21:21:06 +01:00
Ferdinand Schober
9a4206dab7 simplify logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
677f662f4f fix disconnect 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1d45d14992 fix clippy lint 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b87fe4164a remove key-gen script 2024-11-07 21:21:06 +01:00
Ferdinand Schober
151fbe37de fix capture termination when in client creation 2024-11-07 21:21:06 +01:00
Ferdinand Schober
095937e943 fix service exit 2024-11-07 21:21:06 +01:00
Ferdinand Schober
5461c6a00e resolve dns when activating 2024-11-07 21:21:06 +01:00
Ferdinand Schober
aa4097b4ee restructure resolver + capture 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1489719e01 handle port change 2024-11-07 21:21:06 +01:00
Ferdinand Schober
2291cf25a3 improve error handling 2024-11-07 21:21:06 +01:00
Ferdinand Schober
a5727801e3 fix connection logic
dont abort after first ip,
add timeout logic
2024-11-07 21:21:06 +01:00
Ferdinand Schober
92398db918 update log msg 2024-11-07 21:21:06 +01:00
Ferdinand Schober
847826e8ba create config directory if it does not exist 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f62cd3d11c (hopefully) fix ping logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
9dc4e95a4d avoid duplicating incoming handles 2024-11-07 21:21:06 +01:00
Ferdinand Schober
87304744ad fix release logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0277b6b4d3 add release capture log message 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b6c2cfe8a6 fix clippy 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4a64a97273 release capture only if no active capture at pos 2024-11-07 21:21:06 +01:00
Ferdinand Schober
06d4e8d836 implementing release logic for one-way connection 2024-11-07 21:21:06 +01:00
Ferdinand Schober
44e34918bb fix config path in windows 2024-11-07 21:21:06 +01:00
Ferdinand Schober
52c4e1cd7f silence server_name warning 2024-11-07 21:21:06 +01:00
Ferdinand Schober
a39f9172d5 load authorized keys from config 2024-11-07 21:21:06 +01:00
Ferdinand Schober
c205371dfc [wip] one-way control 2024-11-07 21:21:06 +01:00
Ferdinand Schober
81ca510d12 put hostname + port into one row 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4b6a7d5410 fix tooltip text 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1979f203d2 release capture if emulation disabled on target 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f6a7010d17 merge incoming conns and "authorized" section 2024-11-07 21:21:06 +01:00
Ferdinand Schober
cf4a06a44a make private key file inaccessible to other users 2024-11-07 21:21:06 +01:00
Ferdinand Schober
49e139198a remove log messages 2024-11-07 21:21:06 +01:00
Ferdinand Schober
d7c8d79b94 fix ui reversed 2024-11-07 21:21:06 +01:00
Ferdinand Schober
2ed3624505 fix fingerprint add 2024-11-07 21:21:06 +01:00
Ferdinand Schober
c75a085e78 use certificate when connecting 2024-11-07 21:21:06 +01:00
Ferdinand Schober
790e1b927f gen or load certificate 2024-11-07 21:21:06 +01:00
Ferdinand Schober
61d4a6ceac enable client cert auth 2024-11-07 21:21:06 +01:00
Ferdinand Schober
dedf59d1c7 add logic for fingerprint verification 2024-11-07 21:21:06 +01:00
Ferdinand Schober
1c7490c58d impl fingerprint ui logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0038178f0d fingerprint add ui mockup 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f056f790c7 add ui mockup 2024-11-07 21:21:06 +01:00
Ferdinand Schober
c480bb6ea6 move cert verification to server 2024-11-07 21:21:06 +01:00
Ferdinand Schober
94ece6dfe6 verify_peer_certificate fn 2024-11-07 21:21:06 +01:00
Ferdinand Schober
44707bd786 update logs 2024-11-07 21:21:06 +01:00
Ferdinand Schober
640447ecaa fingerprints 2024-11-07 21:21:06 +01:00
Ferdinand Schober
859bec5df8 remove some debug logs 2024-11-07 21:21:06 +01:00
Ferdinand Schober
ed7771691f dont activate multiple times 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b16f3b272a fix double release 2024-11-07 21:21:06 +01:00
Ferdinand Schober
fa63a7e9e5 fix active 2024-11-07 21:21:06 +01:00
Ferdinand Schober
f825f3be53 fix ping logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
22bf7dc8c2 use addr from accept 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4bbabf1e0a fix compilation 2024-11-07 21:21:06 +01:00
Ferdinand Schober
64c4480e93 enter acknowledgement 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b41ee94a2b hotfix accept bug
https://github.com/webrtc-rs/webrtc/issues/614
2024-11-07 21:21:06 +01:00
Ferdinand Schober
3e62739f7e debug connections 2024-11-07 21:21:06 +01:00
Ferdinand Schober
9593b97fbe formatting 2024-11-07 21:21:06 +01:00
Ferdinand Schober
ae5f3e5303 remove unnecessary let 2024-11-07 21:21:06 +01:00
Ferdinand Schober
eb0022995c fix deadlock 2024-11-07 21:21:06 +01:00
Ferdinand Schober
874fba670d fix 2024-11-07 21:21:06 +01:00
Ferdinand Schober
fd5b8ed25e release capture 2024-11-07 21:21:06 +01:00
Ferdinand Schober
bc192bdf6b ping server 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b747c7252b prevent releasing keys logspam 2024-11-07 21:21:06 +01:00
Ferdinand Schober
89a4672d36 fix clippy 2024-11-07 21:21:06 +01:00
Ferdinand Schober
89ab328fc9 fix termination 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b710fe273f fix comment 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e2479762ab fix capture reenable 2024-11-07 21:21:06 +01:00
Ferdinand Schober
2c9ecf98d0 update log message 2024-11-07 21:21:06 +01:00
Ferdinand Schober
62de35de9d fix connection logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
7baacf4546 debounce release log 2024-11-07 21:21:06 +01:00
Ferdinand Schober
7f0815facf improve logging, remove failed connection 2024-11-07 21:21:06 +01:00
Ferdinand Schober
7186ae40b1 resolver: encapsulate channel logic 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0bf0173971 add release bind check 2024-11-07 21:21:06 +01:00
Ferdinand Schober
17f3d7237c remove unused error 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e427439099 connect asynchronously 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0fe5416ac5 dont use remote_addr 2024-11-07 21:21:06 +01:00
Ferdinand Schober
937652ac44 finish up server 2024-11-07 21:21:06 +01:00
Ferdinand Schober
ad8c92cfbe capture stuffs 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e4a7f0b4fc connection things 2024-11-07 21:21:06 +01:00
Ferdinand Schober
28e4895418 impl emulation stuff 2024-11-07 21:21:06 +01:00
Ferdinand Schober
b62540d9be asdf 2024-11-07 21:21:06 +01:00
Ferdinand Schober
90561744a0 asdf 2024-11-07 21:21:06 +01:00
Ferdinand Schober
4c000fa6d4 test fix port 2024-11-07 21:21:06 +01:00
Ferdinand Schober
e7166d4373 test selfsigned 2024-11-07 21:21:06 +01:00
Ferdinand Schober
79bc64e56e start working on encryption 2024-11-07 21:21:06 +01:00
Ferdinand Schober
0d074e19f1 try fixing CI 2024-11-07 20:53:27 +01:00
Ferdinand Schober
4703a4c947 chore: Release 2024-11-07 12:49:59 +01:00
Ferdinand Schober
a870a9e3a9 split features for emulation and capture backends 2024-11-07 12:43:42 +01:00
Ferdinand Schober
1433a3021b fix conditional compilation for xdp only build 2024-11-07 12:21:38 +01:00
Ferdinand Schober
ef92bd69d9 Update README.md 2024-11-07 02:37:40 +01:00
Ferdinand Schober
526d8004b6 Update README.md 2024-11-07 02:36:11 +01:00
Ferdinand Schober
fef702ffdd Update README.md (#229) 2024-11-07 02:35:32 +01:00
Ferdinand Schober
41ab25cc19 update screenshots (#228) 2024-11-07 01:18:55 +01:00
Ferdinand Schober
66456f18f1 update core-graphics / foundation (#227) 2024-11-07 01:15:35 +01:00
Ferdinand Schober
1d25dfbe50 upgrade ashpd + reis (#226) 2024-11-07 00:38:26 +01:00
Ferdinand Schober
9d28fe6c7b bump dependencies 2024-11-06 23:53:42 +01:00
byquanton
de674c631a Rename Synergy Community Edition to Deskflow (#225) 2024-11-06 13:46:19 +01:00
Ferdinand Schober
cd2cabf25b adapt config backend options to cli arg names 2024-11-06 12:14:47 +01:00
Ferdinand Schober
71f7e2e5e0 fix left over capture barrier 2024-11-05 14:32:39 +01:00
Ferdinand Schober
e7a18b9696 recreate wl_{pointer,keyboard} on capability event (#222)
this should fix #88
fixes #123
2024-11-04 22:45:57 +01:00
Ferdinand Schober
7496015d8d macos: implement client side modifier events (#219)
closes #198
closes #199
2024-10-27 08:41:26 +01:00
56 changed files with 4938 additions and 2165 deletions

View File

@@ -3,7 +3,7 @@ name: "Tagged Release"
on:
push:
tags:
- "v*"
- v**
jobs:
linux-release-build:
@@ -114,7 +114,7 @@ jobs:
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
- name: "Create Release"
- name: Create Release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"

5
.gitignore vendored
View File

@@ -4,4 +4,7 @@
.vs/
.vscode/
.direnv/
result
result
*.pem
*.csr
extfile.conf

1968
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,23 +12,25 @@ members = [
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.9.1"
version = "0.10.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[profile.release]
strip = true
codegen-units = 1
lto = "fat"
strip = true
panic = "abort"
[dependencies]
input-event = { path = "input-event", version = "0.2.1" }
input-emulation = { path = "input-emulation", version = "0.2.1", default-features = false }
input-capture = { path = "input-capture", version = "0.2.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.1.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.1.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.1.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.1.0" }
input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
hickory-resolver = "0.24.1"
toml = "0.8"
@@ -49,17 +51,37 @@ tokio = { version = "1.32.0", features = [
futures = "0.3.28"
clap = { version = "4.4.11", features = ["derive"] }
slab = "0.4.9"
thiserror = "1.0.61"
thiserror = "2.0.0"
tokio-util = "0.7.11"
local-channel = "0.1.5"
webrtc-dtls = { version = "0.10.0", features = ["pem"] }
webrtc-util = "0.9.0"
rustls = { version = "0.23.12", default-features = false, features = [
"std",
"ring",
] }
rcgen = "0.13.1"
sha2 = "0.10.8"
[target.'cfg(unix)'.dependencies]
libc = "0.2.148"
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
wayland = ["input-capture/wayland", "input-emulation/wayland"]
x11 = ["input-capture/x11", "input-emulation/x11"]
xdg_desktop_portal = ["input-emulation/xdg_desktop_portal"]
libei = ["input-event/libei", "input-capture/libei", "input-emulation/libei"]
default = [
"gtk",
"layer_shell_capture",
"x11_capture",
"libei_capture",
"wlroots_emulation",
"libei_emulation",
"rdp_emulation",
"x11_emulation",
]
gtk = ["dep:lan-mouse-gtk"]
layer_shell_capture = ["input-capture/layer_shell"]
x11_capture = ["input-capture/x11"]
libei_capture = ["input-event/libei", "input-capture/libei"]
libei_emulation = ["input-event/libei", "input-emulation/libei"]
wlroots_emulation = ["input-emulation/wlroots"]
x11_emulation = ["input-emulation/x11"]
rdp_emulation = ["input-emulation/remote_desktop_portal"]

354
README.md
View File

@@ -1,78 +1,60 @@
# Lan Mouse
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple pcs with a single set of mouse and keyboard.
Lan Mouse is a *cross-platform* mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple PCs via a single set of mouse and keyboard.
This is also known as a Software KVM switch.
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
Goal of this project is to be an open-source alternative to proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/)
and other open source tools like [Deskflow](https://github.com/deskflow/deskflow) or [Input Leap](https://github.com/input-leap) (Synergy fork).
Focus lies on performance, ease of use and a maintainable implementation that can be expanded to support additional backends for e.g. Android, iOS, ... in the future.
***blazingly fast™*** because it's written in rust.
- _Now with a gtk frontend_
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/feschber/lan-mouse/assets/40996949/d6318340-f811-4e16-9d6e-d1b79883c709">
<img alt="Screenshot of Lan-Mouse" srcset="https://github.com/feschber/lan-mouse/assets/40996949/016a06a9-76db-4951-9dcc-127d012c59df">
<source media="(prefers-color-scheme: dark)" srcset="/screenshots/dark.png?raw=true">
<source media="(prefers-color-scheme: light)" srcset="/screenshots/light.png?raw=true">
<img alt="Screenshot of Lan-Mouse" srcset="/screenshots/dark.png">
</picture>
Goal of this project is to be an open-source replacement for proprietary tools like [Synergy 2/3](https://symless.com/synergy), [Share Mouse](https://www.sharemouse.com/de/).
Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
***blazingly fast™*** because it's written in rust.
For an alternative (with slightly different goals) you may check out [Synergy 1 Community Edition](https://github.com/symless/synergy) or [Input Leap](https://github.com/input-leap) (Synergy fork).
> [!WARNING]
> Since this tool has gained a bit of popularity over the past couple of days:
>
> All network traffic is currently **unencrypted** and sent in **plaintext**.
>
> A malicious actor with access to the network could read input data or send input events with spoofed IPs to take control over a device.
>
> Therefore you should only use this tool in your local network with trusted devices for now
> and I take no responsibility for any leakage of data!
## Encryption
Lan Mouse encrypts all network traffic using the DTLS implementation provided by [WebRTC.rs](https://github.com/webrtc-rs/webrtc).
There are currently no mitigations in place for timing side-channel attacks.
## OS Support
The following table shows support for input emulation (to emulate events received from other clients) and
input capture (to send events *to* other clients) on different operating systems:
Most current desktop environments and operating systems are fully supported, this includes
- GNOME >= 45
- KDE Plasma >= 6.1
- Most wlroots based compositors, including Sway (>= 1.8), Hyprland and Wayfire
- Windows
- MacOS
| OS / Desktop Environment | input emulation | input capture |
|---------------------------|--------------------------|--------------------------------------|
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: (starting at GNOME 45) |
| Windows | :heavy_check_mark: | :heavy_check_mark: |
| X11 | :heavy_check_mark: | WIP |
| MacOS | :heavy_check_mark: | WIP |
### Caveats / Known Issues
> [!Important]
> Gnome -> Sway only partially works (modifier events are not handled correctly)
> [!Important]
> **Wayfire**
> - **X11** currently only has support for input emulation, i.e. can only be used on the receiving end.
>
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> - **Sway / wlroots**: Wlroots based compositors without libei support on the receiving end currently do not handle modifier events on the client side.
> This results in CTRL / SHIFT / ALT / SUPER keys not working with a sending device that is NOT using the `layer-shell` backend
>
> - **Wayfire**: If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
> [!Important]
> The mouse cursor will be invisible when sending input to a Windows system if
>
> - **Windows**: The mouse cursor will be invisible when sending input to a Windows system if
> there is no real mouse connected to the machine.
For more detailed information about os support see [Detailed OS Support](#detailed-os-support)
## Installation
### Install via cargo
```sh
cargo install lan-mouse
```
### Download from Releases
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux
<details>
<summary>Arch Linux</summary>
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
@@ -80,46 +62,37 @@ Lan Mouse can be installed from the [official repositories](https://archlinux.or
pacman -S lan-mouse
```
It is also available on the AUR:
The prerelease version (following `main`) is available on the AUR:
```sh
# git version (includes latest changes)
paru -S lan-mouse-git
# alternatively
paru -S lan-mouse-bin
```
</details>
<details>
<summary>Nix (OS)</summary>
### Nix
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
- flake: [README.md](./nix/README.md)
</details>
### Building from source with Nix
In the root of the project to build Lan Mouse, run
```sh
nix-build
```
You can find the executable in `result/bin/lan-mouse` .
### Manual Installation
<details>
<summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies).
Build in release mode:
```sh
cargo build --release
```
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
Run directly:
```sh
cargo run --release
```
Alternatively, the `lan-mouse` binary can be compiled from source (see below).
Install the files:
### Installing desktop file, app icon and firewall rules (optional)
```sh
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
# install lan-mouse (replace path/to/ with the correct path)
sudo cp path/to/lan-mouse /usr/local/bin/
# install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
@@ -137,17 +110,34 @@ sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings
```
### Conditional Compilation
Instead of downloading from the releases, the `lan-mouse` binary
can be easily compiled via cargo or nix:
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
### Compiling and installing manually:
```sh
# compile in release mode
cargo build --release
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
```
This is possible through
### Compiling and installing via cargo:
```sh
# will end up in ~/.cargo/bin
cargo install lan-mouse
```
### Compiling and installing via nix:
```sh
# you can find the executable in result/bin/lan-mouse
nix-build
```
### Conditional compilation
Support for other platforms is omitted automatically based on the active
rust toolchain.
Additionally, available backends and frontends can be configured manually via
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
@@ -156,14 +146,17 @@ an executable with just support for wayland:
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
</details>
## Installing Dependencies
## Installing Dependencies for Development / Compiling from Source
<details>
<summary>MacOS</summary>
```sh
brew install libadwaita
brew install libadwaita pkg-config
```
</details>
@@ -191,10 +184,22 @@ sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
</details>
<details>
<summary>Windows</summary>
<summary>Nix</summary>
> [!NOTE]
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
```sh
nix-shell .
```
</details>
<details>
<summary>Nix (flake)</summary>
```sh
nix develop
```
</details>
<details>
<summary>Windows</summary>
- First install [Rust](https://www.rust-lang.org/tools/install).
@@ -226,27 +231,38 @@ python -m pipx ensurepath
pipx install gvsbuild
# build gtk + libadwaita
gvsbuild build gtk4 libadwaita librsvg
gvsbuild build gtk4 libadwaita librsvg adwaita-icon-theme
```
- **Make sure to add the directory** `C:\gtk-build\gtk\x64\release\bin`
[**to the `PATH` environment variable**]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below).
the gtk frontend (see conditional compilation).
</details>
## Usage
### Gtk Frontend
<details>
<summary>Gtk Frontend</summary>
By default the gtk frontend will open when running `lan-mouse`.
To add a new connection, simply click the `Add` button on *both* devices,
enter the corresponding hostname and activate it.
To connect a device you want to control, simply click the `Add` button and enter the hostname
of the device.
If the mouse can not be moved onto a device, make sure you have port `4242` (or the one selected)
opened up in your firewall.
On the *remote* device, authorize your *local* device for incoming traffic using the `Authorize` button
under the "Incoming Connections" section.
The fingerprint for authorization can be found under the general section of your *local* device.
It is of the form "aa:bb:cc:..."
Authorized devices can be persisted using the configuration file (see [Configuration](#configuration)).
If the device still can not be entered, make sure you have UDP port `4242` (or the one selected) opened up in your firewall.
</details>
<details>
<summary>Command Line Interface</summary>
### Command Line Interface
The cli interface can be enabled using `--frontend cli` as commandline arguments.
Type `help` to list the available commands.
@@ -260,13 +276,17 @@ $ cargo run --release -- --frontend cli
(...)
> activate 0
```
</details>
<details>
<summary>Daemon Mode</summary>
Lan Mouse can be launched in daemon mode to keep it running in the background (e.g. for use in a systemd-service).
### Daemon
Lan Mouse can be launched in daemon mode to keep it running in the background.
To do so, add `--daemon` to the commandline args:
```sh
$ cargo run --release -- --daemon
lan-mouse --daemon
```
In order to start lan-mouse with a graphical session automatically,
@@ -279,6 +299,7 @@ cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service
```
</details>
## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
@@ -301,9 +322,14 @@ release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242)
port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# list of authorized tls certificate fingerprints that
# are accepted for incoming traffic
[authorized_fingerprints]
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium"
[right]
# hostname
@@ -334,109 +360,57 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
- [x] Liveness tracking: Automatically release keys, when server offline
- [x] MacOS KeyCode Translation
- [x] Libei Input Capture
- [x] MacOS Input Capture
- [x] Windows Input Capture
- [x] Encryption
- [ ] X11 Input Capture
- [ ] Windows Input Capture
- [ ] MacOS Input Capture
- [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support
- [ ] *Encryption*
## Protocol
Currently *all* mouse and keyboard events are sent via **UDP** for performance reasons.
Each event is sent as one single datagram, currently without any acknowledgement to guarantee 0% packet loss.
This means, any packet that is lost results in a discarded mouse / key event, which is ignored for now.
**UDP** also has the additional benefit that no reconnection logic is required.
Any client can just go offline and it will simply start working again as soon as it comes back online.
Additionally a tcp server is hosted for data that needs to be sent reliably (e.g. the keymap from the server or clipboard contents in the future) can be requested via a tcp connection.
## Bandwidth considerations
The most bandwidth is taken up by mouse events. A typical office mouse has a polling rate of 125Hz
while gaming mice typically have a much higher polling rate of 1000Hz.
A mouse Event consists of 21 Bytes:
- 1 Byte for the event type enum,
- 4 Bytes (u32) for the timestamp,
- 8 Bytes (f64) for dx,
- 8 Bytes (f64) for dy.
Additionally the IP header with 20 Bytes and the udp header with 8 Bytes take up another 28 Byte.
So in total there is 49 * 1000 Bytes/s for a 1000Hz gaming mouse.
This makes for a bandwidth requirement of 392 kbit/s in total _even_ for a high end gaming mouse.
So bandwidth is a non-issue.
Larger data chunks, like the keymap are offered by the server via tcp listening on the same port.
This way we dont need to implement any congestion control and leave this up to tcp.
In the future this can be used for e.g. clipboard contents as well.
## Packets per Second
While on LAN the performance is great,
some WIFI cards seem to struggle with the amount of packets per second,
particularly on high-end gaming mice with 1000Hz+ polling rates.
The plan is to implement a way of accumulating packets and sending them as
one single key event to reduce the packet rate (basically reducing the polling
rate artificially).
The way movement data is currently sent is also quite wasteful since even a 16bit integer
is likely enough to represent even the fastest possible mouse movement.
A different encoding that is more efficient for smaller values like
[Protocol Buffers](https://protobuf.dev/programming-guides/encoding/)
would be a better choice for the future and could also help for WIFI connections.
## Security
Sending key and mouse event data over the local network might not be the biggest security concern but in any public network or business environment it's *QUITE* a problem to basically broadcast your keystrokes.
- There should be an encryption layer below the application to enable a secure link.
- The encryption keys could be generated by the graphical frontend.
## Wayland support
### Input Emulation (for receiving events)
On wayland input-emulation is in an early/unstable state as of writing this.
## Detailed OS Support
For this reason a suitable backend is chosen based on the active desktop environment / compositor.
In order to use a device for sending events, an **input-capture** backend is required, while receiving events requires
a supported **input-emulation** *and* **input-capture** backend.
Different compositors have different ways of enabling input emulation:
A suitable backend is chosen automatically based on the active desktop environment / compositor.
#### Wlroots
Most wlroots-based compositors like Hyprland and Sway support the following
unstable wayland protocols for keyboard and mouse emulation:
- [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1)
- [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1)
The following sections detail the emulation and capture backends provided by lan-mouse and their support in desktop environments / operating systems.
#### KDE
KDE also has a protocol for input emulation ([kde-fake-input](https://wayland.app/protocols/kde-fake-input)),
it is however not exposed to third party applications.
### Input Emulation Support
The recommended way to emulate input on KDE is the
[freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop).
| Desktop / Backend | layer-shell | libei | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|-----|
| Wayland (wlroots) | :heavy_check_mark: | | | | |
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | | | |
| Windows | | | :heavy_check_mark: | | |
| MacOS | | | | :heavy_check_mark: | |
| X11 | | | | | WIP |
#### Gnome
Gnome uses [libei](https://gitlab.freedesktop.org/libinput/libei) for input emulation and capture,
which has the goal to become the general approach for emulating and capturing Input on Wayland.
- `wlroots`: This backend makes use of the [wlr-virtual-pointer-unstable-v1](https://wayland.app/protocols/wlr-virtual-pointer-unstable-v1) and [virtual-keyboard-unstable-v1](https://wayland.app/protocols/virtual-keyboard-unstable-v1) protocols and is supported by most wlroots based compositors.
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
- `xdp`: This backend uses the [freedesktop remote-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.RemoteDesktop) and is supported on GNOME and Plasma.
- `x11`: Backend for X11 sessions.
- `windows`: Backend for Windows.
- `macos`: Backend for MacOS.
### Input capture
To capture mouse and keyboard input, a few things are necessary:
- Displaying an immovable surface at screen edges
- Locking the mouse in place
- (optionally but highly recommended) reading unaccelerated mouse input
| Required Protocols (Event Emitting) | Sway | Kwin | Gnome |
|----------------------------------------|--------------------|----------------------|----------------------|
| pointer-constraints-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| relative-pointer-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| keyboard-shortcuts-inhibit-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| wlr-layer-shell-unstable-v1 | :heavy_check_mark: | :heavy_check_mark: | :x: |
### Input Capture Support
The [zwlr\_virtual\_pointer\_manager\_v1](wlr-virtual-pointer-unstable-v1) is required
to display surfaces on screen edges and used to display the immovable window on
both wlroots based compositors and KDE.
Gnome unfortunately does not support this protocol
and [likely won't ever support it](https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1141).
~In order for layershell surfaces to be able to lock the pointer using the pointer\_constraints protocol [this patch](https://github.com/swaywm/sway/pull/7178) needs to be applied to sway.~
(this works natively on sway versions >= 1.8)
| Desktop / Backend | wlroots | libei | remote-desktop portal | windows | macos | x11 |
|---------------------------|--------------------------|--------------------------|--------------------------|--------------------------|----------------------------------------|--------------------|
| Wayland (wlroots) | :heavy_check_mark: | | | | | |
| Wayland (KDE) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Wayland (Gnome) | | :heavy_check_mark: | :heavy_check_mark: | | | |
| Windows | | | | :heavy_check_mark: | | |
| MacOS | | | | | :heavy_check_mark: | |
| X11 | | | | | | :heavy_check_mark: |
- `layer-shell`: This backend creates a single pixel wide window on the edges of Displays to capture the cursor using the [layer-shell protocol](https://wayland.app/protocols/wlr-layer-shell-unstable-v1).
- `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1.
- `windows`: Backend for input capture on Windows.
- `macos`: Backend for input capture on MacOS.
- `x11`: TODO (not yet supported)

View File

@@ -3,13 +3,18 @@
# capture_backend = "LayerShell"
# release bind
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
release_bind = ["KeyA", "KeyS", "KeyD", "KeyF"]
# optional port (defaults to 4242)
port = 4242
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# list of authorized tls certificate fingerprints that
# are accepted for incoming traffic
[authorized_fingerprints]
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium"
[right]
# hostname

View File

@@ -1,7 +1,7 @@
[package]
name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -10,10 +10,10 @@ repository = "https://github.com/feschber/lan-mouse"
futures = "0.3.28"
futures-core = "0.3.30"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.2.1" }
input-event = { path = "../input-event", version = "0.3.0" }
memmap = "0.7"
tempfile = "3.8"
thiserror = "1.0.61"
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
@@ -40,18 +40,18 @@ wayland-protocols-wlr = { version = "0.3.1", features = [
"client",
], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.9", default-features = false, features = [
ashpd = { version = "0.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.2", features = ["tokio"], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.23", features = ["highsierra"] }
core-foundation = "0.9.4"
core-graphics = { version = "0.24.0", features = ["highsierra"] }
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
libc = "0.2.155"
keycode = "0.4.0"
bitflags = "2.5.0"
bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58.0", features = [
@@ -65,8 +65,8 @@ windows = { version = "0.58.0", features = [
] }
[features]
default = ["wayland", "x11", "libei"]
wayland = [
default = ["layer_shell", "x11", "libei"]
layer_shell = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",

View File

@@ -8,9 +8,9 @@ pub enum InputCaptureError {
Capture(#[from] CaptureError),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use std::io;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
@@ -19,26 +19,10 @@ use wayland_client::{
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use ashpd::desktop::ResponseError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::{EiConvertEventStreamError, HandshakeError};
#[cfg(target_os = "macos")]
use core_graphics::base::CGError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("error in libei stream: {inner:?}")]
pub struct ReisConvertEventStreamError {
inner: EiConvertEventStreamError,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl From<EiConvertEventStreamError> for ReisConvertEventStreamError {
fn from(e: EiConvertEventStreamError) -> Self {
Self { inner: e }
}
}
#[derive(Debug, Error)]
pub enum CaptureError {
#[error("activation stream closed unexpectedly")]
@@ -48,11 +32,8 @@ pub enum CaptureError {
#[error("io error: `{0}`")]
Io(#[from] std::io::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error in libei stream: `{0}`")]
Reis(#[from] ReisConvertEventStreamError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei handshake failed: `{0}`")]
Handshake(#[from] HandshakeError),
#[error("libei error: `{0}`")]
Reis(#[from] reis::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error(transparent)]
Portal(#[from] ashpd::Error),
@@ -83,7 +64,7 @@ pub enum CaptureCreationError {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("error creating input-capture-portal backend: `{0}`")]
Libei(#[from] LibeiCaptureCreationError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[error("error creating layer-shell capture backend: `{0}`")]
LayerShell(#[from] LayerShellCaptureCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
@@ -121,7 +102,7 @@ pub enum LibeiCaptureCreationError {
Ashpd(#[from] ashpd::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("{protocol} protocol not supported: {inner}")]
pub struct WaylandBindError {
@@ -129,14 +110,14 @@ pub struct WaylandBindError {
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum LayerShellCaptureCreationError {
#[error(transparent)]

View File

@@ -648,10 +648,16 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() {
if capabilities.contains(wl_seat::Capability::Pointer) {
if let Some(p) = state.pointer.take() {
p.release();
}
state.pointer.replace(seat.get_pointer(qh, ()));
}
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() {
if capabilities.contains(wl_seat::Capability::Keyboard) {
if let Some(k) = state.keyboard.take() {
k.release();
}
seat.get_keyboard(qh, ());
}
}

View File

@@ -21,8 +21,8 @@ mod libei;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
mod wayland;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
mod layer_shell;
#[cfg(windows)]
mod windows;
@@ -87,7 +87,7 @@ impl Display for Position {
pub enum Backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
@@ -103,7 +103,7 @@ impl Display for Backend {
match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"),
@@ -132,31 +132,35 @@ pub struct InputCapture {
impl InputCapture {
/// create a new client with the given id
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
assert!(!self.id_map.contains_key(&id));
self.id_map.insert(id, pos);
if let Some(v) = self.position_map.get_mut(&pos) {
v.push(id);
Ok(())
} else {
self.position_map.insert(pos, vec![id]);
self.id_map.insert(id, pos);
self.capture.create(pos).await
}
}
/// destroy the client with the given id, if it exists
pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> {
if let Some(pos) = self.id_map.remove(&id) {
let destroy = if let Some(v) = self.position_map.get_mut(&pos) {
v.retain(|&i| i != id);
// we were the last id registered at this position
v.is_empty()
} else {
// nothing to destroy
false
};
if destroy {
self.position_map.remove(&pos);
self.capture.destroy(pos).await?;
}
let pos = self
.id_map
.remove(&id)
.expect("no position for this handle");
log::debug!("destroying capture {id} @ {pos}");
let remaining = self.position_map.get_mut(&pos).expect("id vector");
remaining.retain(|&i| i != id);
log::debug!("remaining ids @ {pos}: {remaining:?}");
if remaining.is_empty() {
log::debug!("destroying capture @ {pos} - no remaining ids");
self.position_map.remove(&pos);
self.capture.destroy(pos).await?;
}
Ok(())
}
@@ -283,8 +287,8 @@ async fn create_backend(
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(wayland::LayerShellInputCapture::new()?)),
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
#[cfg(windows)]
@@ -312,7 +316,7 @@ async fn create(
for backend in [
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
Backend::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,

View File

@@ -1,6 +1,9 @@
use ashpd::{
desktop::{
input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones},
input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones,
},
Session,
},
enumflags2::BitFlags,
@@ -8,14 +11,15 @@ use ashpd::{
use async_trait::async_trait;
use futures::{FutureExt, StreamExt};
use reis::{
ei,
event::{DeviceCapability, EiEvent},
tokio::{EiConvertEventStream, EiEventStream},
ei::{self, handshake::ContextType},
event::{Connection, DeviceCapability, EiEvent},
tokio::EiConvertEventStream,
};
use std::{
cell::Cell,
collections::HashMap,
io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
@@ -32,14 +36,13 @@ use tokio::{
use tokio_util::sync::CancellationToken;
use futures_core::Stream;
use once_cell::sync::Lazy;
use input_event::Event;
use crate::CaptureEvent;
use super::{
error::{CaptureError, LibeiCaptureCreationError, ReisConvertEventStreamError},
error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, Position,
};
@@ -65,22 +68,6 @@ pub struct LibeiInputCapture<'a> {
terminated: bool,
}
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("ei_connection", 1);
m.insert("ei_callback", 1);
m.insert("ei_pingpong", 1);
m.insert("ei_seat", 1);
m.insert("ei_device", 2);
m.insert("ei_pointer", 1);
m.insert("ei_pointer_absolute", 1);
m.insert("ei_scroll", 1);
m.insert("ei_button", 1);
m.insert("ei_keyboard", 1);
m.insert("ei_touchscreen", 1);
m
});
/// returns (start pos, end pos), inclusive
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
let (x, y) = (r.x_offset(), r.y_offset());
@@ -118,7 +105,7 @@ impl From<ICBarrier> for Barrier {
fn select_barriers(
zones: &Zones,
clients: &[Position],
next_barrier_id: &mut u32,
next_barrier_id: &mut NonZeroU32,
) -> (Vec<ICBarrier>, HashMap<BarrierID, Position>) {
let mut pos_for_barrier = HashMap::new();
let mut barriers: Vec<ICBarrier> = vec![];
@@ -129,7 +116,9 @@ fn select_barriers(
.iter()
.map(|r| {
let id = *next_barrier_id;
*next_barrier_id = id + 1;
*next_barrier_id = next_barrier_id
.checked_add(1)
.expect("barrier id out of range");
let position = pos_to_barrier(r, *pos);
pos_for_barrier.insert(id, *pos);
ICBarrier::new(id, position)
@@ -144,7 +133,7 @@ async fn update_barriers(
input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>,
active_clients: &[Position],
next_barrier_id: &mut u32,
next_barrier_id: &mut NonZeroU32,
) -> Result<(Vec<ICBarrier>, HashMap<BarrierID, Position>), ashpd::Error> {
let zones = input_capture.zones(session).await?.response()?;
log::debug!("zones: {zones:?}");
@@ -168,7 +157,7 @@ async fn create_session<'a>(
log::debug!("creating input capture session");
input_capture
.create_session(
&ashpd::WindowIdentifier::default(),
None,
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
)
.await
@@ -177,7 +166,7 @@ async fn create_session<'a>(
async fn connect_to_eis(
input_capture: &InputCapture<'_>,
session: &Session<'_, InputCapture<'_>>,
) -> Result<(ei::Context, EiConvertEventStream), CaptureError> {
) -> Result<(ei::Context, Connection, EiConvertEventStream), CaptureError> {
log::debug!("connect_to_eis");
let fd = input_capture.connect_to_eis(session).await?;
@@ -187,17 +176,11 @@ async fn connect_to_eis(
// create ei context
let context = ei::Context::new(stream)?;
let mut event_stream = EiEventStream::new(context.clone())?;
let response = reis::tokio::ei_handshake(
&mut event_stream,
"de.feschber.LanMouse",
ei::handshake::ContextType::Receiver,
&INTERFACES,
)
.await?;
let event_stream = EiConvertEventStream::new(event_stream, response.serial);
let (conn, event_stream) = context
.handshake_tokio("de.feschber.LanMouse", ContextType::Receiver)
.await?;
Ok((context, event_stream))
Ok((context, conn, event_stream))
}
async fn libei_event_handler(
@@ -211,8 +194,7 @@ async fn libei_event_handler(
let ei_event = ei_event_stream
.next()
.await
.ok_or(CaptureError::EndOfStream)?
.map_err(ReisConvertEventStreamError::from)?;
.ok_or(CaptureError::EndOfStream)??;
log::trace!("from ei: {ei_event:?}");
let client = current_pos.get();
handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?;
@@ -268,7 +250,7 @@ async fn do_capture(
/* safety: libei_task does not outlive Self */
let input_capture = unsafe { &*input_capture };
let mut active_clients: Vec<Position> = vec![];
let mut next_barrier_id = 1u32;
let mut next_barrier_id = NonZeroU32::new(1).expect("id must be non-zero");
let mut zones_changed = input_capture.receive_zones_changed().await?;
@@ -358,7 +340,7 @@ async fn do_capture_session(
session: &mut Session<'_, InputCapture<'_>>,
event_tx: &Sender<(Position, CaptureEvent)>,
active_clients: &[Position],
next_barrier_id: &mut u32,
next_barrier_id: &mut NonZeroU32,
notify_release: &Notify,
cancel: (CancellationToken, CancellationToken),
) -> Result<(), CaptureError> {
@@ -367,7 +349,7 @@ async fn do_capture_session(
let current_pos = Rc::new(Cell::new(None));
// connect to eis server
let (context, ei_event_stream) = connect_to_eis(input_capture, session).await?;
let (context, _conn, ei_event_stream) = connect_to_eis(input_capture, session).await?;
// set barriers
let (barriers, pos_for_barrier_id) =
@@ -415,9 +397,9 @@ async fn do_capture_session(
// get barrier id from activation
let barrier_id = match activated.barrier_id() {
Some(bid) => bid,
Some(ActivatedBarrier::Barrier(id)) => id,
// workaround for KDE plasma not reporting barrier ids
None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")),
Some(ActivatedBarrier::UnknownBarrier) | None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")),
};
// find client corresponding to barrier

View File

@@ -1,7 +1,7 @@
[package]
name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -10,8 +10,8 @@ repository = "https://github.com/feschber/lan-mouse"
async-trait = "0.1.80"
futures = "0.3.28"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.2.1" }
thiserror = "1.0.61"
input-event = { path = "../input-event", version = "0.3.0" }
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",
@@ -38,14 +38,14 @@ wayland-protocols-misc = { version = "0.3.1", features = [
"client",
], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.9", default-features = false, features = [
ashpd = { version = "0.10", default-features = false, features = [
"tokio",
], optional = true }
reis = { version = "0.2", features = ["tokio"], optional = true }
reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.5.0"
core-graphics = { version = "0.23", features = ["highsierra"] }
bitflags = "2.6.0"
core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "0.4.0"
[target.'cfg(windows)'.dependencies]
@@ -60,13 +60,13 @@ windows = { version = "0.58.0", features = [
] }
[features]
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
wayland = [
default = ["wlroots", "x11", "remote_desktop_portal", "libei"]
wlroots = [
"dep:wayland-client",
"dep:wayland-protocols",
"dep:wayland-protocols-wlr",
"dep:wayland-protocols-misc",
]
x11 = ["dep:x11"]
xdg_desktop_portal = ["dep:ashpd"]
remote_desktop_portal = ["dep:ashpd"]
libei = ["dep:reis", "dep:ashpd"]

View File

@@ -6,53 +6,35 @@ pub enum InputEmulationError {
Emulate(#[from] EmulationError),
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(
unix,
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
use ashpd::{desktop::ResponseError, Error::Response};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::EiConvertEventStreamError;
use std::io;
use thiserror::Error;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{
backend::WaylandError,
globals::{BindError, GlobalError},
ConnectError, DispatchError,
};
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
use reis::tokio::HandshakeError;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("error in libei stream: {inner:?}")]
pub struct ReisConvertStreamError {
inner: EiConvertEventStreamError,
}
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
impl From<EiConvertEventStreamError> for ReisConvertStreamError {
fn from(e: EiConvertEventStreamError) -> Self {
Self { inner: e }
}
}
#[derive(Debug, Error)]
pub enum EmulationError {
#[error("event stream closed")]
EndOfStream,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei error flushing events: `{0}`")]
Libei(#[from] reis::event::Error),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("")]
LibeiConvertStream(#[from] ReisConvertStreamError),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[error("libei error: `{0}`")]
Libei(#[from] reis::Error),
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wayland error: `{0}`")]
Wayland(#[from] wayland_client::backend::WaylandError),
#[cfg(all(
unix,
any(feature = "xdg_desktop_portal", feature = "libei"),
any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos")
))]
#[error("xdg-desktop-portal: `{0}`")]
@@ -63,13 +45,13 @@ pub enum EmulationError {
#[derive(Debug, Error)]
pub enum EmulationCreationError {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[error("wlroots backend: `{0}`")]
Wlroots(#[from] WlrootsEmulationCreationError),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[error("libei backend: `{0}`")]
Libei(#[from] LibeiEmulationCreationError),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[error("xdg-desktop-portal: `{0}`")]
Xdp(#[from] XdpEmulationCreationError),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
@@ -97,7 +79,7 @@ impl EmulationCreationError {
) {
return true;
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
if matches!(
self,
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
@@ -110,7 +92,7 @@ impl EmulationCreationError {
}
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum WlrootsEmulationCreationError {
#[error(transparent)]
@@ -127,7 +109,7 @@ pub enum WlrootsEmulationCreationError {
Io(#[from] std::io::Error),
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
#[derive(Debug, Error)]
#[error("wayland protocol \"{protocol}\" not supported: {inner}")]
pub struct WaylandBindError {
@@ -135,7 +117,7 @@ pub struct WaylandBindError {
protocol: &'static str,
}
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
impl WaylandBindError {
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
Self { inner, protocol }
@@ -150,10 +132,10 @@ pub enum LibeiEmulationCreationError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Handshake(#[from] HandshakeError),
Reis(#[from] reis::Error),
}
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
#[derive(Debug, Error)]
pub enum XdpEmulationCreationError {
#[error(transparent)]

View File

@@ -14,10 +14,10 @@ mod windows;
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
mod x11;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
@@ -34,11 +34,11 @@ pub type EmulationHandle = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
X11,
@@ -52,11 +52,11 @@ pub enum Backend {
impl Display for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => write!(f, "X11"),
@@ -78,13 +78,13 @@ pub struct InputEmulation {
impl InputEmulation {
async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
let emulation: Box<dyn Emulation> = match backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11 => Box::new(x11::X11Emulation::new()?),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
#[cfg(windows)]
Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
@@ -109,11 +109,11 @@ impl InputEmulation {
}
for backend in [
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
Backend::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
Backend::Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
Backend::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
Backend::X11,

View File

@@ -1,23 +1,18 @@
use futures::{future, StreamExt};
use once_cell::sync::Lazy;
use std::{
collections::HashMap,
io,
os::{fd::OwnedFd, unix::net::UnixStream},
sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::{SystemTime, UNIX_EPOCH},
};
use tokio::task::JoinHandle;
use ashpd::{
desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session,
},
WindowIdentifier,
use ashpd::desktop::{
remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session,
};
use async_trait::async_trait;
@@ -26,32 +21,16 @@ use reis::{
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
Pointer, Scroll,
},
event::{DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::{ei_handshake, EiConvertEventStream, EiEventStream},
event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::EiConvertEventStream,
};
use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::{EmulationError, ReisConvertStreamError};
use crate::error::EmulationError;
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("ei_connection", 1);
m.insert("ei_callback", 1);
m.insert("ei_pingpong", 1);
m.insert("ei_seat", 1);
m.insert("ei_device", 2);
m.insert("ei_pointer", 1);
m.insert("ei_pointer_absolute", 1);
m.insert("ei_scroll", 1);
m.insert("ei_button", 1);
m.insert("ei_keyboard", 1);
m.insert("ei_touchscreen", 1);
m
});
#[derive(Clone, Default)]
struct Devices {
pointer: Arc<RwLock<Option<(ei::Device, ei::Pointer)>>>,
@@ -62,11 +41,11 @@ struct Devices {
pub(crate) struct LibeiEmulation<'a> {
context: ei::Context,
conn: event::Connection,
devices: Devices,
ei_task: JoinHandle<()>,
error: Arc<Mutex<Option<EmulationError>>>,
libei_error: Arc<AtomicBool>,
serial: AtomicU32,
_remote_desktop: RemoteDesktop<'a>,
session: Session<'a, RemoteDesktop<'a>>,
}
@@ -89,10 +68,7 @@ async fn get_ei_fd<'a>(
.await?;
log::info!("requesting permission for input emulation");
let _devices = remote_desktop
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
let _devices = remote_desktop.start(&session, None).await?.response()?;
let fd = remote_desktop.connect_to_eis(&session).await?;
Ok((remote_desktop, session, fd))
@@ -104,20 +80,15 @@ impl<'a> LibeiEmulation<'a> {
let stream = UnixStream::from(eifd);
stream.set_nonblocking(true)?;
let context = ei::Context::new(stream)?;
let mut events = EiEventStream::new(context.clone())?;
let handshake = ei_handshake(
&mut events,
"de.feschber.LanMouse",
ContextType::Sender,
&INTERFACES,
)
.await?;
let events = EiConvertEventStream::new(events, handshake.serial);
let (conn, events) = context
.handshake_tokio("de.feschber.LanMouse", ContextType::Sender)
.await?;
let devices = Devices::default();
let libei_error = Arc::new(AtomicBool::default());
let error = Arc::new(Mutex::new(None));
let ei_handler = ei_task(
events,
conn.clone(),
context.clone(),
devices.clone(),
libei_error.clone(),
@@ -125,15 +96,13 @@ impl<'a> LibeiEmulation<'a> {
);
let ei_task = tokio::task::spawn_local(ei_handler);
let serial = AtomicU32::new(handshake.serial);
Ok(Self {
context,
conn,
devices,
ei_task,
error,
libei_error,
serial,
_remote_desktop,
session,
})
@@ -169,7 +138,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
let pointer_device = self.devices.pointer.read().unwrap();
if let Some((d, p)) = pointer_device.as_ref() {
p.motion_relative(dx as f32, dy as f32);
d.frame(self.serial.load(Ordering::SeqCst), now);
d.frame(self.conn.serial(), now);
}
}
PointerEvent::Button {
@@ -186,7 +155,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
_ => ButtonState::Press,
},
);
d.frame(self.serial.load(Ordering::SeqCst), now);
d.frame(self.conn.serial(), now);
}
}
PointerEvent::Axis {
@@ -200,7 +169,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
0 => s.scroll(0., value as f32),
_ => s.scroll(value as f32, 0.),
}
d.frame(self.serial.load(Ordering::SeqCst), now);
d.frame(self.conn.serial(), now);
}
}
PointerEvent::AxisDiscrete120 { axis, value } => {
@@ -210,7 +179,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
0 => s.scroll_discrete(0, value),
_ => s.scroll_discrete(value, 0),
}
d.frame(self.serial.load(Ordering::SeqCst), now);
d.frame(self.conn.serial(), now);
}
}
},
@@ -229,7 +198,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
_ => KeyState::Press,
},
);
d.frame(self.serial.load(Ordering::SeqCst), now);
d.frame(self.conn.serial(), now);
}
}
KeyboardEvent::Modifiers { .. } => {}
@@ -252,6 +221,7 @@ impl<'a> Emulation for LibeiEmulation<'a> {
async fn ei_task(
mut events: EiConvertEventStream,
_conn: Connection,
context: ei::Context,
devices: Devices,
libei_error: Arc<AtomicBool>,
@@ -276,11 +246,7 @@ async fn ei_event_handler(
devices: &Devices,
) -> Result<(), EmulationError> {
loop {
let event = events
.next()
.await
.ok_or(EmulationError::EndOfStream)?
.map_err(ReisConvertStreamError::from)?;
let event = events.next().await.ok_or(EmulationError::EndOfStream)??;
const CAPABILITIES: &[DeviceCapability] = &[
DeviceCapability::Pointer,
DeviceCapability::PointerAbsolute,

View File

@@ -4,7 +4,6 @@ use ashpd::{
PersistMode, Session,
},
zbus::AsyncDrop,
WindowIdentifier,
};
use async_trait::async_trait;
@@ -43,10 +42,7 @@ impl<'a> DesktopPortalEmulation<'a> {
.await?;
log::info!("requesting permission for input emulation");
let _devices = proxy
.start(&session, &WindowIdentifier::default())
.await?
.response()?;
let _devices = proxy.start(&session, None).await?.response()?;
log::debug!("started session");
let session = session;

View File

@@ -1,7 +1,7 @@
[package]
name = "input-event"
description = "cross-platform input-event types for input-capture / input-emulation"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -11,10 +11,10 @@ futures-core = "0.3.30"
log = "0.4.22"
num_enum = "0.7.2"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0.61"
thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
reis = { version = "0.2.0", optional = true }
reis = { version = "0.4", optional = true }
[features]
default = ["libei"]

View File

@@ -1,14 +1,14 @@
[package]
name = "lan-mouse-cli"
description = "CLI Frontend for lan-mouse"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.1.0" }
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
tokio = { version = "1.32.0", features = [
"io-util",
"io-std",

View File

@@ -274,6 +274,17 @@ impl Cli {
FrontendEvent::EmulationStatus(s) => {
eprintln!("emulation status: {s:?}")
}
FrontendEvent::AuthorizedUpdated(fingerprints) => {
eprintln!("authorized keys changed:");
for (desc, fp) in fingerprints {
eprintln!("{desc}: {fp}");
}
}
FrontendEvent::PublicKeyFingerprint(fp) => {
eprintln!("the public key fingerprint of this device is {fp}");
}
FrontendEvent::IncomingConnected(..) => {}
FrontendEvent::IncomingDisconnected(..) => {}
}
}

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse-gtk"
description = "GTK4 / Libadwaita Frontend for lan-mouse"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -12,7 +12,7 @@ adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
async-channel = { version = "2.1.1" }
hostname = "0.4.0"
log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.1.0" }
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
[build-dependencies]
glib-build-tools = { version = "0.20.0" }

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<template class="FingerprintWindow" parent="AdwWindow">
<property name="modal">True</property>
<property name="width-request">880</property>
<property name="default-width">880</property>
<property name="height-request">380</property>
<property name="default-height">380</property>
<property name="title" translatable="yes">Add Certificate Fingerprint</property>
<property name="content">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar"/>
</child>
<property name="content">
<object class="AdwClamp">
<property name="maximum-size">770</property>
<property name="tightening-threshold">0</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkLabel">
<property name="label">The certificate fingerprint serves as a unique identifier for your device.</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">You can find it under the `General` section of the device you want to connect</property>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title">description</property>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkText" id="description">
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="enable-undo">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="max-length">0</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title">sha256 fingerprint</property>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkText" id="fingerprint">
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="enable-undo">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="max-length">0</property>
</object>
</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="halign">center</property>
<child>
<object class="GtkButton" id="confirm_button">
<signal name="clicked" handler="handle_confirm" swapped="true"/>
<property name="label" translatable="yes">Confirm</property>
<property name="can-shrink">True</property>
<style>
<class name="pill"/>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
</template>
</interface>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="KeyRow" parent="AdwActionRow">
<child type="prefix">
<object class="GtkButton" id="delete_button">
<property name="valign">center</property>
<property name="halign">end</property>
<property name="tooltip-text" translatable="yes">revoke authorization</property>
<property name="icon-name">edit-delete-symbolic</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
</template>
</interface>

View File

@@ -2,7 +2,9 @@
<gresources>
<gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">key_row.ui</file>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>

View File

@@ -121,7 +121,31 @@
-->
<child>
<object class="AdwActionRow">
<property name="title">port</property>
<property name="title">hostname &#38;amp; port</property>
<child>
<object class="GtkButton" id="copy-hostname-button">
<!--<property name="icon-name">edit-copy-symbolic</property>-->
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkImage" id="hostname_copy_icon">
<property name="icon-name">edit-copy-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkEntry" id="port_entry">
<property name="max-width-chars">5</property>
@@ -160,20 +184,14 @@
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title">hostname</property>
<object class="AdwActionRow" id="fingerprint_row">
<property name="title">certificate fingerprint</property>
<property name="icon-name">auth-fingerprint-symbolic</property>
<child>
<object class="GtkLabel" id="hostname_label">
<property name="label">&lt;span font_style=&quot;italic&quot; font_weight=&quot;light&quot; foreground=&quot;darkgrey&quot;&gt;could not determine hostname&lt;/span&gt;</property>
<property name="use-markup">true</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="copy-hostname-button">
<object class="GtkButton" id="copy-fingerprint-button">
<property name="icon-name">edit-copy-symbolic</property>
<property name="valign">center</property>
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
<signal name="clicked" handler="handle_copy_fingerprint" swapped="true"/>
</object>
</child>
</object>
@@ -213,6 +231,39 @@
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Incoming Connections</property>
<property name="header-suffix">
<object class="GtkButton">
<signal name="clicked" handler="handle_add_cert_fingerprint" swapped="true"/>
<property name="child">
<object class="AdwButtonContent">
<property name="icon-name">auth-fingerprint-symbolic</property>
<property name="label" translatable="yes">Authorize</property>
</object>
</property>
<style>
<class name="flat"/>
</style>
</object>
</property>
<child>
<object class="GtkListBox" id="authorized_list">
<property name="selection-mode">none</property>
<child type="placeholder">
<object class="AdwActionRow" id="authorized_placeholder">
<property name="title">no devices registered!</property>
<property name="subtitle">authorize a new device via the "Authorize" button</property>
</object>
</child>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>

View File

@@ -0,0 +1,18 @@
mod imp;
use glib::Object;
use gtk::{gio, glib};
glib::wrapper! {
pub struct FingerprintWindow(ObjectSubclass<imp::FingerprintWindow>)
@extends adw::Window, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl FingerprintWindow {
pub(crate) fn new() -> Self {
let window: Self = Object::builder().build();
window
}
}

View File

@@ -0,0 +1,64 @@
use std::sync::OnceLock;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
use gtk::{
glib::{self, subclass::Signal},
template_callbacks, Button, CompositeTemplate, Text,
};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/fingerprint_window.ui")]
pub struct FingerprintWindow {
#[template_child]
pub description: TemplateChild<Text>,
#[template_child]
pub fingerprint: TemplateChild<Text>,
#[template_child]
pub confirm_button: TemplateChild<Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for FingerprintWindow {
const NAME: &'static str = "FingerprintWindow";
const ABSTRACT: bool = false;
type Type = super::FingerprintWindow;
type ParentType = adw::Window;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[template_callbacks]
impl FingerprintWindow {
#[template_callback]
fn handle_confirm(&self, _button: Button) {
let desc = self.description.text().as_str().trim().to_owned();
let fp = self.fingerprint.text().as_str().trim().to_owned();
self.obj().emit_by_name("confirm-clicked", &[&desc, &fp])
}
}
impl ObjectImpl for FingerprintWindow {
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![Signal::builder("confirm-clicked")
.param_types([String::static_type(), String::static_type()])
.build()]
})
}
}
impl WidgetImpl for FingerprintWindow {}
impl WindowImpl for FingerprintWindow {}
impl ApplicationWindowImpl for FingerprintWindow {}
impl AdwWindowImpl for FingerprintWindow {}

View File

@@ -0,0 +1,25 @@
mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
glib::wrapper! {
pub struct KeyObject(ObjectSubclass<imp::KeyObject>);
}
impl KeyObject {
pub fn new(desc: String, fp: String) -> Self {
Object::builder()
.property("description", desc)
.property("fingerprint", fp)
.build()
}
pub fn get_description(&self) -> String {
self.imp().description.borrow().clone()
}
pub fn get_fingerprint(&self) -> String {
self.imp().fingerprint.borrow().clone()
}
}

View File

@@ -0,0 +1,24 @@
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
#[derive(Properties, Default)]
#[properties(wrapper_type = super::KeyObject)]
pub struct KeyObject {
#[property(name = "description", get, set, type = String)]
pub description: RefCell<String>,
#[property(name = "fingerprint", get, set, type = String)]
pub fingerprint: RefCell<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for KeyObject {
const NAME: &'static str = "KeyObject";
type Type = super::KeyObject;
}
#[glib::derived_properties]
impl ObjectImpl for KeyObject {}

View File

@@ -0,0 +1,48 @@
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use super::KeyObject;
glib::wrapper! {
pub struct KeyRow(ObjectSubclass<imp::KeyRow>)
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl Default for KeyRow {
fn default() -> Self {
Self::new()
}
}
impl KeyRow {
pub fn new() -> Self {
Object::builder().build()
}
pub fn bind(&self, key_object: &KeyObject) {
let mut bindings = self.imp().bindings.borrow_mut();
let title_binding = key_object
.bind_property("description", self, "title")
.sync_create()
.build();
let subtitle_binding = key_object
.bind_property("fingerprint", self, "subtitle")
.sync_create()
.build();
bindings.push(title_binding);
bindings.push(subtitle_binding);
}
pub fn unbind(&self) {
for binding in self.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}

View File

@@ -0,0 +1,68 @@
use std::cell::RefCell;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate};
use std::sync::OnceLock;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/key_row.ui")]
pub struct KeyRow {
#[template_child]
pub delete_button: TemplateChild<gtk::Button>,
pub bindings: RefCell<Vec<Binding>>,
}
#[glib::object_subclass]
impl ObjectSubclass for KeyRow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "KeyRow";
const ABSTRACT: bool = false;
type Type = super::KeyRow;
type ParentType = ActionRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for KeyRow {
fn constructed(&self) {
self.parent_constructed();
self.delete_button.connect_clicked(clone!(
#[weak(rename_to = row)]
self,
move |button| {
row.handle_delete(button);
}
));
}
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| vec![Signal::builder("request-delete").build()])
}
}
#[gtk::template_callbacks]
impl KeyRow {
#[template_callback]
fn handle_delete(&self, _button: &Button) {
self.obj().emit_by_name::<()>("request-delete", &[]);
}
}
impl WidgetImpl for KeyRow {}
impl BoxImpl for KeyRow {}
impl ListBoxRowImpl for KeyRow {}
impl PreferencesRowImpl for KeyRow {}
impl ActionRowImpl for KeyRow {}

View File

@@ -1,5 +1,8 @@
mod client_object;
mod client_row;
mod fingerprint_window;
mod key_object;
mod key_row;
mod window;
use std::{env, process, str};
@@ -15,6 +18,7 @@ use gtk::{
use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject;
use self::key_object::KeyObject;
pub fn run() -> glib::ExitCode {
log::debug!("running gtk frontend");
@@ -132,6 +136,14 @@ fn build_ui(app: &Application) {
FrontendEvent::EmulationStatus(s) => {
window.set_emulation(s.into());
}
FrontendEvent::AuthorizedUpdated(keys) => {
window.set_authorized_keys(keys);
}
FrontendEvent::PublicKeyFingerprint(fp) => {
window.set_pk_fp(&fp);
}
FrontendEvent::IncomingConnected(..) => {}
FrontendEvent::IncomingDisconnected(..) => {}
}
}
}

View File

@@ -1,5 +1,7 @@
mod imp;
use std::collections::HashMap;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
@@ -14,6 +16,8 @@ use lan_mouse_ipc::{
DEFAULT_PORT,
};
use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow};
use super::{client_object::ClientObject, client_row::ClientRow};
glib::wrapper! {
@@ -42,10 +46,55 @@ impl Window {
.expect("Could not get clients")
}
pub fn authorized(&self) -> gio::ListStore {
self.imp()
.authorized
.borrow()
.clone()
.expect("Could not get authorized")
}
fn client_by_idx(&self, idx: u32) -> Option<ClientObject> {
self.clients().item(idx).map(|o| o.downcast().unwrap())
}
fn authorized_by_idx(&self, idx: u32) -> Option<KeyObject> {
self.authorized().item(idx).map(|o| o.downcast().unwrap())
}
fn setup_authorized(&self) {
let store = gio::ListStore::new::<KeyObject>();
self.imp().authorized.replace(Some(store));
let selection_model = NoSelection::new(Some(self.authorized()));
self.imp().authorized_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let key_obj = obj.downcast_ref().expect("object of type `KeyObject`");
let row = window.create_key_row(key_obj);
row.connect_closure(
"request-delete",
false,
closure_local!(
#[strong]
window,
move |row: KeyRow| {
if let Some(key_obj) = window.authorized_by_idx(row.index() as u32)
{
window.request_fingerprint_remove(key_obj.get_fingerprint());
}
}
),
);
row.upcast()
}
),
)
}
fn setup_clients(&self) {
let model = gio::ListStore::new::<ClientObject>();
self.imp().clients.replace(Some(model));
@@ -114,7 +163,8 @@ impl Window {
/// workaround for a bug in libadwaita that shows an ugly line beneath
/// the last element if a placeholder is set.
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
pub fn set_placeholder_visible(&self, visible: bool) {
pub fn update_placeholder_visibility(&self) {
let visible = self.clients().n_items() == 0;
let placeholder = self.imp().client_placeholder.get();
self.imp().client_list.set_placeholder(match visible {
true => Some(&placeholder),
@@ -122,6 +172,15 @@ impl Window {
});
}
pub fn update_auth_placeholder_visibility(&self) {
let visible = self.authorized().n_items() == 0;
let placeholder = self.imp().authorized_placeholder.get();
self.imp().authorized_list.set_placeholder(match visible {
true => Some(&placeholder),
false => None,
});
}
fn setup_icon(&self) {
self.set_icon_name(Some("de.feschber.LanMouse"));
}
@@ -132,10 +191,16 @@ impl Window {
row
}
fn create_key_row(&self, key_object: &KeyObject) -> KeyRow {
let row = KeyRow::new();
row.bind(key_object);
row
}
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
let client = ClientObject::new(handle, client, state.clone());
self.clients().append(&client);
self.set_placeholder_visible(false);
self.update_placeholder_visibility();
self.update_dns_state(handle, !state.ips.is_empty());
}
@@ -157,7 +222,7 @@ impl Window {
self.clients().remove(idx as u32);
if self.clients().n_items() == 0 {
self.set_placeholder_visible(true);
self.update_placeholder_visibility();
}
}
@@ -286,6 +351,32 @@ impl Window {
self.request(FrontendRequest::Delete(client.handle()));
}
pub fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new();
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
false,
closure_local!(
#[strong(rename_to = parent)]
self,
move |w: FingerprintWindow, desc: String, fp: String| {
parent.request_fingerprint_add(desc, fp);
w.close();
}
),
);
window.present();
}
pub fn request_fingerprint_add(&self, desc: String, fp: String) {
self.request(FrontendRequest::AuthorizeKey(desc, fp));
}
pub fn request_fingerprint_remove(&self, fp: String) {
self.request(FrontendRequest::RemoveAuthorizedKey(fp));
}
pub fn request(&self, request: FrontendRequest) {
let mut requester = self.imp().frontend_request_writer.borrow_mut();
let requester = requester.as_mut().unwrap();
@@ -319,4 +410,20 @@ impl Window {
.capture_emulation_group
.set_visible(!capture || !emulation);
}
pub(crate) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) {
let authorized = self.authorized();
// clear list
authorized.remove_all();
// insert fingerprints
for (fingerprint, description) in fingerprints {
let key_obj = KeyObject::new(description, fingerprint);
authorized.append(&key_obj);
}
self.update_auth_placeholder_visibility();
}
pub(crate) fn set_pk_fp(&self, fingerprint: &str) {
self.imp().fingerprint_row.set_subtitle(fingerprint);
}
}

View File

@@ -4,13 +4,17 @@ use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
use glib::subclass::InitializingObject;
use gtk::glib::clone;
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Image, Label, ListBox};
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window {
#[template_child]
pub authorized_placeholder: TemplateChild<ActionRow>,
#[template_child]
pub fingerprint_row: TemplateChild<ActionRow>,
#[template_child]
pub port_edit_apply: TemplateChild<Button>,
#[template_child]
@@ -22,6 +26,8 @@ pub struct Window {
#[template_child]
pub port_entry: TemplateChild<Entry>,
#[template_child]
pub hostname_copy_icon: TemplateChild<Image>,
#[template_child]
pub hostname_label: TemplateChild<Label>,
#[template_child]
pub toast_overlay: TemplateChild<ToastOverlay>,
@@ -35,7 +41,10 @@ pub struct Window {
pub input_emulation_button: TemplateChild<Button>,
#[template_child]
pub input_capture_button: TemplateChild<Button>,
#[template_child]
pub authorized_list: TemplateChild<ListBox>,
pub clients: RefCell<Option<gio::ListStore>>,
pub authorized: RefCell<Option<gio::ListStore>>,
pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
pub port: Cell<u16>,
pub capture_active: Cell<bool>,
@@ -69,25 +78,45 @@ impl Window {
}
#[template_callback]
fn handle_copy_hostname(&self, button: &Button) {
fn handle_copy_hostname(&self, _: &Button) {
if let Ok(hostname) = hostname::get() {
let display = gdk::Display::default().unwrap();
let clipboard = display.clipboard();
clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
button.set_icon_name("emblem-ok-symbolic");
button.set_css_classes(&["success"]);
let icon = self.hostname_copy_icon.clone();
icon.set_icon_name(Some("emblem-ok-symbolic"));
icon.set_css_classes(&["success"]);
glib::spawn_future_local(clone!(
#[weak]
button,
icon,
async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
icon.set_icon_name(Some("edit-copy-symbolic"));
icon.set_css_classes(&[]);
}
));
}
}
#[template_callback]
fn handle_copy_fingerprint(&self, button: &Button) {
let fingerprint: String = self.fingerprint_row.property("subtitle");
let display = gdk::Display::default().unwrap();
let clipboard = display.clipboard();
clipboard.set_text(&fingerprint);
button.set_icon_name("emblem-ok-symbolic");
button.set_css_classes(&["success"]);
glib::spawn_future_local(clone!(
#[weak]
button,
async move {
glib::timeout_future_seconds(1).await;
button.set_icon_name("edit-copy-symbolic");
button.set_css_classes(&[]);
}
));
}
#[template_callback]
fn handle_port_changed(&self, _entry: &Entry) {
self.port_edit_apply.set_visible(true);
@@ -118,6 +147,11 @@ impl Window {
self.obj().request_capture();
}
#[template_callback]
fn handle_add_cert_fingerprint(&self, _button: &Button) {
self.obj().open_fingerprint_dialog();
}
pub fn set_port(&self, port: u16) {
self.port.set(port);
if port == DEFAULT_PORT {
@@ -141,6 +175,7 @@ impl ObjectImpl for Window {
let obj = self.obj();
obj.setup_icon();
obj.setup_clients();
obj.setup_authorized();
}
}

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse-ipc"
description = "library for communication between lan-mouse service and frontends"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -11,6 +11,6 @@ futures = "0.3.30"
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.107"
thiserror = "1.0.63"
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
tokio-stream = { version = "0.1.15", features = ["io-util"] }

View File

@@ -1,5 +1,5 @@
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
env::VarError,
fmt::Display,
io,
@@ -33,7 +33,7 @@ pub enum ConnectionError {
}
#[derive(Debug, Error)]
pub enum ListenerCreationError {
pub enum IpcListenerCreationError {
#[error("could not determine socket-path: `{0}`")]
SocketPath(#[from] SocketPathError),
#[error("service already running!")]
@@ -51,7 +51,7 @@ pub enum IpcError {
#[error(transparent)]
Connection(#[from] ConnectionError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
Listen(#[from] IpcListenerCreationError),
}
pub const DEFAULT_PORT: u16 = 4242;
@@ -65,6 +65,17 @@ pub enum Position {
Bottom,
}
impl Position {
pub fn opposite(&self) -> Self {
match self {
Position::Left => Position::Right,
Position::Right => Position::Left,
Position::Top => Position::Bottom,
Position::Bottom => Position::Top,
}
}
}
#[derive(Debug, Error)]
#[error("not a valid position: {pos}")]
pub struct PositionParseError {
@@ -150,7 +161,7 @@ pub struct ClientState {
/// This should generally be the socket address where data
/// was last received from.
pub active_addr: Option<SocketAddr>,
/// tracks whether or not the client is responding to pings
/// tracks whether or not the client is available for emulation
pub alive: bool,
/// ips from dns
pub dns_ips: Vec<IpAddr>,
@@ -186,6 +197,14 @@ pub enum FrontendEvent {
CaptureStatus(Status),
/// emulation status
EmulationStatus(Status),
/// authorized public key fingerprints have been updated
AuthorizedUpdated(HashMap<String, String>),
/// public key fingerprint of this device
PublicKeyFingerprint(String),
/// incoming connected
IncomingConnected(String, SocketAddr, Position),
/// incoming disconnected
IncomingDisconnected(SocketAddr),
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
@@ -218,9 +237,13 @@ pub enum FrontendRequest {
EnableEmulation,
/// synchronize all state
Sync,
/// authorize fingerprint (description, fingerprint)
AuthorizeKey(String, String),
/// remove fingerprint (fingerprint)
RemoveAuthorizedKey(String),
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub enum Status {
#[default]
Disabled,

View File

@@ -20,7 +20,7 @@ use tokio::net::TcpListener;
#[cfg(windows)]
use tokio::net::TcpStream;
use crate::{FrontendEvent, FrontendRequest, IpcError, ListenerCreationError};
use crate::{FrontendEvent, FrontendRequest, IpcError, IpcListenerCreationError};
pub struct AsyncFrontendListener {
#[cfg(windows)]
@@ -40,7 +40,7 @@ pub struct AsyncFrontendListener {
}
impl AsyncFrontendListener {
pub async fn new() -> Result<Self, ListenerCreationError> {
pub async fn new() -> Result<Self, IpcListenerCreationError> {
#[cfg(unix)]
let (socket_path, listener) = {
let socket_path = crate::default_socket_path()?;
@@ -51,7 +51,7 @@ impl AsyncFrontendListener {
// of lan-mouse is already running
match UnixStream::connect(&socket_path).await {
// connected -> lan-mouse is already running
Ok(_) => return Err(ListenerCreationError::AlreadyRunning),
Ok(_) => return Err(IpcListenerCreationError::AlreadyRunning),
// lan-mouse is not running but a socket was left behind
Err(e) => {
log::debug!("{socket_path:?}: {e} - removing left behind socket");
@@ -63,9 +63,9 @@ impl AsyncFrontendListener {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(ListenerCreationError::AlreadyRunning)
return Err(IpcListenerCreationError::AlreadyRunning)
}
Err(e) => return Err(ListenerCreationError::Bind(e)),
Err(e) => return Err(IpcListenerCreationError::Bind(e)),
};
(socket_path, listener)
};
@@ -75,9 +75,9 @@ impl AsyncFrontendListener {
Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(ListenerCreationError::AlreadyRunning)
return Err(IpcListenerCreationError::AlreadyRunning)
}
Err(e) => return Err(ListenerCreationError::Bind(e)),
Err(e) => return Err(IpcListenerCreationError::Bind(e)),
};
let adapter = Self {

View File

@@ -1,13 +1,13 @@
[package]
name = "lan-mouse-proto"
description = "network protocol for lan-mouse"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
num_enum = "0.7.2"
thiserror = "1.0.61"
input-event = { path = "../input-event", version = "0.2.1" }
thiserror = "2.0.0"
input-event = { path = "../input-event", version = "0.3.0" }
paste = "1.0"

View File

@@ -2,7 +2,7 @@ use input_event::{Event as InputEvent, KeyboardEvent, PointerEvent};
use num_enum::{IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
use paste::paste;
use std::{
fmt::{Debug, Display},
fmt::{Debug, Display, Formatter},
mem::size_of,
};
use thiserror::Error;
@@ -18,14 +18,39 @@ pub enum ProtocolError {
/// event type does not exist
#[error("invalid event id: `{0}`")]
InvalidEventId(#[from] TryFromPrimitiveError<EventType>),
/// position type does not exist
#[error("invalid event id: `{0}`")]
InvalidPosition(#[from] TryFromPrimitiveError<Position>),
}
/// Position of a client
#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum Position {
Left,
Right,
Top,
Bottom,
}
impl Display for Position {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let pos = match self {
Position::Left => "left",
Position::Right => "right",
Position::Top => "top",
Position::Bottom => "bottom",
};
write!(f, "{pos}")
}
}
/// main lan-mouse protocol event type
#[derive(Clone, Copy, Debug)]
pub enum ProtoEvent {
/// notify a client that the cursor entered its region
/// notify a client that the cursor entered its region at the given position
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
Enter(u32),
Enter(Position),
/// notify a client that the cursor left its region
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
Leave(u32),
@@ -36,19 +61,25 @@ pub enum ProtoEvent {
/// Ping event for tracking unresponsive clients.
/// A client has to respond with [`ProtoEvent::Pong`].
Ping,
/// Response to [`ProtoEvent::Ping`]
Pong,
/// Response to [`ProtoEvent::Ping`], true if emulation is enabled / available
Pong(bool),
}
impl Display for ProtoEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ProtoEvent::Enter(s) => write!(f, "Enter({s})"),
ProtoEvent::Leave(s) => write!(f, "Leave({s})"),
ProtoEvent::Ack(s) => write!(f, "Ack({s})"),
ProtoEvent::Input(e) => write!(f, "{e}"),
ProtoEvent::Ping => write!(f, "ping"),
ProtoEvent::Pong => write!(f, "pong"),
ProtoEvent::Pong(alive) => {
write!(
f,
"pong: {}",
if *alive { "alive" } else { "not available" }
)
}
}
}
}
@@ -85,7 +116,7 @@ impl ProtoEvent {
},
},
ProtoEvent::Ping => EventType::Ping,
ProtoEvent::Pong => EventType::Pong,
ProtoEvent::Pong(_) => EventType::Pong,
ProtoEvent::Enter(_) => EventType::Enter,
ProtoEvent::Leave(_) => EventType::Leave,
ProtoEvent::Ack(_) => EventType::Ack,
@@ -139,8 +170,8 @@ impl TryFrom<[u8; MAX_EVENT_SIZE]> for ProtoEvent {
},
))),
EventType::Ping => Ok(Self::Ping),
EventType::Pong => Ok(Self::Pong),
EventType::Enter => Ok(Self::Enter(decode_u32(&mut buf)?)),
EventType::Pong => Ok(Self::Pong(decode_u8(&mut buf)? != 0)),
EventType::Enter => Ok(Self::Enter(decode_u8(&mut buf)?.try_into()?)),
EventType::Leave => Ok(Self::Leave(decode_u32(&mut buf)?)),
EventType::Ack => Ok(Self::Ack(decode_u32(&mut buf)?)),
}
@@ -203,8 +234,8 @@ impl From<ProtoEvent> for ([u8; MAX_EVENT_SIZE], usize) {
},
},
ProtoEvent::Ping => {}
ProtoEvent::Pong => {}
ProtoEvent::Enter(serial) => encode_u32(buf, len, serial),
ProtoEvent::Pong(alive) => encode_u8(buf, len, alive as u8),
ProtoEvent::Enter(pos) => encode_u8(buf, len, pos as u8),
ProtoEvent::Leave(serial) => encode_u32(buf, len, serial),
ProtoEvent::Ack(serial) => encode_u32(buf, len, serial),
}

BIN
screenshots/dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
screenshots/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

418
src/capture.rs Normal file
View File

@@ -0,0 +1,418 @@
use std::{
cell::{Cell, RefCell},
rc::Rc,
time::{Duration, Instant},
};
use futures::StreamExt;
use input_capture::{
CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
};
use input_event::scancode;
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{spawn_local, JoinHandle};
use tokio_util::sync::CancellationToken;
use crate::connect::LanMouseConnection;
pub(crate) struct Capture {
cancellation_token: CancellationToken,
request_tx: Sender<CaptureRequest>,
task: JoinHandle<()>,
event_rx: Receiver<ICaptureEvent>,
}
pub(crate) enum ICaptureEvent {
/// a client was entered
CaptureBegin(CaptureHandle),
/// capture disabled
CaptureDisabled,
/// capture disabled
CaptureEnabled,
/// A (new) client was entered.
/// In contrast to [`ICaptureEvent::CaptureBegin`] this
/// event is only triggered when the capture was
/// explicitly released in the meantime by
/// either the remote client leaving its device region,
/// a new device entering the screen or the release bind.
ClientEntered(u64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CaptureType {
/// a normal input capture
Default,
/// A capture only interested in [`CaptureEvent::Begin`] events.
/// The capture is released immediately, if there is no
/// Default capture at the same position.
EnterOnly,
}
#[derive(Clone, Copy, Debug)]
enum CaptureRequest {
/// capture must release the mouse
Release,
/// add a capture client
Create(CaptureHandle, Position, CaptureType),
/// destory a capture client
Destroy(CaptureHandle),
/// reenable input capture
Reenable,
}
impl Capture {
pub(crate) fn new(
backend: Option<input_capture::Backend>,
conn: LanMouseConnection,
release_bind: Vec<scancode::Linux>,
) -> Self {
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new();
let capture_task = CaptureTask {
active_client: None,
backend,
cancellation_token: cancellation_token.clone(),
captures: Default::default(),
conn,
event_tx,
request_rx,
release_bind: Rc::new(RefCell::new(release_bind)),
state: Default::default(),
};
let task = spawn_local(capture_task.run());
Self {
cancellation_token,
request_tx,
task,
event_rx,
}
}
pub(crate) fn reenable(&self) {
self.request_tx
.send(CaptureRequest::Reenable)
.expect("channel closed");
}
pub(crate) async fn terminate(&mut self) {
self.cancellation_token.cancel();
log::debug!("terminating capture");
if let Err(e) = (&mut self.task).await {
log::warn!("{e}");
}
}
pub(crate) fn create(
&self,
handle: CaptureHandle,
pos: lan_mouse_ipc::Position,
capture_type: CaptureType,
) {
let pos = to_capture_pos(pos);
self.request_tx
.send(CaptureRequest::Create(handle, pos, capture_type))
.expect("channel closed");
}
pub(crate) fn destroy(&self, handle: CaptureHandle) {
self.request_tx
.send(CaptureRequest::Destroy(handle))
.expect("channel closed");
}
pub(crate) fn release(&self) {
self.request_tx
.send(CaptureRequest::Release)
.expect("channel closed");
}
pub(crate) async fn event(&mut self) -> ICaptureEvent {
self.event_rx.recv().await.expect("channel closed")
}
}
/// debounce a statement `$st`, i.e. the statement is executed only if the
/// time since the previous execution is at least `$dur`.
/// `$prev` is used to keep track of this timestamp
macro_rules! debounce {
($prev:ident, $dur:expr, $st:stmt) => {
let exec = match $prev.get() {
None => true,
Some(instant) if instant.elapsed() > $dur => true,
_ => false,
};
if exec {
$prev.replace(Some(Instant::now()));
$st
}
};
}
struct CaptureTask {
active_client: Option<CaptureHandle>,
backend: Option<input_capture::Backend>,
cancellation_token: CancellationToken,
captures: Vec<(CaptureHandle, Position, CaptureType)>,
conn: LanMouseConnection,
event_tx: Sender<ICaptureEvent>,
release_bind: Rc<RefCell<Vec<scancode::Linux>>>,
request_rx: Receiver<CaptureRequest>,
state: State,
}
impl CaptureTask {
fn add_capture(&mut self, handle: CaptureHandle, pos: Position, capture_type: CaptureType) {
self.captures.push((handle, pos, capture_type));
}
fn remove_capture(&mut self, handle: CaptureHandle) {
self.captures.retain(|&(h, ..)| handle != h);
}
fn is_default_capture_at(&self, pos: Position) -> bool {
self.captures
.iter()
.any(|&(_, p, t)| p == pos && t == CaptureType::Default)
}
fn get_pos(&self, handle: CaptureHandle) -> Position {
self.captures
.iter()
.find(|(h, ..)| *h == handle)
.expect("no such capture")
.1
}
fn get_type(&self, handle: CaptureHandle) -> CaptureType {
self.captures
.iter()
.find(|(h, ..)| *h == handle)
.expect("no such capture")
.2
}
async fn run(mut self) {
loop {
if let Err(e) = self.do_capture().await {
log::warn!("input capture exited: {e}");
}
loop {
tokio::select! {
r = self.request_rx.recv() => match r.expect("channel closed") {
CaptureRequest::Reenable => break,
CaptureRequest::Create(h, p, t) => self.add_capture(h, p, t),
CaptureRequest::Destroy(h) => self.remove_capture(h),
CaptureRequest::Release => { /* nothing to do */ }
},
_ = self.cancellation_token.cancelled() => return,
}
}
}
}
async fn do_capture(&mut self) -> Result<(), InputCaptureError> {
/* allow cancelling capture request */
let mut capture = tokio::select! {
r = InputCapture::new(self.backend) => r?,
_ = self.cancellation_token.cancelled() => return Ok(()),
};
let _capture_guard = DropGuard::new(
self.event_tx.clone(),
ICaptureEvent::CaptureEnabled,
ICaptureEvent::CaptureDisabled,
);
/* create barriers for active clients */
let r = self.create_captures(&mut capture).await;
if let Err(e) = r {
capture.terminate().await?;
return Err(e.into());
}
let r = self.do_capture_session(&mut capture).await;
// FIXME replace with async drop when stabilized
capture.terminate().await?;
r
}
async fn create_captures(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> {
let captures = self.captures.clone();
for (handle, pos, _type) in captures {
tokio::select! {
r = capture.create(handle, pos) => r?,
_ = self.cancellation_token.cancelled() => return Ok(()),
}
}
Ok(())
}
async fn do_capture_session(
&mut self,
capture: &mut InputCapture,
) -> Result<(), InputCaptureError> {
loop {
tokio::select! {
event = capture.next() => match event {
Some(event) => self.handle_capture_event(capture, event?).await?,
None => return Ok(()),
},
(handle, event) = self.conn.recv() => {
if let Some(active) = self.active_client {
if handle != active {
// we only care about events coming from the client we are currently connected to
// only `Ack` and `Leave` are relevant
continue
}
}
match event {
// connection acknowlegded => set state to Sending
ProtoEvent::Ack(_) => {
log::info!("client {handle} acknowledged the connection!");
self.state = State::Sending;
}
// client disconnected
ProtoEvent::Leave(_) => {
log::info!("releasing capture: left remote client device region");
self.release_capture(capture).await?;
},
_ => {}
}
},
e = self.request_rx.recv() => match e.expect("channel closed") {
CaptureRequest::Reenable => { /* already active */ },
CaptureRequest::Release => self.release_capture(capture).await?,
CaptureRequest::Create(h, p, t) => {
self.add_capture(h, p, t);
capture.create(h, p).await?;
}
CaptureRequest::Destroy(h) => {
self.remove_capture(h);
capture.destroy(h).await?;
}
},
_ = self.cancellation_token.cancelled() => break,
}
}
Ok(())
}
async fn handle_capture_event(
&mut self,
capture: &mut InputCapture,
event: (CaptureHandle, CaptureEvent),
) -> Result<(), CaptureError> {
let (handle, event) = event;
log::trace!("({handle}): {event:?}");
if capture.keys_pressed(&self.release_bind.borrow()) {
log::info!("releasing capture: release-bind pressed");
return self.release_capture(capture).await;
}
if event == CaptureEvent::Begin {
self.event_tx
.send(ICaptureEvent::CaptureBegin(handle))
.expect("channel closed");
}
// enter only capture (for incoming connections)
if self.get_type(handle) == CaptureType::EnterOnly {
// if there is no active outgoing connection at the current capture,
// we release the capture
if !self.is_default_capture_at(self.get_pos(handle)) {
log::info!("releasing capture: no active client at this position");
capture.release().await?;
}
// we dont care about events from incoming handles except for releasing the capture
return Ok(());
}
// activated a new client
if event == CaptureEvent::Begin && Some(handle) != self.active_client {
self.state = State::WaitingForAck;
self.active_client.replace(handle);
self.event_tx
.send(ICaptureEvent::ClientEntered(handle))
.expect("channel closed");
}
let opposite_pos = to_proto_pos(self.get_pos(handle).opposite());
let event = match event {
CaptureEvent::Begin => ProtoEvent::Enter(opposite_pos),
CaptureEvent::Input(e) => match self.state {
// connection not acknowledged, repeat `Enter` event
State::WaitingForAck => ProtoEvent::Enter(opposite_pos),
State::Sending => ProtoEvent::Input(e),
},
};
if let Err(e) = self.conn.send(event, handle).await {
const DUR: Duration = Duration::from_millis(500);
debounce!(PREV_LOG, DUR, log::warn!("releasing capture: {e}"));
capture.release().await?;
}
Ok(())
}
async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> {
self.active_client.take();
capture.release().await
}
}
thread_local! {
static PREV_LOG: Cell<Option<Instant>> = const { Cell::new(None) };
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum State {
#[default]
WaitingForAck,
Sending,
}
fn to_capture_pos(pos: lan_mouse_ipc::Position) -> input_capture::Position {
match pos {
lan_mouse_ipc::Position::Left => input_capture::Position::Left,
lan_mouse_ipc::Position::Right => input_capture::Position::Right,
lan_mouse_ipc::Position::Top => input_capture::Position::Top,
lan_mouse_ipc::Position::Bottom => input_capture::Position::Bottom,
}
}
fn to_proto_pos(pos: input_capture::Position) -> lan_mouse_proto::Position {
match pos {
input_capture::Position::Left => lan_mouse_proto::Position::Left,
input_capture::Position::Right => lan_mouse_proto::Position::Right,
input_capture::Position::Top => lan_mouse_proto::Position::Top,
input_capture::Position::Bottom => lan_mouse_proto::Position::Bottom,
}
}
struct DropGuard<T> {
tx: Sender<T>,
on_drop: Option<T>,
}
impl<T> DropGuard<T> {
fn new(tx: Sender<T>, on_new: T, on_drop: T) -> Self {
tx.send(on_new).expect("channel closed");
let on_drop = Some(on_drop);
Self { tx, on_drop }
}
}
impl<T> Drop for DropGuard<T> {
fn drop(&mut self) {
self.tx
.send(self.on_drop.take().expect("item"))
.expect("channel closed");
}
}

View File

@@ -1,18 +1,63 @@
use std::net::SocketAddr;
use std::{
cell::RefCell,
collections::HashSet,
net::{IpAddr, SocketAddr},
rc::Rc,
};
use slab::Slab;
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
#[derive(Default)]
#[derive(Clone, Default)]
pub struct ClientManager {
clients: Slab<(ClientConfig, ClientState)>,
clients: Rc<RefCell<Slab<(ClientConfig, ClientState)>>>,
}
impl ClientManager {
/// add a new client to this manager
pub fn add_client(&mut self) -> ClientHandle {
self.clients.insert(Default::default()) as ClientHandle
pub fn add_client(&self) -> ClientHandle {
self.clients.borrow_mut().insert(Default::default()) as ClientHandle
}
/// set the config of the given client
pub fn set_config(&self, handle: ClientHandle, config: ClientConfig) {
if let Some((c, _)) = self.clients.borrow_mut().get_mut(handle as usize) {
*c = config;
}
}
/// set the state of the given client
pub fn set_state(&self, handle: ClientHandle, state: ClientState) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
*s = state;
}
}
/// activate the given client
/// returns, whether the client was activated
pub fn activate_client(&self, handle: ClientHandle) -> bool {
let mut clients = self.clients.borrow_mut();
match clients.get_mut(handle as usize) {
Some((_, s)) if !s.active => {
s.active = true;
true
}
_ => false,
}
}
/// deactivate the given client
/// returns, whether the client was deactivated
pub fn deactivate_client(&self, handle: ClientHandle) -> bool {
let mut clients = self.clients.borrow_mut();
match clients.get_mut(handle as usize) {
Some((_, s)) if s.active => {
s.active = false;
true
}
_ => false,
}
}
/// find a client by its address
@@ -20,6 +65,7 @@ impl ClientManager {
// since there shouldn't be more than a handful of clients at any given
// time this is likely faster than using a HashMap
self.clients
.borrow()
.iter()
.find_map(|(k, (_, s))| {
if s.active && s.ips.contains(&addr.ip()) {
@@ -31,8 +77,10 @@ impl ClientManager {
.map(|p| p as ClientHandle)
}
pub fn find_client(&self, pos: Position) -> Option<ClientHandle> {
/// get the client at the given position
pub fn client_at(&self, pos: Position) -> Option<ClientHandle> {
self.clients
.borrow()
.iter()
.find_map(|(k, (c, s))| {
if s.active && c.pos == pos {
@@ -44,31 +92,176 @@ impl ClientManager {
.map(|p| p as ClientHandle)
}
pub(crate) fn get_hostname(&self, handle: ClientHandle) -> Option<String> {
self.clients
.borrow_mut()
.get_mut(handle as usize)
.and_then(|(c, _)| c.hostname.clone())
}
/// get the position of the corresponding client
pub(crate) fn get_pos(&self, handle: ClientHandle) -> Option<Position> {
self.clients
.borrow()
.get(handle as usize)
.map(|(c, _)| c.pos)
}
/// remove a client from the list
pub fn remove_client(&mut self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
pub fn remove_client(&self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
// remove id from occupied ids
self.clients.try_remove(client as usize)
self.clients.borrow_mut().try_remove(client as usize)
}
// returns an immutable reference to the client state corresponding to `client`
pub fn get(&self, handle: ClientHandle) -> Option<&(ClientConfig, ClientState)> {
self.clients.get(handle as usize)
/// get the config & state of the given client
pub fn get_state(&self, handle: ClientHandle) -> Option<(ClientConfig, ClientState)> {
self.clients.borrow().get(handle as usize).cloned()
}
/// returns a mutable reference to the client state corresponding to `client`
pub fn get_mut(&mut self, handle: ClientHandle) -> Option<&mut (ClientConfig, ClientState)> {
self.clients.get_mut(handle as usize)
/// get the current config & state of all clients
pub fn get_client_states(&self) -> Vec<(ClientHandle, ClientConfig, ClientState)> {
self.clients
.borrow()
.iter()
.map(|(k, v)| (k as ClientHandle, v.0.clone(), v.1.clone()))
.collect()
}
pub fn get_client_states(
&self,
) -> impl Iterator<Item = (ClientHandle, &(ClientConfig, ClientState))> {
self.clients.iter().map(|(k, v)| (k as ClientHandle, v))
/// update the fix ips of the client
pub fn set_fix_ips(&self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
if let Some((c, _)) = self.clients.borrow_mut().get_mut(handle as usize) {
c.fix_ips = fix_ips
}
self.update_ips(handle);
}
pub fn get_client_states_mut(
&mut self,
) -> impl Iterator<Item = (ClientHandle, &mut (ClientConfig, ClientState))> {
self.clients.iter_mut().map(|(k, v)| (k as ClientHandle, v))
/// update the dns-ips of the client
pub fn set_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
s.dns_ips = dns_ips
}
self.update_ips(handle);
}
fn update_ips(&self, handle: ClientHandle) {
if let Some((c, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
s.ips = c
.fix_ips
.iter()
.cloned()
.chain(s.dns_ips.iter().cloned())
.collect::<HashSet<_>>();
}
}
/// update the hostname of the given client
/// this automatically clears the active ip address and ips from dns
pub fn set_hostname(&self, handle: ClientHandle, hostname: Option<String>) -> bool {
let mut clients = self.clients.borrow_mut();
let Some((c, s)) = clients.get_mut(handle as usize) else {
return false;
};
// hostname changed
if c.hostname != hostname {
c.hostname = hostname;
s.active_addr = None;
s.dns_ips.clear();
drop(clients);
self.update_ips(handle);
true
} else {
false
}
}
/// update the port of the client
pub(crate) fn set_port(&self, handle: ClientHandle, port: u16) {
match self.clients.borrow_mut().get_mut(handle as usize) {
Some((c, s)) if c.port != port => {
c.port = port;
s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
_ => {}
};
}
/// update the position of the client
/// returns true, if a change in capture position is required (pos changed & client is active)
pub(crate) fn set_pos(&self, handle: ClientHandle, pos: Position) -> bool {
match self.clients.borrow_mut().get_mut(handle as usize) {
Some((c, s)) if c.pos != pos => {
log::info!("update pos {handle} {} -> {}", c.pos, pos);
c.pos = pos;
s.active
}
_ => false,
}
}
/// set resolving status of the client
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
s.resolving = status;
}
}
/// get the enter hook command
pub(crate) fn get_enter_cmd(&self, handle: ClientHandle) -> Option<String> {
self.clients
.borrow()
.get(handle as usize)
.and_then(|(c, _)| c.cmd.clone())
}
/// returns all clients that are currently active
pub(crate) fn active_clients(&self) -> Vec<ClientHandle> {
self.clients
.borrow()
.iter()
.filter(|(_, (_, s))| s.active)
.map(|(h, _)| h as ClientHandle)
.collect()
}
pub(crate) fn set_active_addr(&self, handle: ClientHandle, addr: Option<SocketAddr>) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
s.active_addr = addr;
}
}
pub(crate) fn set_alive(&self, handle: ClientHandle, alive: bool) {
if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
s.alive = alive;
}
}
pub(crate) fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
self.clients
.borrow()
.get(handle as usize)
.and_then(|(_, s)| s.active_addr)
}
pub(crate) fn alive(&self, handle: ClientHandle) -> bool {
self.clients
.borrow()
.get(handle as usize)
.map(|(_, s)| s.alive)
.unwrap_or(false)
}
pub(crate) fn get_port(&self, handle: ClientHandle) -> Option<u16> {
self.clients
.borrow()
.get(handle as usize)
.map(|(c, _)| c.port)
}
pub(crate) fn get_ips(&self, handle: ClientHandle) -> Option<HashSet<IpAddr>> {
self.clients
.borrow()
.get(handle as usize)
.map(|(_, s)| s.ips.clone())
}
}

View File

@@ -1,9 +1,11 @@
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env::{self, VarError};
use std::fmt::Display;
use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::{collections::HashSet, io};
use thiserror::Error;
use toml;
@@ -22,10 +24,12 @@ pub struct ConfigToml {
pub port: Option<u16>,
pub frontend: Option<Frontend>,
pub release_bind: Option<Vec<scancode::Linux>>,
pub cert_path: Option<PathBuf>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
pub authorized_fingerprints: Option<HashMap<String, String>>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -40,9 +44,8 @@ pub struct TomlClient {
}
impl ConfigToml {
pub fn new(path: &str) -> Result<ConfigToml, ConfigError> {
pub fn new(path: &Path) -> Result<ConfigToml, ConfigError> {
let config = fs::read_to_string(path)?;
log::info!("using config: \"{path}\"");
Ok(toml::from_str::<_>(&config)?)
}
}
@@ -81,31 +84,41 @@ struct CliArgs {
/// emulation backend override
#[arg(long)]
emulation_backend: Option<EmulationBackend>,
/// path to non-default certificate location
#[arg(long)]
cert_path: Option<PathBuf>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum CaptureBackend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
#[serde(rename = "input-capture-portal")]
InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
#[serde(rename = "layer-shell")]
LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
#[serde(rename = "windows")]
Windows,
#[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs,
#[serde(rename = "dummy")]
Dummy,
}
impl Display for CaptureBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => write!(f, "layer-shell"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
CaptureBackend::Windows => write!(f, "windows"),
@@ -119,11 +132,11 @@ impl Display for CaptureBackend {
impl From<CaptureBackend> for input_capture::Backend {
fn from(backend: CaptureBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(unix, feature = "libei_capture", not(target_os = "macos")))]
CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "layer_shell_capture", not(target_os = "macos")))]
CaptureBackend::LayerShell => Self::LayerShell,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[cfg(all(unix, feature = "x11_capture", not(target_os = "macos")))]
CaptureBackend::X11 => Self::X11,
#[cfg(windows)]
CaptureBackend::Windows => Self::Windows,
@@ -136,31 +149,38 @@ impl From<CaptureBackend> for input_capture::Backend {
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
pub enum EmulationBackend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
#[serde(rename = "wlroots")]
Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
#[serde(rename = "libei")]
Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
#[serde(rename = "xdp")]
Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
#[serde(rename = "x11")]
X11,
#[cfg(windows)]
#[serde(rename = "windows")]
Windows,
#[cfg(target_os = "macos")]
#[serde(rename = "macos")]
MacOs,
#[serde(rename = "dummy")]
Dummy,
}
impl From<EmulationBackend> for input_emulation::Backend {
fn from(backend: EmulationBackend) -> Self {
match backend {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => Self::Wlroots,
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => Self::Libei,
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => Self::Xdp,
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => Self::X11,
#[cfg(windows)]
EmulationBackend::Windows => Self::Windows,
@@ -174,13 +194,13 @@ impl From<EmulationBackend> for input_emulation::Backend {
impl Display for EmulationBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
#[cfg(all(unix, feature = "wlroots_emulation", not(target_os = "macos")))]
EmulationBackend::Wlroots => write!(f, "wlroots"),
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
#[cfg(all(unix, feature = "libei_emulation", not(target_os = "macos")))]
EmulationBackend::Libei => write!(f, "libei"),
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
#[cfg(all(unix, feature = "rdp_emulation", not(target_os = "macos")))]
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
#[cfg(all(unix, feature = "x11_emulation", not(target_os = "macos")))]
EmulationBackend::X11 => write!(f, "X11"),
#[cfg(windows)]
EmulationBackend::Windows => write!(f, "windows"),
@@ -193,7 +213,9 @@ impl Display for EmulationBackend {
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
pub enum Frontend {
#[serde(rename = "gtk")]
Gtk,
#[serde(rename = "cli")]
Cli,
}
@@ -209,15 +231,30 @@ impl Default for Frontend {
#[derive(Debug)]
pub struct Config {
/// the path to the configuration file used
pub path: PathBuf,
/// public key fingerprints authorized for connection
pub authorized_fingerprints: HashMap<String, String>,
/// optional input-capture backend override
pub capture_backend: Option<CaptureBackend>,
/// optional input-emulation backend override
pub emulation_backend: Option<EmulationBackend>,
/// the frontend to use
pub frontend: Frontend,
/// the port to use (initially)
pub port: u16,
/// list of clients
pub clients: Vec<(TomlClient, Position)>,
/// whether or not to run as a daemon
pub daemon: bool,
/// configured release bind
pub release_bind: Vec<scancode::Linux>,
/// test capture instead of running the app
pub test_capture: bool,
/// test emulation instead of running the app
pub test_emulation: bool,
/// path to the tls certificate to use
pub cert_path: PathBuf,
}
pub struct ConfigClient {
@@ -245,27 +282,32 @@ const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
impl Config {
pub fn new() -> Result<Self, ConfigError> {
let args = CliArgs::parse();
let config_file = "config.toml";
const CONFIG_FILE_NAME: &str = "config.toml";
const CERT_FILE_NAME: &str = "lan-mouse.pem";
#[cfg(unix)]
let config_path = {
let xdg_config_home =
env::var("XDG_CONFIG_HOME").unwrap_or(format!("{}/.config", env::var("HOME")?));
format!("{xdg_config_home}/lan-mouse/{config_file}")
format!("{xdg_config_home}/lan-mouse/")
};
#[cfg(not(unix))]
let config_path = {
let app_data =
env::var("LOCALAPPDATA").unwrap_or(format!("{}/.config", env::var("USERPROFILE")?));
format!("{app_data}\\lan-mouse\\{config_file}")
format!("{app_data}\\lan-mouse\\")
};
// --config <file> overrules default location
let config_path = args.config.unwrap_or(config_path);
let config_path = PathBuf::from(config_path);
let config_file = config_path.join(CONFIG_FILE_NAME);
let config_toml = match ConfigToml::new(config_path.as_str()) {
// --config <file> overrules default location
let config_file = args.config.map(PathBuf::from).unwrap_or(config_file);
let mut config_toml = match ConfigToml::new(&config_file) {
Err(e) => {
log::warn!("{config_path}: {e}");
log::warn!("{config_file:?}: {e}");
log::warn!("Continuing without config file ...");
None
}
@@ -295,6 +337,16 @@ impl Config {
.emulation_backend
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
let cert_path = args
.cert_path
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
.unwrap_or(config_path.join(CERT_FILE_NAME));
let authorized_fingerprints = config_toml
.as_mut()
.and_then(|c| std::mem::take(&mut c.authorized_fingerprints))
.unwrap_or_default();
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
@@ -317,6 +369,8 @@ impl Config {
let test_emulation = args.test_emulation;
Ok(Config {
path: config_path,
authorized_fingerprints,
capture_backend,
emulation_backend,
daemon,
@@ -326,6 +380,7 @@ impl Config {
release_bind,
test_capture,
test_emulation,
cert_path,
})
}

281
src/connect.rs Normal file
View File

@@ -0,0 +1,281 @@
use crate::client::ClientManager;
use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT};
use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{channel, Receiver, Sender};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
io,
net::SocketAddr,
rc::Rc,
sync::Arc,
time::Duration,
};
use thiserror::Error;
use tokio::{
net::UdpSocket,
sync::Mutex,
task::{spawn_local, JoinSet},
};
use webrtc_dtls::{
config::{Config, ExtendedMasterSecretType},
conn::DTLSConn,
crypto::Certificate,
};
use webrtc_util::Conn;
#[derive(Debug, Error)]
pub(crate) enum LanMouseConnectionError {
#[error(transparent)]
Bind(#[from] io::Error),
#[error(transparent)]
Dtls(#[from] webrtc_dtls::Error),
#[error(transparent)]
Webrtc(#[from] webrtc_util::Error),
#[error("not connected")]
NotConnected,
#[error("emulation is disabled on the target device")]
TargetEmulationDisabled,
#[error("Connection timed out")]
Timeout,
}
const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
async fn connect(
addr: SocketAddr,
cert: Certificate,
) -> Result<(Arc<dyn Conn + Sync + Send>, SocketAddr), (SocketAddr, LanMouseConnectionError)> {
log::info!("connecting to {addr} ...");
let conn = Arc::new(
UdpSocket::bind("0.0.0.0:0")
.await
.map_err(|e| (addr, e.into()))?,
);
conn.connect(addr).await.map_err(|e| (addr, e.into()))?;
let config = Config {
certificates: vec![cert],
server_name: "ignored".to_owned(),
insecure_skip_verify: true,
extended_master_secret: ExtendedMasterSecretType::Require,
..Default::default()
};
let timeout = tokio::time::sleep(DEFAULT_CONNECTION_TIMEOUT);
tokio::select! {
_ = timeout => Err((addr, LanMouseConnectionError::Timeout)),
result = DTLSConn::new(conn, config, true, None) => match result {
Ok(dtls_conn) => Ok((Arc::new(dtls_conn), addr)),
Err(e) => Err((addr, e.into())),
}
}
}
async fn connect_any(
addrs: &[SocketAddr],
cert: Certificate,
) -> Result<(Arc<dyn Conn + Send + Sync>, SocketAddr), LanMouseConnectionError> {
let mut joinset = JoinSet::new();
for &addr in addrs {
joinset.spawn_local(connect(addr, cert.clone()));
}
loop {
match joinset.join_next().await {
None => return Err(LanMouseConnectionError::NotConnected),
Some(r) => match r.expect("join error") {
Ok(conn) => return Ok(conn),
Err((a, e)) => {
log::warn!("failed to connect to {a}: `{e}`")
}
},
};
}
}
pub(crate) struct LanMouseConnection {
cert: Certificate,
client_manager: ClientManager,
conns: Rc<Mutex<HashMap<SocketAddr, Arc<dyn Conn + Send + Sync>>>>,
connecting: Rc<Mutex<HashSet<ClientHandle>>>,
recv_rx: Receiver<(ClientHandle, ProtoEvent)>,
recv_tx: Sender<(ClientHandle, ProtoEvent)>,
ping_response: Rc<RefCell<HashSet<SocketAddr>>>,
}
impl LanMouseConnection {
pub(crate) fn new(cert: Certificate, client_manager: ClientManager) -> Self {
let (recv_tx, recv_rx) = channel();
Self {
cert,
client_manager,
conns: Default::default(),
connecting: Default::default(),
recv_rx,
recv_tx,
ping_response: Default::default(),
}
}
pub(crate) async fn recv(&mut self) -> (ClientHandle, ProtoEvent) {
self.recv_rx.recv().await.expect("channel closed")
}
pub(crate) async fn send(
&self,
event: ProtoEvent,
handle: ClientHandle,
) -> Result<(), LanMouseConnectionError> {
let (buf, len): ([u8; MAX_EVENT_SIZE], usize) = event.into();
let buf = &buf[..len];
if let Some(addr) = self.client_manager.active_addr(handle) {
let conn = {
let conns = self.conns.lock().await;
conns.get(&addr).cloned()
};
if let Some(conn) = conn {
if !self.client_manager.alive(handle) {
return Err(LanMouseConnectionError::TargetEmulationDisabled);
}
match conn.send(buf).await {
Ok(_) => {}
Err(e) => {
log::warn!("client {handle} failed to send: {e}");
disconnect(&self.client_manager, handle, addr, &self.conns).await;
}
}
log::trace!("{event} >->->->->- {addr}");
return Ok(());
}
}
// check if we are already trying to connect
let mut connecting = self.connecting.lock().await;
if !connecting.contains(&handle) {
connecting.insert(handle);
// connect in the background
spawn_local(connect_to_handle(
self.client_manager.clone(),
self.cert.clone(),
handle,
self.conns.clone(),
self.connecting.clone(),
self.recv_tx.clone(),
self.ping_response.clone(),
));
}
Err(LanMouseConnectionError::NotConnected)
}
}
async fn connect_to_handle(
client_manager: ClientManager,
cert: Certificate,
handle: ClientHandle,
conns: Rc<Mutex<HashMap<SocketAddr, Arc<dyn Conn + Send + Sync>>>>,
connecting: Rc<Mutex<HashSet<ClientHandle>>>,
tx: Sender<(ClientHandle, ProtoEvent)>,
ping_response: Rc<RefCell<HashSet<SocketAddr>>>,
) -> Result<(), LanMouseConnectionError> {
log::info!("client {handle} connecting ...");
// sending did not work, figure out active conn.
if let Some(addrs) = client_manager.get_ips(handle) {
let port = client_manager.get_port(handle).unwrap_or(DEFAULT_PORT);
let addrs = addrs
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect::<Vec<_>>();
log::info!("client ({handle}) connecting ... (ips: {addrs:?})");
let res = connect_any(&addrs, cert).await;
let (conn, addr) = match res {
Ok(c) => c,
Err(e) => {
connecting.lock().await.remove(&handle);
return Err(e);
}
};
log::info!("client ({handle}) connected @ {addr}");
client_manager.set_active_addr(handle, Some(addr));
conns.lock().await.insert(addr, conn.clone());
connecting.lock().await.remove(&handle);
// poll connection for active
spawn_local(ping_pong(addr, conn.clone(), ping_response.clone()));
// receiver
spawn_local(receive_loop(
client_manager,
handle,
addr,
conn,
conns,
tx,
ping_response.clone(),
));
return Ok(());
}
connecting.lock().await.remove(&handle);
Err(LanMouseConnectionError::NotConnected)
}
async fn ping_pong(
addr: SocketAddr,
conn: Arc<dyn Conn + Send + Sync>,
ping_response: Rc<RefCell<HashSet<SocketAddr>>>,
) {
loop {
let (buf, len) = ProtoEvent::Ping.into();
if let Err(e) = conn.send(&buf[..len]).await {
log::warn!("{addr}: send error `{e}`, closing connection");
let _ = conn.close().await;
break;
}
log::trace!("PING >->->->->- {addr}");
tokio::time::sleep(Duration::from_millis(500)).await;
if !ping_response.borrow_mut().remove(&addr) {
log::warn!("{addr} did not respond, closing connection");
let _ = conn.close().await;
return;
}
}
}
async fn receive_loop(
client_manager: ClientManager,
handle: ClientHandle,
addr: SocketAddr,
conn: Arc<dyn Conn + Send + Sync>,
conns: Rc<Mutex<HashMap<SocketAddr, Arc<dyn Conn + Send + Sync>>>>,
tx: Sender<(ClientHandle, ProtoEvent)>,
ping_response: Rc<RefCell<HashSet<SocketAddr>>>,
) {
let mut buf = [0u8; MAX_EVENT_SIZE];
while conn.recv(&mut buf).await.is_ok() {
if let Ok(event) = buf.try_into() {
log::trace!("{addr} <==<==<== {event}");
match event {
ProtoEvent::Pong(b) => {
client_manager.set_active_addr(handle, Some(addr));
client_manager.set_alive(handle, b);
ping_response.borrow_mut().insert(addr);
}
event => tx.send((handle, event)).expect("channel closed"),
}
}
}
log::warn!("recv error");
disconnect(&client_manager, handle, addr, &conns).await;
}
async fn disconnect(
client_manager: &ClientManager,
handle: ClientHandle,
addr: SocketAddr,
conns: &Mutex<HashMap<SocketAddr, Arc<dyn Conn + Send + Sync>>>,
) {
log::warn!("client ({handle}) @ {addr} connection closed");
conns.lock().await.remove(&addr);
client_manager.set_active_addr(handle, None);
let active: Vec<SocketAddr> = conns.lock().await.keys().copied().collect();
log::info!("active connections: {active:?}");
}

71
src/crypto.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::fs;
use std::io::{self, BufWriter, Read, Write};
use std::path::Path;
use std::{fs::File, io::BufReader};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use sha2::{Digest, Sha256};
use thiserror::Error;
use webrtc_dtls::crypto::Certificate;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Dtls(#[from] webrtc_dtls::Error),
}
pub fn generate_fingerprint(cert: &[u8]) -> String {
let mut hash = Sha256::new();
hash.update(cert);
let bytes = hash
.finalize()
.iter()
.map(|x| format!("{x:02x}"))
.collect::<Vec<_>>();
bytes.join(":").to_lowercase()
}
pub fn certificate_fingerprint(cert: &Certificate) -> String {
let certificate = cert.certificate.first().expect("certificate missing");
generate_fingerprint(certificate)
}
/// load certificate from file
pub fn load_certificate(path: &Path) -> Result<Certificate, Error> {
let f = File::open(path)?;
let mut reader = BufReader::new(f);
let mut pem = String::new();
reader.read_to_string(&mut pem)?;
Ok(Certificate::from_pem(pem.as_str())?)
}
pub(crate) fn load_or_generate_key_and_cert(path: &Path) -> Result<Certificate, Error> {
if path.exists() && path.is_file() {
Ok(load_certificate(path)?)
} else {
generate_key_and_cert(path)
}
}
pub(crate) fn generate_key_and_cert(path: &Path) -> Result<Certificate, Error> {
let cert = Certificate::generate_self_signed(["ignored".to_owned()])?;
let serialized = cert.serialize_pem();
let parent = path.parent().expect("is a path");
fs::create_dir_all(parent)?;
let f = File::create(path)?;
#[cfg(unix)]
{
let mut perm = f.metadata()?.permissions();
perm.set_mode(0o400); /* r-- --- --- */
f.set_permissions(perm)?;
}
/* FIXME windows permissions */
let mut writer = BufWriter::new(f);
writer.write_all(serialized.as_bytes())?;
Ok(cert)
}

View File

@@ -1,63 +1,106 @@
use local_channel::mpsc::Receiver;
use std::net::IpAddr;
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{spawn_local, JoinHandle};
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use tokio_util::sync::CancellationToken;
use crate::server::Server;
use lan_mouse_ipc::ClientHandle;
pub(crate) struct DnsResolver {
cancellation_token: CancellationToken,
task: Option<JoinHandle<()>>,
request_tx: Sender<DnsRequest>,
event_rx: Receiver<DnsEvent>,
}
struct DnsRequest {
handle: ClientHandle,
hostname: String,
}
pub(crate) enum DnsEvent {
Resolving(ClientHandle),
Resolved(ClientHandle, String, Result<Vec<IpAddr>, ResolveError>),
}
struct DnsTask {
resolver: TokioAsyncResolver,
dns_request: Receiver<ClientHandle>,
request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken,
}
impl DnsResolver {
pub(crate) fn new(dns_request: Receiver<ClientHandle>) -> Result<Self, ResolveError> {
pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
Ok(Self {
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new();
let dns_task = DnsTask {
resolver,
dns_request,
request_rx,
event_tx,
cancellation_token: cancellation_token.clone(),
};
let task = Some(spawn_local(dns_task.run()));
Ok(Self {
cancellation_token,
task,
event_rx,
request_tx,
})
}
async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, ResolveError> {
let response = self.resolver.lookup_ip(host).await?;
for ip in response.iter() {
log::info!("{host}: adding ip {ip}");
}
Ok(response.iter().collect())
pub(crate) fn resolve(&self, handle: ClientHandle, hostname: String) {
let request = DnsRequest { handle, hostname };
self.request_tx.send(request).expect("channel closed");
}
pub(crate) async fn run(mut self, server: Server) {
pub(crate) async fn event(&mut self) -> DnsEvent {
self.event_rx.recv().await.expect("channel closed")
}
pub(crate) async fn terminate(&mut self) {
self.cancellation_token.cancel();
self.task.take().expect("task").await.expect("join error");
}
}
impl DnsTask {
async fn run(mut self) {
let cancellation_token = self.cancellation_token.clone();
tokio::select! {
_ = server.cancelled() => {},
_ = self.do_dns(&server) => {},
_ = self.do_dns() => {},
_ = cancellation_token.cancelled() => {},
}
}
async fn do_dns(&mut self, server: &Server) {
loop {
let handle = self.dns_request.recv().await.expect("channel closed");
async fn do_dns(&mut self) {
while let Some(dns_request) = self.request_rx.recv().await {
let DnsRequest { handle, hostname } = dns_request;
/* update resolving status */
let hostname = match server.get_hostname(handle) {
Some(hostname) => hostname,
None => continue,
};
self.event_tx
.send(DnsEvent::Resolving(handle))
.expect("channel closed");
log::info!("resolving ({handle}) `{hostname}` ...");
server.set_resolving(handle, true);
/* spawn task for dns request */
let event_tx = self.event_tx.clone();
let resolver = self.resolver.clone();
let cancellation_token = self.cancellation_token.clone();
let ips = match self.resolve(&hostname).await {
Ok(ips) => ips,
Err(e) => {
log::warn!("could not resolve host '{hostname}': {e}");
vec![]
tokio::task::spawn_local(async move {
tokio::select! {
ips = resolver.lookup_ip(&hostname) => {
let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
event_tx
.send(DnsEvent::Resolved(handle, hostname, ips))
.expect("channel closed");
}
_ = cancellation_token.cancelled() => {},
}
};
server.update_dns_ips(handle, ips);
server.set_resolving(handle, false);
});
}
}
}

407
src/emulation.rs Normal file
View File

@@ -0,0 +1,407 @@
use crate::listen::{LanMouseListener, ListenerCreationError};
use futures::StreamExt;
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
use input_event::Event;
use lan_mouse_proto::{Position, ProtoEvent};
use local_channel::mpsc::{channel, Receiver, Sender};
use std::{
cell::Cell,
collections::HashMap,
net::SocketAddr,
rc::Rc,
time::{Duration, Instant},
};
use tokio::{
select,
task::{spawn_local, JoinHandle},
};
/// emulation handling events received from a listener
pub(crate) struct Emulation {
task: JoinHandle<()>,
request_tx: Sender<EmulationRequest>,
event_rx: Receiver<EmulationEvent>,
}
pub(crate) enum EmulationEvent {
/// new connection
Connected {
/// address of the connection
addr: SocketAddr,
/// position of the connection
pos: lan_mouse_ipc::Position,
/// certificate fingerprint of the connection
fingerprint: String,
},
/// connection closed
Disconnected { addr: SocketAddr },
/// the port of the listener has changed
PortChanged(Result<u16, ListenerCreationError>),
/// emulation was disabled
EmulationDisabled,
/// emulation was enabled
EmulationEnabled,
/// capture should be released
ReleaseNotify,
}
enum EmulationRequest {
Reenable,
Release(SocketAddr),
ChangePort(u16),
Terminate,
}
impl Emulation {
pub(crate) fn new(
backend: Option<input_emulation::Backend>,
listener: LanMouseListener,
) -> Self {
let emulation_proxy = EmulationProxy::new(backend);
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let emulation_task = ListenTask {
listener,
emulation_proxy,
request_rx,
event_tx,
};
let task = spawn_local(emulation_task.run());
Self {
task,
request_tx,
event_rx,
}
}
pub(crate) fn send_leave_event(&self, addr: SocketAddr) {
self.request_tx
.send(EmulationRequest::Release(addr))
.expect("channel closed");
}
pub(crate) fn reenable(&self) {
self.request_tx
.send(EmulationRequest::Reenable)
.expect("channel closed");
}
pub(crate) fn request_port_change(&self, port: u16) {
self.request_tx
.send(EmulationRequest::ChangePort(port))
.expect("channel closed")
}
pub(crate) async fn event(&mut self) -> EmulationEvent {
self.event_rx.recv().await.expect("channel closed")
}
/// wait for termination
pub(crate) async fn terminate(&mut self) {
log::debug!("terminating emulation");
self.request_tx
.send(EmulationRequest::Terminate)
.expect("channel closed");
if let Err(e) = (&mut self.task).await {
log::warn!("{e}");
}
}
}
struct ListenTask {
listener: LanMouseListener,
emulation_proxy: EmulationProxy,
request_rx: Receiver<EmulationRequest>,
event_tx: Sender<EmulationEvent>,
}
impl ListenTask {
async fn run(mut self) {
let mut interval = tokio::time::interval(Duration::from_secs(5));
let mut last_response = HashMap::new();
loop {
select! {
e = self.listener.next() => {
let (event, addr) = match e {
Some(e) => e,
None => break,
};
log::trace!("{event} <-<-<-<-<- {addr}");
last_response.insert(addr, Instant::now());
match event {
ProtoEvent::Enter(pos) => {
if let Some(fingerprint) = self.listener.get_certificate_fingerprint(addr).await {
log::info!("releasing capture: {addr} entered this device");
self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed");
self.listener.reply(addr, ProtoEvent::Ack(0)).await;
self.event_tx.send(EmulationEvent::Connected{addr, pos: to_ipc_pos(pos), fingerprint}).expect("channel closed");
}
}
ProtoEvent::Leave(_) => {
self.emulation_proxy.remove(addr);
self.listener.reply(addr, ProtoEvent::Ack(0)).await;
}
ProtoEvent::Input(event) => self.emulation_proxy.consume(event, addr),
ProtoEvent::Ping => self.listener.reply(addr, ProtoEvent::Pong(self.emulation_proxy.emulation_active.get())).await,
_ => {}
}
}
event = self.emulation_proxy.event() => {
self.event_tx.send(event).expect("channel closed");
}
request = self.request_rx.recv() => match request.expect("channel closed") {
// reenable emulation
EmulationRequest::Reenable => self.emulation_proxy.reenable(),
// notify the other end that we hit a barrier (should release capture)
EmulationRequest::Release(addr) => self.listener.reply(addr, ProtoEvent::Leave(0)).await,
EmulationRequest::ChangePort(port) => {
self.listener.request_port_change(port);
let result = self.listener.port_changed().await;
self.event_tx.send(EmulationEvent::PortChanged(result)).expect("channel closed");
}
EmulationRequest::Terminate => break,
},
_ = interval.tick() => {
last_response.retain(|&addr,instant| {
if instant.elapsed() > Duration::from_secs(1) {
log::warn!("releasing keys: {addr} not responding!");
self.emulation_proxy.remove(addr);
self.event_tx.send(EmulationEvent::Disconnected { addr }).expect("channel closed");
false
} else {
true
}
});
}
}
}
self.listener.terminate().await;
self.emulation_proxy.terminate().await;
}
}
/// proxy handling the actual input emulation,
/// discarding events when it is disabled
pub(crate) struct EmulationProxy {
emulation_active: Rc<Cell<bool>>,
exit_requested: Rc<Cell<bool>>,
request_tx: Sender<ProxyRequest>,
event_rx: Receiver<EmulationEvent>,
task: JoinHandle<()>,
}
enum ProxyRequest {
Input(Event, SocketAddr),
Remove(SocketAddr),
Terminate,
Reenable,
}
impl EmulationProxy {
fn new(backend: Option<input_emulation::Backend>) -> Self {
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
let emulation_active = Rc::new(Cell::new(false));
let exit_requested = Rc::new(Cell::new(false));
let emulation_task = EmulationTask {
backend,
exit_requested: exit_requested.clone(),
request_rx,
event_tx,
handles: Default::default(),
next_id: 0,
};
let task = spawn_local(emulation_task.run());
Self {
emulation_active,
exit_requested,
request_tx,
task,
event_rx,
}
}
async fn event(&mut self) -> EmulationEvent {
let event = self.event_rx.recv().await.expect("channel closed");
if let EmulationEvent::EmulationEnabled = event {
self.emulation_active.replace(true);
}
if let EmulationEvent::EmulationDisabled = event {
self.emulation_active.replace(false);
}
event
}
fn consume(&self, event: Event, addr: SocketAddr) {
// ignore events if emulation is currently disabled
if self.emulation_active.get() {
self.request_tx
.send(ProxyRequest::Input(event, addr))
.expect("channel closed");
}
}
fn remove(&self, addr: SocketAddr) {
self.request_tx
.send(ProxyRequest::Remove(addr))
.expect("channel closed");
}
fn reenable(&self) {
self.request_tx
.send(ProxyRequest::Reenable)
.expect("channel closed");
}
async fn terminate(&mut self) {
self.exit_requested.replace(true);
self.request_tx
.send(ProxyRequest::Terminate)
.expect("channel closed");
let _ = (&mut self.task).await;
}
}
struct EmulationTask {
backend: Option<input_emulation::Backend>,
exit_requested: Rc<Cell<bool>>,
request_rx: Receiver<ProxyRequest>,
event_tx: Sender<EmulationEvent>,
handles: HashMap<SocketAddr, EmulationHandle>,
next_id: EmulationHandle,
}
impl EmulationTask {
async fn run(mut self) {
loop {
if let Err(e) = self.do_emulation().await {
log::warn!("input emulation exited: {e}");
}
if self.exit_requested.get() {
break;
}
// wait for reenable request
loop {
match self.request_rx.recv().await.expect("channel closed") {
ProxyRequest::Reenable => break,
ProxyRequest::Terminate => return,
ProxyRequest::Input(..) => { /* emulation inactive => ignore */ }
ProxyRequest::Remove(..) => { /* emulation inactive => ignore */ }
}
}
}
}
async fn do_emulation(&mut self) -> Result<(), InputEmulationError> {
log::info!("creating input emulation ...");
let mut emulation = tokio::select! {
r = InputEmulation::new(self.backend) => r?,
// allow termination event while requesting input emulation
_ = wait_for_termination(&mut self.request_rx) => return Ok(()),
};
// used to send enabled and disabled events
let _emulation_guard = DropGuard::new(
self.event_tx.clone(),
EmulationEvent::EmulationEnabled,
EmulationEvent::EmulationDisabled,
);
// create active handles
if let Err(e) = self.create_clients(&mut emulation).await {
emulation.terminate().await;
return Err(e);
}
let res = self.do_emulation_session(&mut emulation).await;
// FIXME replace with async drop when stabilized
emulation.terminate().await;
res
}
async fn create_clients(
&mut self,
emulation: &mut InputEmulation,
) -> Result<(), InputEmulationError> {
for handle in self.handles.values() {
tokio::select! {
_ = emulation.create(*handle) => {},
_ = wait_for_termination(&mut self.request_rx) => return Ok(()),
}
}
Ok(())
}
async fn do_emulation_session(
&mut self,
emulation: &mut InputEmulation,
) -> Result<(), InputEmulationError> {
loop {
tokio::select! {
e = self.request_rx.recv() => match e.expect("channel closed") {
ProxyRequest::Input(event, addr) => {
let handle = match self.handles.get(&addr) {
Some(&handle) => handle,
None => {
let handle = self.next_id;
self.next_id += 1;
emulation.create(handle).await;
self.handles.insert(addr, handle);
handle
}
};
emulation.consume(event, handle).await?;
},
ProxyRequest::Remove(addr) => {
if let Some(handle) = self.handles.remove(&addr) {
emulation.destroy(handle).await;
}
}
ProxyRequest::Terminate => break Ok(()),
ProxyRequest::Reenable => continue,
},
}
}
}
}
fn to_ipc_pos(pos: Position) -> lan_mouse_ipc::Position {
match pos {
Position::Left => lan_mouse_ipc::Position::Left,
Position::Right => lan_mouse_ipc::Position::Right,
Position::Top => lan_mouse_ipc::Position::Top,
Position::Bottom => lan_mouse_ipc::Position::Bottom,
}
}
async fn wait_for_termination(rx: &mut Receiver<ProxyRequest>) {
loop {
match rx.recv().await.expect("channel closed") {
ProxyRequest::Terminate => return,
ProxyRequest::Input(_, _) => continue,
ProxyRequest::Remove(_) => continue,
ProxyRequest::Reenable => continue,
}
}
}
struct DropGuard<T> {
tx: Sender<T>,
on_drop: Option<T>,
}
impl<T> DropGuard<T> {
fn new(tx: Sender<T>, on_new: T, on_drop: T) -> Self {
tx.send(on_new).expect("channel closed");
let on_drop = Some(on_drop);
Self { tx, on_drop }
}
}
impl<T> Drop for DropGuard<T> {
fn drop(&mut self) {
self.tx
.send(self.on_drop.take().expect("item"))
.expect("channel closed");
}
}

View File

@@ -1,7 +1,11 @@
mod capture;
pub mod capture_test;
pub mod client;
pub mod config;
pub mod dns;
pub mod server;
pub mod capture_test;
mod connect;
mod crypto;
mod dns;
mod emulation;
pub mod emulation_test;
mod listen;
pub mod service;

224
src/listen.rs Normal file
View File

@@ -0,0 +1,224 @@
use futures::{Stream, StreamExt};
use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{channel, Receiver, Sender};
use rustls::pki_types::CertificateDer;
use std::{
collections::HashMap,
net::SocketAddr,
rc::Rc,
sync::{Arc, RwLock},
time::Duration,
};
use thiserror::Error;
use tokio::{
sync::Mutex,
task::{spawn_local, JoinHandle},
};
use webrtc_dtls::{
config::{ClientAuthType::RequireAnyClientCert, Config, ExtendedMasterSecretType},
conn::DTLSConn,
crypto::Certificate,
listener::listen,
};
use webrtc_util::{conn::Listener, Conn, Error};
use crate::crypto;
#[derive(Error, Debug)]
pub enum ListenerCreationError {
#[error(transparent)]
WebrtcUtil(#[from] webrtc_util::Error),
#[error(transparent)]
WebrtcDtls(#[from] webrtc_dtls::Error),
}
type ArcConn = Arc<dyn Conn + Send + Sync>;
pub(crate) struct LanMouseListener {
listen_rx: Receiver<(ProtoEvent, SocketAddr)>,
listen_tx: Sender<(ProtoEvent, SocketAddr)>,
listen_task: JoinHandle<()>,
conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>>,
request_port_change: Sender<u16>,
port_changed: Receiver<Result<u16, ListenerCreationError>>,
}
type VerifyPeerCertificateFn = Arc<
dyn (Fn(&[Vec<u8>], &[CertificateDer<'static>]) -> Result<(), webrtc_dtls::Error>)
+ Send
+ Sync,
>;
impl LanMouseListener {
pub(crate) async fn new(
port: u16,
cert: Certificate,
authorized_keys: Arc<RwLock<HashMap<String, String>>>,
) -> Result<Self, ListenerCreationError> {
let (listen_tx, listen_rx) = channel();
let (request_port_change, mut request_port_change_rx) = channel();
let (port_changed_tx, port_changed) = channel();
let authorized = authorized_keys.clone();
let verify_peer_certificate: Option<VerifyPeerCertificateFn> = Some(Arc::new(
move |certs: &[Vec<u8>], _chains: &[CertificateDer<'static>]| {
assert!(certs.len() == 1);
let fingerprints = certs
.iter()
.map(|c| crypto::generate_fingerprint(c))
.collect::<Vec<_>>();
if authorized
.read()
.expect("lock")
.contains_key(&fingerprints[0])
{
Ok(())
} else {
Err(webrtc_dtls::Error::ErrVerifyDataMismatch)
}
},
));
let cfg = Config {
certificates: vec![cert.clone()],
extended_master_secret: ExtendedMasterSecretType::Require,
client_auth: RequireAnyClientCert,
verify_peer_certificate,
..Default::default()
};
let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port);
let mut listener = listen(listen_addr, cfg.clone()).await?;
let conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>> = Rc::new(Mutex::new(Vec::new()));
let conns_clone = conns.clone();
let tx = listen_tx.clone();
let listen_task: JoinHandle<()> = spawn_local(async move {
loop {
let sleep = tokio::time::sleep(Duration::from_secs(2));
tokio::select! {
/* workaround for https://github.com/webrtc-rs/webrtc/issues/614 */
_ = sleep => continue,
c = listener.accept() => match c {
Ok((conn, addr)) => {
log::info!("dtls client connected, ip: {addr}");
let mut conns = conns_clone.lock().await;
conns.push((addr, conn.clone()));
spawn_local(read_loop(conns_clone.clone(), addr, conn, tx.clone()));
},
Err(e) => log::warn!("accept: {e}"),
},
port = request_port_change_rx.recv() => {
let port = port.expect("channel closed");
let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port);
match listen(listen_addr, cfg.clone()).await {
Ok(new_listener) => {
let _ = listener.close().await;
listener = new_listener;
port_changed_tx.send(Ok(port)).expect("channel closed");
}
Err(e) => {
log::warn!("unable to change port: {e}");
port_changed_tx.send(Err(e.into())).expect("channel closed");
}
};
},
};
}
});
Ok(Self {
conns,
listen_rx,
listen_tx,
listen_task,
port_changed,
request_port_change,
})
}
pub(crate) fn request_port_change(&mut self, port: u16) {
self.request_port_change.send(port).expect("channel closed");
}
pub(crate) async fn port_changed(&mut self) -> Result<u16, ListenerCreationError> {
self.port_changed.recv().await.expect("channel closed")
}
pub(crate) async fn terminate(&mut self) {
self.listen_task.abort();
let conns = self.conns.lock().await;
for (_, conn) in conns.iter() {
let _ = conn.close().await;
}
self.listen_tx.close();
}
pub(crate) async fn reply(&self, addr: SocketAddr, event: ProtoEvent) {
log::trace!("reply {event} >=>=>=>=>=> {addr}");
let (buf, len): ([u8; MAX_EVENT_SIZE], usize) = event.into();
let conns = self.conns.lock().await;
for (a, conn) in conns.iter() {
if *a == addr {
let _ = conn.send(&buf[..len]).await;
}
}
}
pub(crate) async fn get_certificate_fingerprint(&self, addr: SocketAddr) -> Option<String> {
if let Some(conn) = self
.conns
.lock()
.await
.iter()
.find(|(a, _)| *a == addr)
.map(|(_, c)| c.clone())
{
let conn: &DTLSConn = conn.as_any().downcast_ref().expect("dtls conn");
let certs = conn.connection_state().await.peer_certificates;
let cert = certs.first()?;
let fingerprint = crypto::generate_fingerprint(cert);
Some(fingerprint)
} else {
None
}
}
}
impl Stream for LanMouseListener {
type Item = (ProtoEvent, SocketAddr);
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.listen_rx.poll_next_unpin(cx)
}
}
async fn read_loop(
conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>>,
addr: SocketAddr,
conn: ArcConn,
dtls_tx: Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), Error> {
let mut b = [0u8; MAX_EVENT_SIZE];
while conn.recv(&mut b).await.is_ok() {
match b.try_into() {
Ok(event) => dtls_tx.send((event, addr)).expect("channel closed"),
Err(e) => {
log::warn!("error receiving event: {e}");
break;
}
}
}
log::info!("dtls client disconnected {:?}", addr);
let mut conns = conns.lock().await;
let index = conns
.iter()
.position(|(a, _)| *a == addr)
.expect("connection not found");
conns.remove(index);
Ok(())
}

View File

@@ -5,9 +5,9 @@ use lan_mouse::{
capture_test,
config::{Config, ConfigError, Frontend},
emulation_test,
server::{Server, ServiceError},
service::{Service, ServiceError},
};
use lan_mouse_ipc::IpcError;
use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
use std::{
future::Future,
io,
@@ -32,7 +32,7 @@ enum LanMouseError {
Emulation(#[from] InputEmulationError),
}
pub fn main() {
fn main() {
// init logging
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
env_logger::init_from_env(env);
@@ -46,16 +46,18 @@ pub fn main() {
fn run() -> Result<(), LanMouseError> {
// parse config file + cli args
let config = Config::new()?;
log::debug!("{config:?}");
log::info!("release bind: {:?}", config.release_bind);
if config.test_capture {
run_async(capture_test::run(config))?;
} else if config.test_emulation {
run_async(emulation_test::run(config))?;
} else if config.daemon {
// if daemon is specified we run the service
run_async(run_service(config))?;
match run_async(run_service(config)) {
Err(LanMouseError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"),
r => r?,
}
} else {
// otherwise start the service as a child process and
// run a frontend
@@ -100,8 +102,10 @@ fn start_service() -> Result<Child, io::Error> {
}
async fn run_service(config: Config) -> Result<(), ServiceError> {
log::info!("using config: {:?}", config.path);
log::info!("Press {:?} to release the mouse", config.release_bind);
Server::new(config).run().await?;
let mut service = Service::new(config).await?;
service.run().await?;
log::info!("service exited!");
Ok(())
}

View File

@@ -1,562 +0,0 @@
use capture_task::CaptureRequest;
use emulation_task::EmulationRequest;
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use local_channel::mpsc::{channel, Sender};
use log;
use std::{
cell::{Cell, RefCell},
collections::{HashSet, VecDeque},
io,
net::{IpAddr, SocketAddr},
rc::Rc,
};
use thiserror::Error;
use tokio::{join, signal, sync::Notify};
use tokio_util::sync::CancellationToken;
use crate::{client::ClientManager, config::Config, dns::DnsResolver};
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
ListenerCreationError, Position, Status,
};
mod capture_task;
mod emulation_task;
mod network_task;
mod ping_task;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
/// Currently sending events to another device
Sending,
/// Currently receiving events from other devices
Receiving,
/// Entered the deadzone of another device but waiting
/// for acknowledgement (Leave event) from the device
AwaitAck,
}
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
Listen(#[from] ListenerCreationError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Clone)]
pub struct Server {
active_client: Rc<Cell<Option<ClientHandle>>>,
pub(crate) client_manager: Rc<RefCell<ClientManager>>,
port: Rc<Cell<u16>>,
state: Rc<Cell<State>>,
release_bind: Vec<input_event::scancode::Linux>,
notifies: Rc<Notifies>,
config: Rc<Config>,
pending_frontend_events: Rc<RefCell<VecDeque<FrontendEvent>>>,
capture_status: Rc<Cell<Status>>,
emulation_status: Rc<Cell<Status>>,
}
#[derive(Default)]
struct Notifies {
capture: Notify,
emulation: Notify,
ping: Notify,
port_changed: Notify,
frontend_event_pending: Notify,
cancel: CancellationToken,
}
impl Server {
pub fn new(config: Config) -> Self {
let active_client = Rc::new(Cell::new(None));
let client_manager = Rc::new(RefCell::new(ClientManager::default()));
let state = Rc::new(Cell::new(State::Receiving));
let port = Rc::new(Cell::new(config.port));
for config_client in config.get_clients() {
let client = ClientConfig {
hostname: config_client.hostname,
fix_ips: config_client.ips.into_iter().collect(),
port: config_client.port,
pos: config_client.pos,
cmd: config_client.enter_hook,
};
let state = ClientState {
active: config_client.active,
ips: HashSet::from_iter(client.fix_ips.iter().cloned()),
..Default::default()
};
let mut client_manager = client_manager.borrow_mut();
let handle = client_manager.add_client();
let c = client_manager.get_mut(handle).expect("invalid handle");
*c = (client, state);
}
// task notification tokens
let notifies = Rc::new(Notifies::default());
let release_bind = config.release_bind.clone();
let config = Rc::new(config);
Self {
config,
active_client,
client_manager,
port,
state,
release_bind,
notifies,
pending_frontend_events: Rc::new(RefCell::new(VecDeque::new())),
capture_status: Default::default(),
emulation_status: Default::default(),
}
}
pub async fn run(&mut self) -> Result<(), ServiceError> {
// create frontend communication adapter, exit if already running
let mut frontend = match AsyncFrontendListener::new().await {
Ok(f) => f,
Err(ListenerCreationError::AlreadyRunning) => {
log::info!("service already running, exiting");
return Ok(());
}
e => e?,
};
let (capture_tx, capture_rx) = channel(); /* requests for input capture */
let (emulation_tx, emulation_rx) = channel(); /* emulation requests */
let (udp_recv_tx, udp_recv_rx) = channel(); /* udp receiver */
let (udp_send_tx, udp_send_rx) = channel(); /* udp sender */
let (dns_tx, dns_rx) = channel(); /* dns requests */
let network = network_task::new(self.clone(), udp_recv_tx.clone(), udp_send_rx).await?;
let capture = capture_task::new(self.clone(), capture_rx, udp_send_tx.clone());
let emulation =
emulation_task::new(self.clone(), emulation_rx, udp_recv_rx, udp_send_tx.clone());
let resolver = DnsResolver::new(dns_rx)?;
let dns_task = tokio::task::spawn_local(resolver.run(self.clone()));
// task that pings clients to see if they are responding
let ping = ping_task::new(
self.clone(),
udp_send_tx.clone(),
emulation_tx.clone(),
capture_tx.clone(),
);
for handle in self.active_clients() {
dns_tx.send(handle).expect("channel closed");
}
loop {
tokio::select! {
request = frontend.next() => {
let request = match request {
Some(Ok(r)) => r,
Some(Err(e)) => {
log::error!("error receiving request: {e}");
continue;
}
None => break,
};
log::debug!("handle frontend request: {request:?}");
self.handle_request(&capture_tx.clone(), &emulation_tx.clone(), request, &dns_tx);
}
_ = self.notifies.frontend_event_pending.notified() => {
while let Some(event) = {
/* need to drop borrow before next iteration! */
let event = self.pending_frontend_events.borrow_mut().pop_front();
event
} {
frontend.broadcast(event).await;
}
},
_ = self.cancelled() => break,
r = signal::ctrl_c() => {
r.expect("failed to wait for CTRL+C");
break;
}
}
}
log::info!("terminating service");
self.cancel();
let _ = join!(capture, dns_task, emulation, network, ping);
Ok(())
}
fn notify_frontend(&self, event: FrontendEvent) {
self.pending_frontend_events.borrow_mut().push_back(event);
self.notifies.frontend_event_pending.notify_one();
}
fn cancel(&self) {
self.notifies.cancel.cancel();
}
pub(crate) async fn cancelled(&self) {
self.notifies.cancel.cancelled().await
}
fn is_cancelled(&self) -> bool {
self.notifies.cancel.is_cancelled()
}
fn notify_capture(&self) {
log::info!("received capture enable request");
self.notifies.capture.notify_waiters()
}
async fn capture_enabled(&self) {
self.notifies.capture.notified().await
}
fn notify_emulation(&self) {
log::info!("received emulation enable request");
self.notifies.emulation.notify_waiters()
}
async fn emulation_notified(&self) {
self.notifies.emulation.notified().await
}
fn restart_ping_timer(&self) {
self.notifies.ping.notify_waiters()
}
async fn ping_timer_notified(&self) {
self.notifies.ping.notified().await
}
fn request_port_change(&self, port: u16) {
self.port.replace(port);
self.notifies.port_changed.notify_one();
}
fn notify_port_changed(&self, port: u16, msg: Option<String>) {
self.port.replace(port);
self.notify_frontend(FrontendEvent::PortChanged(port, msg));
}
pub(crate) fn client_updated(&self, handle: ClientHandle) {
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn active_clients(&self) -> Vec<ClientHandle> {
self.client_manager
.borrow()
.get_client_states()
.filter(|(_, (_, s))| s.active)
.map(|(h, _)| h)
.collect()
}
fn handle_request(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
event: FrontendRequest,
dns: &Sender<ClientHandle>,
) -> bool {
log::debug!("frontend: {event:?}");
match event {
FrontendRequest::EnableCapture => self.notify_capture(),
FrontendRequest::EnableEmulation => self.notify_emulation(),
FrontendRequest::Create => {
self.add_client();
}
FrontendRequest::Activate(handle, active) => {
if active {
self.activate_client(capture, emulate, handle);
} else {
self.deactivate_client(capture, emulate, handle);
}
}
FrontendRequest::ChangePort(port) => self.request_port_change(port),
FrontendRequest::Delete(handle) => {
self.remove_client(capture, emulate, handle);
self.notify_frontend(FrontendEvent::Deleted(handle));
}
FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
FrontendRequest::UpdateHostname(handle, host) => {
self.update_hostname(handle, host, dns)
}
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, capture, emulate, pos)
}
FrontendRequest::ResolveDns(handle) => dns.send(handle).expect("channel closed"),
FrontendRequest::Sync => {
self.enumerate();
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status.get()));
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status.get()));
self.notify_frontend(FrontendEvent::PortChanged(self.port.get(), None));
}
};
false
}
fn enumerate(&self) {
let clients = self
.client_manager
.borrow()
.get_client_states()
.map(|(h, (c, s))| (h, c.clone(), s.clone()))
.collect();
self.notify_frontend(FrontendEvent::Enumerate(clients));
}
fn add_client(&self) -> ClientHandle {
let handle = self.client_manager.borrow_mut().add_client();
log::info!("added client {handle}");
let (c, s) = self.client_manager.borrow().get(handle).unwrap().clone();
self.notify_frontend(FrontendEvent::Created(handle, c, s));
handle
}
fn deactivate_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
match self.client_manager.borrow_mut().get_mut(handle) {
None => return,
Some((_, s)) if !s.active => return,
Some((_, s)) => s.active = false,
};
let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle));
self.client_updated(handle);
log::info!("deactivated client {handle}");
}
fn activate_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
/* deactivate potential other client at this position */
let pos = match self.client_manager.borrow().get(handle) {
None => return,
Some((_, s)) if s.active => return,
Some((client, _)) => client.pos,
};
let other = self.client_manager.borrow_mut().find_client(pos);
if let Some(other) = other {
self.deactivate_client(capture, emulate, other);
}
/* activate the client */
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.active = true;
} else {
return;
};
/* notify emulation, capture and frontends */
let _ = capture.send(CaptureRequest::Create(handle, to_capture_pos(pos)));
let _ = emulate.send(EmulationRequest::Create(handle));
self.client_updated(handle);
log::info!("activated client {handle} ({pos})");
}
fn remove_client(
&self,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
handle: ClientHandle,
) {
let Some(active) = self
.client_manager
.borrow_mut()
.remove_client(handle)
.map(|(_, s)| s.active)
else {
return;
};
if active {
let _ = capture.send(CaptureRequest::Destroy(handle));
let _ = emulate.send(EmulationRequest::Destroy(handle));
}
}
fn update_pressed_keys(&self, handle: ClientHandle, has_pressed_keys: bool) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.has_pressed_keys = has_pressed_keys;
}
}
fn update_fix_ips(&self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
if let Some((c, _)) = self.client_manager.borrow_mut().get_mut(handle) {
c.fix_ips = fix_ips;
};
self.update_ips(handle);
self.client_updated(handle);
}
pub(crate) fn update_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.dns_ips = dns_ips;
};
self.update_ips(handle);
self.client_updated(handle);
}
fn update_ips(&self, handle: ClientHandle) {
if let Some((c, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.ips = c
.fix_ips
.iter()
.cloned()
.chain(s.dns_ips.iter().cloned())
.collect::<HashSet<_>>();
}
}
fn update_hostname(
&self,
handle: ClientHandle,
hostname: Option<String>,
dns: &Sender<ClientHandle>,
) {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
// hostname changed
if c.hostname != hostname {
c.hostname = hostname;
s.active_addr = None;
s.dns_ips.clear();
drop(client_manager);
self.update_ips(handle);
dns.send(handle).expect("channel closed");
}
self.client_updated(handle);
}
fn update_port(&self, handle: ClientHandle, port: u16) {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
if c.port != port {
c.port = port;
s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
}
fn update_pos(
&self,
handle: ClientHandle,
capture: &Sender<CaptureRequest>,
emulate: &Sender<EmulationRequest>,
pos: Position,
) {
let (changed, active) = {
let mut client_manager = self.client_manager.borrow_mut();
let Some((c, s)) = client_manager.get_mut(handle) else {
return;
};
let changed = c.pos != pos;
if changed {
log::info!("update pos {handle} {} -> {}", c.pos, pos);
}
c.pos = pos;
(changed, s.active)
};
// update state in event input emulator & input capture
if changed {
self.deactivate_client(capture, emulate, handle);
if active {
self.activate_client(capture, emulate, handle);
}
}
}
fn broadcast_client(&self, handle: ClientHandle) {
let client = self.client_manager.borrow().get(handle).cloned();
let event = if let Some((config, state)) = client {
FrontendEvent::State(handle, config, state)
} else {
FrontendEvent::NoSuchClient(handle)
};
self.notify_frontend(event);
}
fn set_emulation_status(&self, status: Status) {
self.emulation_status.replace(status);
let status = FrontendEvent::EmulationStatus(status);
self.notify_frontend(status);
}
fn set_capture_status(&self, status: Status) {
self.capture_status.replace(status);
let status = FrontendEvent::CaptureStatus(status);
self.notify_frontend(status);
}
pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
if let Some((_, s)) = self.client_manager.borrow_mut().get_mut(handle) {
s.resolving = status;
}
self.client_updated(handle);
}
pub(crate) fn get_hostname(&self, handle: ClientHandle) -> Option<String> {
self.client_manager
.borrow_mut()
.get_mut(handle)
.and_then(|(c, _)| c.hostname.clone())
}
fn get_state(&self) -> State {
self.state.get()
}
fn set_state(&self, state: State) {
log::debug!("state => {state:?}");
self.state.replace(state);
}
fn set_active(&self, handle: Option<ClientHandle>) {
log::debug!("active client => {handle:?}");
self.active_client.replace(handle);
}
fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
self.client_manager
.borrow()
.get(handle)
.and_then(|(_, s)| s.active_addr)
}
}
fn to_capture_pos(pos: Position) -> input_capture::Position {
match pos {
Position::Left => input_capture::Position::Left,
Position::Right => input_capture::Position::Right,
Position::Top => input_capture::Position::Top,
Position::Bottom => input_capture::Position::Bottom,
}
}

View File

@@ -1,206 +0,0 @@
use futures::StreamExt;
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{Receiver, Sender};
use std::net::SocketAddr;
use tokio::{process::Command, task::JoinHandle};
use input_capture::{
self, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
};
use crate::server::State;
use lan_mouse_ipc::{ClientHandle, Status};
use super::Server;
#[derive(Clone, Copy, Debug)]
pub(crate) enum CaptureRequest {
/// capture must release the mouse
Release,
/// add a capture client
Create(CaptureHandle, Position),
/// destory a capture client
Destroy(CaptureHandle),
}
pub(crate) fn new(
server: Server,
capture_rx: Receiver<CaptureRequest>,
udp_send: Sender<(ProtoEvent, SocketAddr)>,
) -> JoinHandle<()> {
let backend = server.config.capture_backend.map(|b| b.into());
tokio::task::spawn_local(capture_task(server, backend, udp_send, capture_rx))
}
async fn capture_task(
server: Server,
backend: Option<input_capture::Backend>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
mut notify_rx: Receiver<CaptureRequest>,
) {
loop {
if let Err(e) = do_capture(backend, &server, &sender_tx, &mut notify_rx).await {
log::warn!("input capture exited: {e}");
}
server.set_capture_status(Status::Disabled);
if server.is_cancelled() {
break;
}
// allow cancellation
loop {
tokio::select! {
_ = notify_rx.recv() => continue, /* need to ignore requests here! */
_ = server.capture_enabled() => break,
_ = server.cancelled() => return,
}
}
}
}
async fn do_capture(
backend: Option<input_capture::Backend>,
server: &Server,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
notify_rx: &mut Receiver<CaptureRequest>,
) -> Result<(), InputCaptureError> {
/* allow cancelling capture request */
let mut capture = tokio::select! {
r = InputCapture::new(backend) => r?,
_ = server.cancelled() => return Ok(()),
};
server.set_capture_status(Status::Enabled);
let clients = server.active_clients();
let clients = clients.iter().copied().map(|handle| {
(
handle,
server
.client_manager
.borrow()
.get(handle)
.map(|(c, _)| c.pos)
.expect("no such client"),
)
});
for (handle, pos) in clients {
capture.create(handle, to_capture_pos(pos)).await?;
}
loop {
tokio::select! {
event = capture.next() => match event {
Some(event) => handle_capture_event(server, &mut capture, sender_tx, event?).await?,
None => return Ok(()),
},
e = notify_rx.recv() => {
log::debug!("input capture notify rx: {e:?}");
match e {
Some(e) => match e {
CaptureRequest::Release => {
capture.release().await?;
server.state.replace(State::Receiving);
}
CaptureRequest::Create(h, p) => capture.create(h, p).await?,
CaptureRequest::Destroy(h) => capture.destroy(h).await?,
},
None => break,
}
}
_ = server.cancelled() => break,
}
}
capture.terminate().await?;
Ok(())
}
async fn handle_capture_event(
server: &Server,
capture: &mut InputCapture,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
event: (CaptureHandle, CaptureEvent),
) -> Result<(), CaptureError> {
let (handle, event) = event;
log::trace!("({handle}) {event:?}");
// capture started
if event == CaptureEvent::Begin {
// wait for remote to acknowlegde enter
server.set_state(State::AwaitAck);
server.set_active(Some(handle));
// restart ping timer to release capture if unreachable
server.restart_ping_timer();
// spawn enter hook cmd
spawn_hook_command(server, handle);
}
// release capture if emulation set state to Receiveing
if server.get_state() == State::Receiving {
capture.release().await?;
return Ok(());
}
// check release bind
if capture.keys_pressed(&server.release_bind) {
capture.release().await?;
server.set_state(State::Receiving);
}
if let Some(addr) = server.active_addr(handle) {
let event = match server.get_state() {
State::Sending => match event {
CaptureEvent::Begin => ProtoEvent::Enter(0),
CaptureEvent::Input(e) => ProtoEvent::Input(e),
},
/* send additional enter events until acknowleged */
State::AwaitAck => ProtoEvent::Enter(0),
/* released capture */
State::Receiving => ProtoEvent::Leave(0),
};
sender_tx.send((event, addr)).expect("sender closed");
};
Ok(())
}
fn spawn_hook_command(server: &Server, handle: ClientHandle) {
let Some(cmd) = server
.client_manager
.borrow()
.get(handle)
.and_then(|(c, _)| c.cmd.clone())
else {
return;
};
tokio::task::spawn_local(async move {
log::info!("spawning command!");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute cmd: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
}
});
}
fn to_capture_pos(pos: lan_mouse_ipc::Position) -> input_capture::Position {
match pos {
lan_mouse_ipc::Position::Left => input_capture::Position::Left,
lan_mouse_ipc::Position::Right => input_capture::Position::Right,
lan_mouse_ipc::Position::Top => input_capture::Position::Top,
lan_mouse_ipc::Position::Bottom => input_capture::Position::Bottom,
}
}

View File

@@ -1,188 +0,0 @@
use local_channel::mpsc::{Receiver, Sender};
use std::net::SocketAddr;
use lan_mouse_proto::ProtoEvent;
use tokio::task::JoinHandle;
use lan_mouse_ipc::ClientHandle;
use crate::{client::ClientManager, server::State};
use input_emulation::{self, EmulationError, EmulationHandle, InputEmulation, InputEmulationError};
use lan_mouse_ipc::Status;
use super::{network_task::NetworkError, Server};
#[derive(Clone, Debug)]
pub(crate) enum EmulationRequest {
/// create a new client
Create(EmulationHandle),
/// destroy a client
Destroy(EmulationHandle),
/// input emulation must release keys for client
ReleaseKeys(ClientHandle),
}
pub(crate) fn new(
server: Server,
emulation_rx: Receiver<EmulationRequest>,
udp_rx: Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
) -> JoinHandle<()> {
let emulation_task = emulation_task(server, emulation_rx, udp_rx, sender_tx);
tokio::task::spawn_local(emulation_task)
}
async fn emulation_task(
server: Server,
mut rx: Receiver<EmulationRequest>,
mut udp_rx: Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: Sender<(ProtoEvent, SocketAddr)>,
) {
loop {
if let Err(e) = do_emulation(&server, &mut rx, &mut udp_rx, &sender_tx).await {
log::warn!("input emulation exited: {e}");
}
server.set_emulation_status(Status::Disabled);
if server.is_cancelled() {
break;
}
// allow cancellation
loop {
tokio::select! {
_ = rx.recv() => continue, /* need to ignore requests here! */
_ = server.emulation_notified() => break,
_ = server.cancelled() => return,
}
}
}
}
async fn do_emulation(
server: &Server,
rx: &mut Receiver<EmulationRequest>,
udp_rx: &mut Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), InputEmulationError> {
let backend = server.config.emulation_backend.map(|b| b.into());
log::info!("creating input emulation...");
let mut emulation = tokio::select! {
r = InputEmulation::new(backend) => r?,
_ = server.cancelled() => return Ok(()),
};
server.set_emulation_status(Status::Enabled);
// add clients
for handle in server.active_clients() {
emulation.create(handle).await;
}
let res = do_emulation_session(server, &mut emulation, rx, udp_rx, sender_tx).await;
emulation.terminate().await; // manual drop
res
}
async fn do_emulation_session(
server: &Server,
emulation: &mut InputEmulation,
rx: &mut Receiver<EmulationRequest>,
udp_rx: &mut Receiver<Result<(ProtoEvent, SocketAddr), NetworkError>>,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), InputEmulationError> {
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = udp_rx.recv() => {
let udp_event = match udp_event.expect("channel closed") {
Ok(e) => e,
Err(e) => {
log::warn!("network error: {e}");
continue;
}
};
handle_incoming_event(server, emulation, sender_tx, &mut last_ignored, udp_event).await?;
}
emulate_event = rx.recv() => {
match emulate_event.expect("channel closed") {
EmulationRequest::Create(h) => { let _ = emulation.create(h).await; },
EmulationRequest::Destroy(h) => emulation.destroy(h).await,
EmulationRequest::ReleaseKeys(c) => emulation.release_keys(c).await?,
}
}
_ = server.notifies.cancel.cancelled() => break Ok(()),
}
}
}
async fn handle_incoming_event(
server: &Server,
emulate: &mut InputEmulation,
sender_tx: &Sender<(ProtoEvent, SocketAddr)>,
last_ignored: &mut Option<SocketAddr>,
event: (ProtoEvent, SocketAddr),
) -> Result<(), EmulationError> {
let (event, addr) = event;
log::trace!("{:20} <-<-<-<------ {addr}", event.to_string());
// get client handle for addr
let Some(handle) =
activate_client_if_exists(&mut server.client_manager.borrow_mut(), addr, last_ignored)
else {
return Ok(());
};
match (event, addr) {
(ProtoEvent::Pong, _) => { /* ignore pong events */ }
(ProtoEvent::Ping, addr) => {
let _ = sender_tx.send((ProtoEvent::Pong, addr));
}
(ProtoEvent::Leave(_), _) => emulate.release_keys(handle).await?,
(ProtoEvent::Ack(_), _) => server.set_state(State::Sending),
(ProtoEvent::Enter(_), _) => {
server.set_state(State::Receiving);
sender_tx
.send((ProtoEvent::Ack(0), addr))
.expect("no channel")
}
(ProtoEvent::Input(e), _) => {
if let State::Receiving = server.get_state() {
log::trace!("{event} => emulate");
emulate.consume(e, handle).await?;
let has_pressed_keys = emulate.has_pressed_keys(handle);
server.update_pressed_keys(handle, has_pressed_keys);
if has_pressed_keys {
server.restart_ping_timer();
}
}
}
}
Ok(())
}
fn activate_client_if_exists(
client_manager: &mut ClientManager,
addr: SocketAddr,
last_ignored: &mut Option<SocketAddr>,
) -> Option<ClientHandle> {
let Some(handle) = client_manager.get_client(addr) else {
// log ignored if it is the first event from the client in a series
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr {
log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr);
}
return None;
};
// next event can be logged as ignored again
last_ignored.take();
let (_, client_state) = client_manager.get_mut(handle)?;
// reset ttl for client
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
Some(handle)
}

View File

@@ -1,99 +0,0 @@
use local_channel::mpsc::{Receiver, Sender};
use std::{io, net::SocketAddr};
use thiserror::Error;
use tokio::{net::UdpSocket, task::JoinHandle};
use super::Server;
use lan_mouse_proto::{ProtoEvent, ProtocolError};
pub(crate) async fn new(
server: Server,
udp_recv_tx: Sender<Result<(ProtoEvent, SocketAddr), NetworkError>>,
udp_send_rx: Receiver<(ProtoEvent, SocketAddr)>,
) -> io::Result<JoinHandle<()>> {
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get());
let mut socket = UdpSocket::bind(listen_addr).await?;
Ok(tokio::task::spawn_local(async move {
let mut sender_rx = udp_send_rx;
loop {
let udp_receiver = udp_receiver(&socket, &udp_recv_tx);
let udp_sender = udp_sender(&socket, &mut sender_rx);
tokio::select! {
_ = udp_receiver => break, /* channel closed */
_ = udp_sender => break, /* channel closed */
_ = server.notifies.port_changed.notified() => update_port(&server, &mut socket).await,
_ = server.cancelled() => break, /* cancellation requested */
}
}
}))
}
async fn update_port(server: &Server, socket: &mut UdpSocket) {
let new_port = server.port.get();
let current_port = socket.local_addr().expect("socket not bound").port();
// if port is the same, we dont need to change it
if current_port == new_port {
return;
}
// bind new socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), new_port);
let new_socket = UdpSocket::bind(listen_addr).await;
let err = match new_socket {
Ok(new_socket) => {
*socket = new_socket;
None
}
Err(e) => Some(e.to_string()),
};
// notify frontend of the actual port
let port = socket.local_addr().expect("socket not bound").port();
server.notify_port_changed(port, err);
}
async fn udp_receiver(
socket: &UdpSocket,
receiver_tx: &Sender<Result<(ProtoEvent, SocketAddr), NetworkError>>,
) {
loop {
let event = receive_event(socket).await;
receiver_tx.send(event).expect("channel closed");
}
}
async fn udp_sender(socket: &UdpSocket, rx: &mut Receiver<(ProtoEvent, SocketAddr)>) {
loop {
let (event, addr) = rx.recv().await.expect("channel closed");
if let Err(e) = send_event(socket, event, addr) {
log::warn!("udp send failed: {e}");
};
}
}
#[derive(Debug, Error)]
pub(crate) enum NetworkError {
#[error(transparent)]
Protocol(#[from] ProtocolError),
#[error("network error: `{0}`")]
Io(#[from] io::Error),
}
async fn receive_event(socket: &UdpSocket) -> Result<(ProtoEvent, SocketAddr), NetworkError> {
let mut buf = [0u8; lan_mouse_proto::MAX_EVENT_SIZE];
let (_len, src) = socket.recv_from(&mut buf).await?;
let event = ProtoEvent::try_from(buf)?;
Ok((event, src))
}
fn send_event(sock: &UdpSocket, e: ProtoEvent, addr: SocketAddr) -> Result<usize, NetworkError> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let (data, len): ([u8; lan_mouse_proto::MAX_EVENT_SIZE], usize) = e.into();
// When udp blocks, we dont want to block the event loop.
// Dropping events is better than potentially crashing the input capture.
Ok(sock.try_send_to(&data[..len], addr)?)
}

View File

@@ -1,138 +0,0 @@
use std::{net::SocketAddr, time::Duration};
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::Sender;
use tokio::task::JoinHandle;
use lan_mouse_ipc::ClientHandle;
use super::{capture_task::CaptureRequest, emulation_task::EmulationRequest, Server, State};
const MAX_RESPONSE_TIME: Duration = Duration::from_millis(500);
pub(crate) fn new(
server: Server,
sender_ch: Sender<(ProtoEvent, SocketAddr)>,
emulate_notify: Sender<EmulationRequest>,
capture_notify: Sender<CaptureRequest>,
) -> JoinHandle<()> {
// timer task
tokio::task::spawn_local(async move {
tokio::select! {
_ = server.notifies.cancel.cancelled() => {}
_ = ping_task(&server, sender_ch, emulate_notify, capture_notify) => {}
}
})
}
async fn ping_task(
server: &Server,
sender_ch: Sender<(ProtoEvent, SocketAddr)>,
emulate_notify: Sender<EmulationRequest>,
capture_notify: Sender<CaptureRequest>,
) {
loop {
// wait for wake up signal
server.ping_timer_notified().await;
loop {
let receiving = server.state.get() == State::Receiving;
let (ping_clients, ping_addrs) = {
let mut client_manager = server.client_manager.borrow_mut();
let ping_clients: Vec<ClientHandle> = if receiving {
// if receiving we care about clients with pressed keys
client_manager
.get_client_states()
.filter(|(_, (_, s))| s.has_pressed_keys)
.map(|(h, _)| h)
.collect()
} else {
// if sending we care about the active client
server.active_client.get().iter().cloned().collect()
};
// get relevant socket addrs for clients
let ping_addrs: Vec<SocketAddr> = {
ping_clients
.iter()
.flat_map(|&h| client_manager.get(h))
.flat_map(|(c, s)| {
if s.alive && s.active_addr.is_some() {
vec![s.active_addr.unwrap()]
} else {
s.ips
.iter()
.cloned()
.map(|ip| SocketAddr::new(ip, c.port))
.collect()
}
})
.collect()
};
// reset alive
for (_, (_, s)) in client_manager.get_client_states_mut() {
s.alive = false;
}
(ping_clients, ping_addrs)
};
if receiving && ping_clients.is_empty() {
// receiving and no client has pressed keys
// -> no need to keep pinging
break;
}
// ping clients
for addr in ping_addrs {
if sender_ch.send((ProtoEvent::Ping, addr)).is_err() {
break;
}
}
// give clients time to resond
if receiving {
log::trace!(
"waiting {MAX_RESPONSE_TIME:?} for response from client with pressed keys ..."
);
} else {
log::trace!(
"state: {:?} => waiting {MAX_RESPONSE_TIME:?} for client to respond ...",
server.state.get()
);
}
tokio::time::sleep(MAX_RESPONSE_TIME).await;
// when anything is received from a client,
// the alive flag gets set
let unresponsive_clients: Vec<_> = {
let client_manager = server.client_manager.borrow();
ping_clients
.iter()
.filter_map(|&h| match client_manager.get(h) {
Some((_, s)) if !s.alive => Some(h),
_ => None,
})
.collect()
};
// we may not be receiving anymore but we should respond
// to the original state and not the "new" one
if receiving {
for h in unresponsive_clients {
log::warn!("device not responding, releasing keys!");
let _ = emulate_notify.send(EmulationRequest::ReleaseKeys(h));
}
} else {
// release pointer if the active client has not responded
if !unresponsive_clients.is_empty() {
log::warn!("client not responding, releasing pointer!");
server.state.replace(State::Receiving);
let _ = capture_notify.send(CaptureRequest::Release);
}
}
}
}
}

513
src/service.rs Normal file
View File

@@ -0,0 +1,513 @@
use crate::{
capture::{Capture, CaptureType, ICaptureEvent},
client::ClientManager,
config::Config,
connect::LanMouseConnection,
crypto,
dns::{DnsEvent, DnsResolver},
emulation::{Emulation, EmulationEvent},
listen::{LanMouseListener, ListenerCreationError},
};
use futures::StreamExt;
use hickory_resolver::error::ResolveError;
use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status,
};
use log;
use std::{
collections::{HashMap, HashSet, VecDeque},
io,
net::{IpAddr, SocketAddr},
sync::{Arc, RwLock},
};
use thiserror::Error;
use tokio::{process::Command, signal, sync::Notify};
#[derive(Debug, Error)]
pub enum ServiceError {
#[error(transparent)]
Dns(#[from] ResolveError),
#[error(transparent)]
IpcListen(#[from] IpcListenerCreationError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
ListenError(#[from] ListenerCreationError),
#[error("failed to load certificate: `{0}`")]
Certificate(#[from] crypto::Error),
}
pub struct Service {
/// input capture
capture: Capture,
/// input emulation
emulation: Emulation,
/// dns resolver
resolver: DnsResolver,
/// frontend listener
frontend_listener: AsyncFrontendListener,
/// authorized public key sha256 fingerprints
authorized_keys: Arc<RwLock<HashMap<String, String>>>,
/// (outgoing) client information
client_manager: ClientManager,
/// current port
port: u16,
/// the public key fingerprint for (D)TLS
public_key_fingerprint: String,
/// notify for pending frontend events
frontend_event_pending: Notify,
/// frontend events queued for sending
pending_frontend_events: VecDeque<FrontendEvent>,
/// status of input capture (enabled / disabled)
capture_status: Status,
/// status of input emulation (enabled / disabled)
emulation_status: Status,
/// keep track of registered connections to avoid duplicate barriers
incoming_conns: HashSet<SocketAddr>,
/// map from capture handle to connection info
incoming_conn_info: HashMap<ClientHandle, Incoming>,
next_trigger_handle: u64,
}
#[derive(Debug)]
struct Incoming {
fingerprint: String,
addr: SocketAddr,
pos: Position,
}
impl Service {
pub async fn new(config: Config) -> Result<Self, ServiceError> {
let client_manager = ClientManager::default();
for client in config.get_clients() {
let config = ClientConfig {
hostname: client.hostname,
fix_ips: client.ips.into_iter().collect(),
port: client.port,
pos: client.pos,
cmd: client.enter_hook,
};
let state = ClientState {
active: client.active,
ips: HashSet::from_iter(config.fix_ips.iter().cloned()),
..Default::default()
};
let handle = client_manager.add_client();
client_manager.set_config(handle, config);
client_manager.set_state(handle, state);
}
// load certificate
let cert = crypto::load_or_generate_key_and_cert(&config.cert_path)?;
let public_key_fingerprint = crypto::certificate_fingerprint(&cert);
// create frontend communication adapter, exit if already running
let frontend_listener = AsyncFrontendListener::new().await?;
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints.clone()));
// listener + connection
let listener =
LanMouseListener::new(config.port, cert.clone(), authorized_keys.clone()).await?;
let conn = LanMouseConnection::new(cert.clone(), client_manager.clone());
// input capture + emulation
let capture_backend = config.capture_backend.map(|b| b.into());
let capture = Capture::new(capture_backend, conn, config.release_bind.clone());
let emulation_backend = config.emulation_backend.map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener);
// create dns resolver
let resolver = DnsResolver::new()?;
let port = config.port;
let service = Self {
capture,
emulation,
frontend_listener,
resolver,
authorized_keys,
public_key_fingerprint,
client_manager,
frontend_event_pending: Default::default(),
port,
pending_frontend_events: Default::default(),
capture_status: Default::default(),
emulation_status: Default::default(),
incoming_conn_info: Default::default(),
incoming_conns: Default::default(),
next_trigger_handle: 0,
};
Ok(service)
}
pub async fn run(&mut self) -> Result<(), ServiceError> {
for handle in self.client_manager.active_clients() {
// small hack: `activate_client()` checks, if the client
// is already active in client_manager and does not create a
// capture barrier in that case so we have to deactivate it first
self.client_manager.deactivate_client(handle);
self.activate_client(handle);
}
loop {
tokio::select! {
request = self.frontend_listener.next() => self.handle_frontend_request(request),
_ = self.frontend_event_pending.notified() => self.handle_frontend_pending().await,
event = self.emulation.event() => self.handle_emulation_event(event),
event = self.capture.event() => self.handle_capture_event(event),
event = self.resolver.event() => self.handle_resolver_event(event),
r = signal::ctrl_c() => break r.expect("failed to wait for CTRL+C"),
}
}
log::info!("terminating service ...");
log::debug!("terminating capture ...");
self.capture.terminate().await;
log::debug!("terminating emulation ...");
self.emulation.terminate().await;
log::debug!("terminating dns resolver ...");
self.resolver.terminate().await;
Ok(())
}
fn handle_frontend_request(&mut self, request: Option<Result<FrontendRequest, IpcError>>) {
let request = match request.expect("frontend listener closed") {
Ok(r) => r,
Err(e) => return log::error!("error receiving request: {e}"),
};
match request {
FrontendRequest::Activate(handle, active) => self.set_client_active(handle, active),
FrontendRequest::AuthorizeKey(desc, fp) => self.add_authorized_key(desc, fp),
FrontendRequest::ChangePort(port) => self.change_port(port),
FrontendRequest::Create => self.add_client(),
FrontendRequest::Delete(handle) => self.remove_client(handle),
FrontendRequest::EnableCapture => self.capture.reenable(),
FrontendRequest::EnableEmulation => self.emulation.reenable(),
FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::GetState(handle) => self.broadcast_client(handle),
FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
FrontendRequest::UpdatePosition(handle, pos) => self.update_pos(handle, pos),
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
}
}
async fn handle_frontend_pending(&mut self) {
while let Some(event) = self.pending_frontend_events.pop_front() {
self.frontend_listener.broadcast(event).await;
}
}
fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event {
EmulationEvent::Connected {
addr,
pos,
fingerprint,
} => {
// check if already registered
if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
} else {
self.update_incoming(addr, pos, fingerprint);
}
}
EmulationEvent::Disconnected { addr } => {
if let Some(addr) = self.remove_incoming(addr) {
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
}
}
EmulationEvent::PortChanged(port) => match port {
Ok(port) => {
self.port = port;
self.notify_frontend(FrontendEvent::PortChanged(port, None));
}
Err(e) => self
.notify_frontend(FrontendEvent::PortChanged(self.port, Some(format!("{e}")))),
},
EmulationEvent::EmulationDisabled => {
self.emulation_status = Status::Disabled;
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
}
EmulationEvent::EmulationEnabled => {
self.emulation_status = Status::Enabled;
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
}
EmulationEvent::ReleaseNotify => self.capture.release(),
}
}
fn handle_capture_event(&mut self, event: ICaptureEvent) {
match event {
ICaptureEvent::CaptureBegin(handle) => {
// we entered the capture zone for an incoming connection
// => notify it that its capture should be released
if let Some(incoming) = self.incoming_conn_info.get(&handle) {
self.emulation.send_leave_event(incoming.addr);
}
}
ICaptureEvent::CaptureDisabled => {
self.capture_status = Status::Disabled;
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status));
}
ICaptureEvent::CaptureEnabled => {
self.capture_status = Status::Enabled;
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status));
}
ICaptureEvent::ClientEntered(handle) => {
log::info!("entering client {handle} ...");
self.spawn_hook_command(handle);
}
}
}
fn handle_resolver_event(&mut self, event: DnsEvent) {
let handle = match event {
DnsEvent::Resolving(handle) => {
self.client_manager.set_resolving(handle, true);
handle
}
DnsEvent::Resolved(handle, hostname, ips) => {
self.client_manager.set_resolving(handle, false);
if let Err(e) = &ips {
log::warn!("could not resolve {hostname}: {e}");
}
let ips = ips.unwrap_or_default();
self.client_manager.set_dns_ips(handle, ips);
handle
}
};
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn resolve(&self, handle: ClientHandle) {
if let Some(hostname) = self.client_manager.get_hostname(handle) {
self.resolver.resolve(handle, hostname);
}
}
fn sync_frontend(&mut self) {
self.enumerate();
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status));
self.notify_frontend(FrontendEvent::PortChanged(self.port, None));
self.notify_frontend(FrontendEvent::PublicKeyFingerprint(
self.public_key_fingerprint.clone(),
));
let keys = self.authorized_keys.read().expect("lock").clone();
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
}
const ENTER_HANDLE_BEGIN: u64 = u64::MAX / 2 + 1;
fn add_incoming(&mut self, addr: SocketAddr, pos: Position, fingerprint: String) {
let handle = Self::ENTER_HANDLE_BEGIN + self.next_trigger_handle;
self.next_trigger_handle += 1;
self.capture.create(handle, pos, CaptureType::EnterOnly);
self.incoming_conns.insert(addr);
self.incoming_conn_info.insert(
handle,
Incoming {
fingerprint,
addr,
pos,
},
);
}
fn update_incoming(&mut self, addr: SocketAddr, pos: Position, fingerprint: String) {
let incoming = self
.incoming_conn_info
.iter_mut()
.find(|(_, i)| i.addr == addr)
.map(|(_, i)| i)
.expect("no such client");
let mut changed = false;
if incoming.fingerprint != fingerprint {
incoming.fingerprint = fingerprint.clone();
changed = true;
}
if incoming.pos != pos {
incoming.pos = pos;
changed = true;
}
if changed {
self.remove_incoming(addr);
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
}
}
fn remove_incoming(&mut self, addr: SocketAddr) -> Option<SocketAddr> {
let handle = self
.incoming_conn_info
.iter()
.find(|(_, incoming)| incoming.addr == addr)
.map(|(k, _)| *k)?;
self.capture.destroy(handle);
self.incoming_conns.remove(&addr);
self.incoming_conn_info
.remove(&handle)
.map(|incoming| incoming.addr)
}
fn notify_frontend(&mut self, event: FrontendEvent) {
self.pending_frontend_events.push_back(event);
self.frontend_event_pending.notify_one();
}
fn add_authorized_key(&mut self, desc: String, fp: String) {
self.authorized_keys.write().expect("lock").insert(fp, desc);
let keys = self.authorized_keys.read().expect("lock").clone();
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
}
fn remove_authorized_key(&mut self, fp: String) {
self.authorized_keys.write().expect("lock").remove(&fp);
let keys = self.authorized_keys.read().expect("lock").clone();
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
}
fn enumerate(&mut self) {
let clients = self.client_manager.get_client_states();
self.notify_frontend(FrontendEvent::Enumerate(clients));
}
fn add_client(&mut self) {
let handle = self.client_manager.add_client();
log::info!("added client {handle}");
let (c, s) = self.client_manager.get_state(handle).unwrap();
self.notify_frontend(FrontendEvent::Created(handle, c, s));
}
fn set_client_active(&mut self, handle: ClientHandle, active: bool) {
if active {
self.activate_client(handle);
} else {
self.deactivate_client(handle);
}
}
fn deactivate_client(&mut self, handle: ClientHandle) {
log::debug!("deactivating client {handle}");
if self.client_manager.deactivate_client(handle) {
self.capture.destroy(handle);
self.notify_frontend(FrontendEvent::Changed(handle));
log::info!("deactivated client {handle}");
}
}
fn activate_client(&mut self, handle: ClientHandle) {
log::debug!("activating client");
/* resolve dns on activate */
self.resolve(handle);
/* deactivate potential other client at this position */
let Some(pos) = self.client_manager.get_pos(handle) else {
return;
};
if let Some(other) = self.client_manager.client_at(pos) {
if other != handle {
self.deactivate_client(other);
}
}
/* activate the client */
if self.client_manager.activate_client(handle) {
/* notify capture and frontends */
self.capture.create(handle, pos, CaptureType::Default);
self.notify_frontend(FrontendEvent::Changed(handle));
log::info!("activated client {handle} ({pos})");
}
}
fn change_port(&mut self, port: u16) {
if self.port != port {
self.emulation.request_port_change(port);
} else {
self.notify_frontend(FrontendEvent::PortChanged(self.port, None));
}
}
fn remove_client(&mut self, handle: ClientHandle) {
if self
.client_manager
.remove_client(handle)
.map(|(_, s)| s.active)
.unwrap_or(false)
{
self.capture.destroy(handle);
}
self.notify_frontend(FrontendEvent::Deleted(handle));
}
fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
self.client_manager.set_fix_ips(handle, fix_ips);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
if self.client_manager.set_hostname(handle, hostname.clone()) {
self.resolve(handle);
}
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn update_port(&mut self, handle: ClientHandle, port: u16) {
self.client_manager.set_port(handle, port);
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
// update state in event input emulator & input capture
if self.client_manager.set_pos(handle, pos) {
self.deactivate_client(handle);
self.activate_client(handle);
}
self.notify_frontend(FrontendEvent::Changed(handle));
}
fn broadcast_client(&mut self, handle: ClientHandle) {
let event = self
.client_manager
.get_state(handle)
.map(|(c, s)| FrontendEvent::State(handle, c, s))
.unwrap_or(FrontendEvent::NoSuchClient(handle));
self.notify_frontend(event);
}
fn spawn_hook_command(&self, handle: ClientHandle) {
let Some(cmd) = self.client_manager.get_enter_cmd(handle) else {
return;
};
tokio::task::spawn_local(async move {
log::info!("spawning command!");
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
Ok(c) => c,
Err(e) => {
log::warn!("could not execute cmd: {e}");
return;
}
};
match child.wait().await {
Ok(s) => {
if s.success() {
log::info!("{cmd} exited successfully");
} else {
log::warn!("{cmd} exited with {s}");
}
}
Err(e) => log::warn!("{cmd}: {e}"),
}
});
}
}