Compare commits

..

3 Commits

Author SHA1 Message Date
Ferdinand Schober
2594a83e6a formatting 2024-01-02 12:43:33 +01:00
Ferdinand Schober
b81e5806ab basic input event decoding 2024-01-02 12:43:33 +01:00
Ferdinand Schober
a129e27a26 start working on x11 producer 2024-01-02 12:43:33 +01:00
10 changed files with 245 additions and 100 deletions

2
Cargo.lock generated
View File

@@ -1226,7 +1226,7 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]] [[package]]
name = "lan-mouse" name = "lan-mouse"
version = "0.5.1" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ashpd", "ashpd",

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lan-mouse" name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks" description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.5.1" version = "0.5.0"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse" repository = "https://github.com/ferdinandschober/lan-mouse"

View File

@@ -14,7 +14,7 @@ The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg h
</picture> </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/). Goal of this project is to be an open-source replacement for proprietary tools like [Synergy](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, ... . Focus lies on performance and a clean, manageable implementation that can easily be expanded to support additional backends like e.g. Android, iOS, ... .
@@ -22,18 +22,6 @@ Focus lies on performance and a clean, manageable implementation that can easily
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap). For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
> [!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!
## OS Support ## OS Support
The following table shows support for input emulation (to emulate events received from other clients) and The following table shows support for input emulation (to emulate events received from other clients) and
@@ -50,10 +38,6 @@ input capture (to send events *to* other clients) on different operating systems
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now. Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
> [!Important]
> 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.
## Build and Run ## Build and Run
### Install Dependencies ### Install Dependencies
@@ -226,12 +210,11 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
- [x] respect xdg-config-home for config file location. - [x] respect xdg-config-home for config file location.
- [x] IP Address switching - [x] IP Address switching
- [x] Liveness tracking Automatically ungrab mouse when client unreachable - [x] Liveness tracking Automatically ungrab mouse when client unreachable
- [x] Liveness tracking: Automatically release keys, when server offline - [ ] Liveness tracking: Automatically release keys, when server offline
- [ ] Libei Input Capture
- [ ] X11 Input Capture - [ ] X11 Input Capture
- [ ] Windows Input Capture - [ ] Windows Input Capture
- [ ] MacOS Input Capture - [ ] MacOS Input Capture
- [ ] MacOS KeyCode Translation - [ ] MaxOS KeyCode Translation
- [ ] Latency measurement and visualization - [ ] Latency measurement and visualization
- [ ] Bandwidth usage measurement and visualization - [ ] Bandwidth usage measurement and visualization
- [ ] Clipboard support - [ ] Clipboard support

View File

@@ -10,4 +10,4 @@ Name=Lan Mouse
StartupNotify=true StartupNotify=true
Terminal=false Terminal=false
Type=Application Type=Application
Version=0.5.1 Version=0.5.0

View File

@@ -5,8 +5,6 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use std::time::Duration;
use tokio::task::AbortHandle;
use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY}; use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
use winapi::{ use winapi::{
self, self,
@@ -23,16 +21,11 @@ use crate::{
event::Event, event::Event,
}; };
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); pub struct WindowsConsumer {}
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct WindowsConsumer {
repeat_task: Option<AbortHandle>,
}
impl WindowsConsumer { impl WindowsConsumer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self { repeat_task: None }) Ok(Self {})
} }
} }
@@ -65,15 +58,7 @@ impl EventConsumer for WindowsConsumer {
time: _, time: _,
key, key,
state, state,
} => { } => key_event(key, state),
match state {
// pressed
0 => self.kill_repeat_task(),
1 => self.spawn_repeat_task(key).await,
_ => {}
}
key_event(key, state)
}
KeyboardEvent::Modifiers { .. } => {} KeyboardEvent::Modifiers { .. } => {}
}, },
_ => {} _ => {}
@@ -87,27 +72,6 @@ impl EventConsumer for WindowsConsumer {
async fn destroy(&mut self) {} async fn destroy(&mut self) {}
} }
impl WindowsConsumer {
async fn spawn_repeat_task(&mut self, key: u32) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn send_mouse_input(mi: MOUSEINPUT) { fn send_mouse_input(mi: MOUSEINPUT) {
unsafe { unsafe {
let mut input = INPUT { let mut input = INPUT {

View File

@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use std::ptr; use std::ptr;
use x11::{ use x11::{
xlib::{self, XCloseDisplay}, xlib::{self, XCloseDisplay, XOpenDisplay},
xtest, xtest,
}; };
@@ -23,7 +23,7 @@ unsafe impl Send for X11Consumer {}
impl X11Consumer { impl X11Consumer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { match XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => { d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(anyhow!("could not open display")) Err(anyhow!("could not open display"))
} }

View File

@@ -712,13 +712,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
app.pending_events.push_back((*client, Event::Enter())); app.pending_events.push_back((*client, Event::Enter()));
} }
wl_pointer::Event::Leave { .. } => { wl_pointer::Event::Leave { .. } => {
/* There are rare cases, where when a window is opened in
* just the wrong moment, the pointer is released, while
* still grabbed.
* In that case, the pointer must be ungrabbed, otherwise
* it is impossible to grab it again (since the pointer
* lock, relative pointer,... objects are still in place)
*/
app.ungrab(); app.ungrab();
} }
wl_pointer::Event::Button { wl_pointer::Event::Button {

View File

@@ -1,19 +1,207 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::io; use x11::xinput::XGrabDevice;
use std::task::Poll; use x11::xinput2::XIAllDevices;
use std::collections::VecDeque;
use std::os::fd::{AsRawFd, RawFd};
use std::task::{ready, Poll};
use std::{io, ptr};
use futures_core::Stream; use futures_core::Stream;
use crate::event::Event; use crate::event::{Event, PointerEvent};
use crate::producer::EventProducer; use crate::producer::EventProducer;
use crate::client::{ClientEvent, ClientHandle}; use crate::client::{ClientEvent, ClientHandle};
use tokio::io::unix::AsyncFd;
pub struct X11Producer {} use x11::xlib::{
self, ButtonPress, ButtonPressMask, ButtonRelease, ButtonReleaseMask, CWBackPixel, CWEventMask,
CWOverrideRedirect, CopyFromParent, CurrentTime, EnterNotify, EnterWindowMask, ExposureMask,
GrabModeAsync, KeyPress, KeyPressMask, KeyRelease, KeyReleaseMask, LeaveWindowMask,
MotionNotify, PointerMotionMask, VisibilityChangeMask, XClassHint, XCloseDisplay,
XCreateWindow, XDefaultScreen, XFlush, XGetInputFocus, XGrabKeyboard,
XGrabPointer, XMapRaised, XNextEvent, XOpenDisplay, XPending, XRootWindow, XSetClassHint,
XSetWindowAttributes, XWhitePixel, XDefaultRootWindow,
};
pub struct X11Producer(AsyncFd<Inner>);
struct Inner {
connection_fd: RawFd,
display: *mut xlib::Display,
pending_events: VecDeque<(ClientHandle, Event)>,
window: u64,
}
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
self.connection_fd
}
}
impl X11Producer { impl X11Producer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Err(anyhow!("not implemented")) let display = unsafe {
match XOpenDisplay(ptr::null()) {
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(anyhow!("could not open display"))
}
display => Ok(display),
}
}?;
let screen = unsafe { XDefaultScreen(display) };
log::warn!("screen: {screen}");
let root_window = unsafe { XRootWindow(display, screen) };
log::warn!("root: {root_window}");
let mut attr: XSetWindowAttributes = unsafe { std::mem::zeroed() };
attr.override_redirect = true as i32;
attr.background_pixel = unsafe { XWhitePixel(display, screen) };
attr.event_mask = ExposureMask
| VisibilityChangeMask
| KeyPressMask
| KeyReleaseMask
| PointerMotionMask
| ButtonPressMask
| ButtonReleaseMask
| EnterWindowMask
| LeaveWindowMask;
let window = unsafe {
XCreateWindow(
display,
root_window,
0, /* x */
0, /* y */
2560, /* min width */
10, /* min height */
0, /* border width */
CopyFromParent, /* depth */
CopyFromParent as u32, /* class */
ptr::null_mut(), /* Visual *visual */
CWOverrideRedirect | CWBackPixel | CWEventMask,
&mut attr as *mut _,
)
};
let mut name: String = "lan-mouse".into();
let name = name.as_mut_ptr();
let mut class_hint = XClassHint {
res_name: name as *mut i8,
res_class: name as *mut i8,
};
unsafe { XSetClassHint(display, window, &mut class_hint as *mut _) };
log::warn!("window: {window}");
// unsafe { XSelectInput(display, window, event_mask as i64) };
unsafe { XMapRaised(display, window) };
unsafe { XFlush(display) };
/* can not fail */
let connection_fd = unsafe { xlib::XConnectionNumber(display) };
let pending_events = VecDeque::new();
let inner = Inner {
connection_fd,
display,
window,
pending_events,
};
let async_fd = AsyncFd::new(inner)?;
Ok(X11Producer(async_fd))
}
}
impl Inner {
fn decode(&mut self, xevent: xlib::XEvent) -> Option<(u32, Event)> {
log::info!("decoding {xevent:?}");
match xevent.get_type() {
t if t == KeyPress || t == KeyRelease => {
let key_event: xlib::XKeyEvent = unsafe { xevent.key };
let code = key_event.keycode;
let linux_code = code - 8;
let state = (xevent.get_type() == KeyPress) as u8;
return Some((
0,
Event::Keyboard(crate::event::KeyboardEvent::Key {
time: 0,
key: linux_code,
state,
}),
));
}
t if t == EnterNotify => {
let mut prev_win = 0;
unsafe {
XGetInputFocus(
self.display,
&mut self.window as *mut _,
&mut prev_win as *mut _,
);
XGrabKeyboard(
self.display,
XDefaultRootWindow(self.display),
true as i32,
GrabModeAsync,
GrabModeAsync,
CurrentTime,
);
XGrabPointer(
self.display,
self.window, /* window to grab */
true as i32, /* owner_events */
(PointerMotionMask | ButtonPressMask | ButtonReleaseMask) as u32, /* event mask */
GrabModeAsync, /* pointer_mode */
GrabModeAsync, /* keyboard_mode */
self.window, /* confine_to */
0, /* cursor */
CurrentTime,
);
};
Some((0, Event::Enter()))
}
t if t == MotionNotify => {
let pointer_event = unsafe { xevent.motion };
let (abs_x, abs_y) = (pointer_event.x, pointer_event.y);
let event = Event::Pointer(PointerEvent::Motion {
time: 0,
relative_x: abs_x as f64,
relative_y: abs_y as f64,
});
Some((0, event))
}
t if t == ButtonPress || t == ButtonRelease => {
let button_event = unsafe { xevent.button };
log::info!("{:?}", xevent);
Some((0, Event::Pointer(PointerEvent::Button {
time: 0,
button: button_event.button,
state: button_event.state,
})))
}
_ => None,
}
}
fn dispatch(&mut self) -> io::Result<bool> {
unsafe {
if XPending(self.display) > 0 {
let mut xevent: xlib::XEvent = std::mem::zeroed();
XNextEvent(self.display, &mut xevent as *mut _);
if let Some(event) = self.decode(xevent) {
self.pending_events.push_back(event);
}
Ok(true)
} else {
Ok(false)
}
}
}
}
impl Drop for Inner {
fn drop(&mut self) {
unsafe {
XCloseDisplay(self.display);
}
} }
} }
@@ -31,9 +219,37 @@ impl Stream for X11Producer {
type Item = io::Result<(ClientHandle, Event)>; type Item = io::Result<(ClientHandle, Event)>;
fn poll_next( fn poll_next(
self: std::pin::Pin<&mut Self>, mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
Poll::Pending if let Some(event) = self.0.get_mut().pending_events.pop_front() {
return Poll::Ready(Some(Ok(event)));
}
loop {
let mut guard = ready!(self.0.poll_read_ready_mut(cx))?;
{
let inner = guard.get_inner_mut();
loop {
if match inner.dispatch() {
Ok(event) => event,
Err(e) => {
guard.clear_ready();
return Poll::Ready(Some(Err(e)));
}
} == false
{
break;
}
}
}
guard.clear_ready();
match guard.get_inner_mut().pending_events.pop_front() {
Some(event) => {
return Poll::Ready(Some(Ok(event)));
}
None => continue,
}
}
} }
} }

View File

@@ -11,7 +11,7 @@ pub const BTN_MIDDLE: u32 = 0x112;
pub const BTN_BACK: u32 = 0x113; pub const BTN_BACK: u32 = 0x113;
pub const BTN_FORWARD: u32 = 0x114; pub const BTN_FORWARD: u32 = 0x114;
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum PointerEvent { pub enum PointerEvent {
Motion { Motion {
time: u32, time: u32,
@@ -31,7 +31,7 @@ pub enum PointerEvent {
Frame {}, Frame {},
} }
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum KeyboardEvent { pub enum KeyboardEvent {
Key { Key {
time: u32, time: u32,
@@ -46,7 +46,7 @@ pub enum KeyboardEvent {
}, },
} }
#[derive(PartialEq, Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Event { pub enum Event {
/// pointer event (motion / button / axis) /// pointer event (motion / button / axis)
Pointer(PointerEvent), Pointer(PointerEvent),

View File

@@ -131,6 +131,7 @@ impl Server {
tokio::select! { tokio::select! {
event = producer.next() => { event = producer.next() => {
let event = event.ok_or(anyhow!("event producer closed"))??; let event = event.ok_or(anyhow!("event producer closed"))??;
log::debug!("producer event: {event:?}");
server.handle_producer_event(&mut producer, &sender_ch, &timer_ch, event).await?; server.handle_producer_event(&mut producer, &sender_ch, &timer_ch, event).await?;
} }
e = producer_notify_rx.recv() => { e = producer_notify_rx.recv() => {
@@ -720,7 +721,6 @@ impl Server {
} }
} }
State::Receiving => { State::Receiving => {
let mut ignore_event = false;
if let Event::Keyboard(KeyboardEvent::Key { if let Event::Keyboard(KeyboardEvent::Key {
time: _, time: _,
key, key,
@@ -736,21 +736,15 @@ impl Server {
return; return;
}; };
if state == 0 { if state == 0 {
// ignore release event if key not pressed client_state.pressed_keys.remove(&key);
ignore_event = !client_state.pressed_keys.remove(&key);
} else { } else {
// ignore press event if key not released client_state.pressed_keys.insert(key);
ignore_event = !client_state.pressed_keys.insert(key);
let _ = timer_tx.try_send(()); let _ = timer_tx.try_send(());
} }
} }
// ignore double press / release events to // consume event
// workaround buggy rdp backend. consumer.consume(event, handle).await;
if !ignore_event { log::trace!("{event:?} => consumer");
// consume event
consumer.consume(event, handle).await;
log::trace!("{event:?} => consumer");
}
} }
State::AwaitingLeave => { State::AwaitingLeave => {
// we just entered the deadzone of a client, so // we just entered the deadzone of a client, so
@@ -830,11 +824,6 @@ impl Server {
start_timer = true; start_timer = true;
log::trace!("STATE ===> AwaitingLeave"); log::trace!("STATE ===> AwaitingLeave");
enter = true; enter = true;
} else {
// ignore any potential events in receiving mode
if self.state.get() == State::Receiving && e != Event::Disconnect() {
return Ok(());
}
} }
(client_state.active_addr, enter, start_timer) (client_state.active_addr, enter, start_timer)