use crate::{
capture::{Capture, CaptureType, ICaptureEvent},
client::ClientManager,
config::{Config, ConfigClient},
connect::MousehopConnection,
crypto,
discovery::{Discovery, PrimaryCache},
dns::{DnsEvent, DnsResolver},
emulation::{Emulation, EmulationEvent},
listen::{ListenerCreationError, MousehopListener},
};
use futures::StreamExt;
use input_capture::clipboard::{ClipboardMonitor, SuppressionList};
use input_capture::frontmost_app;
use input_event::{ClipboardEvent, Event as InputEvent};
use log;
use mousehop_ipc::{
AppIdent, AsyncFrontendListener, ClientHandle, FrontendEvent, FrontendRequest, HostKind,
IncomingPeerConfig, IpcError, IpcListenerCreationError, Position, Status,
};
use mousehop_proto::ProtoEvent;
use std::{
collections::{HashMap, HashSet, VecDeque},
hash::{DefaultHasher, Hash, Hasher},
io,
net::{IpAddr, SocketAddr},
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use thiserror::Error;
use tokio::{process::Command, signal, sync::Notify};
#[derive(Debug, Error)]
pub enum ServiceError {
#[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 {
config: Config,
capture: Capture,
emulation: Emulation,
resolver: DnsResolver,
frontend_listener: AsyncFrontendListener,
authorized_keys: Arc<RwLock<HashMap<String, IncomingPeerConfig>>>,
primary_cache: PrimaryCache,
client_manager: ClientManager,
port: u16,
public_key_fingerprint: String,
frontend_event_pending: Notify,
pending_frontend_events: VecDeque<FrontendEvent>,
capture_status: Status,
emulation_status: Status,
incoming_conns: HashSet<SocketAddr>,
incoming_conn_info: HashMap<ClientHandle, Incoming>,
next_trigger_handle: u64,
discovery: Discovery,
conn: MousehopConnection,
clipboard_monitor: Option<ClipboardMonitor>,
recent_forwarded: HashMap<(String, u64), Instant>,
clipboard_suppression: SuppressionList,
}
const RECENT_FORWARD_TTL: Duration = Duration::from_secs(1);
fn clipboard_hash(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
#[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.clients() {
client_manager.add_with_config(client);
}
let cert = crypto::load_or_generate_key_and_cert(config.cert_path())?;
let public_key_fingerprint = crypto::certificate_fingerprint(&cert);
let frontend_listener = AsyncFrontendListener::new().await?;
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints()));
let listener =
MousehopListener::new(config.port(), cert.clone(), authorized_keys.clone()).await?;
let primary_cache: PrimaryCache = Default::default();
let conn =
MousehopConnection::new(cert.clone(), client_manager.clone(), primary_cache.clone());
let capture_backend = config.capture_backend().map(|b| b.into());
let conn_for_service = conn.sender_clone();
let capture = Capture::new(
capture_backend,
conn,
config.release_bind(),
config.release_threshold_px(),
);
let emulation_backend = config.emulation_backend().map(|b| b.into());
let emulation = Emulation::new(emulation_backend, listener);
emulation.set_incoming_peers(authorized_keys.read().expect("lock").clone());
let resolver = DnsResolver::new()?;
let port = config.port();
let discovery = Discovery::new(port, config.mdns_discovery(), primary_cache.clone());
let clipboard_suppression: SuppressionList = {
let host = HostKind::current();
let initial: HashSet<AppIdent> = config
.clipboard_suppression()
.host()
.iter()
.cloned()
.map(|s| host.make_ident(s))
.collect();
Arc::new(std::sync::Mutex::new(initial))
};
let clipboard_monitor =
match ClipboardMonitor::with_suppression(clipboard_suppression.clone()) {
Ok(m) => Some(m),
Err(e) => {
log::warn!("clipboard monitor unavailable: {e}; clipboard sync disabled");
None
}
};
let service = Self {
config,
capture,
emulation,
frontend_listener,
resolver,
authorized_keys,
primary_cache,
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,
discovery,
conn: conn_for_service,
clipboard_monitor,
recent_forwarded: HashMap::new(),
clipboard_suppression,
};
Ok(service)
}
pub async fn run(&mut self) -> Result<(), ServiceError> {
let active = self.client_manager.active_clients();
for handle in active.iter() {
self.client_manager.deactivate_client(*handle);
}
for handle in active {
self.activate_client(handle);
}
let mut discovery_refresh_tick = tokio::time::interval(Duration::from_secs(30));
discovery_refresh_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
discovery_refresh_tick.tick().await;
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).await,
event = self.capture.event() => self.handle_capture_event(event),
event = self.resolver.event() => self.handle_resolver_event(event),
_ = self.config.changed() => self.handle_config_change(),
_ = discovery_refresh_tick.tick() => self.discovery.refresh(),
event = recv_clipboard(&mut self.clipboard_monitor) => {
self.handle_local_clipboard_event(event).await;
}
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);
self.save_config();
}
FrontendRequest::AuthorizeKey(desc, fp) => {
self.add_authorized_key(desc, fp);
self.save_config();
}
FrontendRequest::ChangePort(port) => self.change_port(port),
FrontendRequest::Create => {
self.add_client();
self.save_config();
}
FrontendRequest::Delete(handle) => {
self.remove_client(handle);
self.save_config();
}
FrontendRequest::EnableCapture => self.capture.reenable(),
FrontendRequest::EnableEmulation => self.emulation.reenable(),
FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::UpdateFixIps(handle, fix_ips) => {
self.update_fix_ips(handle, fix_ips);
self.save_config();
}
FrontendRequest::UpdateHostname(handle, host) => {
self.update_hostname(handle, host);
self.save_config();
}
FrontendRequest::UpdatePort(handle, port) => {
self.update_port(handle, port);
self.save_config();
}
FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, pos);
self.save_config();
}
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => {
self.remove_authorized_key(key);
self.save_config();
}
FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
self.update_enter_hook(handle, enter_hook)
}
FrontendRequest::SaveConfiguration => self.save_config(),
FrontendRequest::SetReleaseThreshold(threshold) => {
self.config.set_release_threshold_px(threshold);
self.capture.set_release_threshold(threshold);
self.notify_frontend(FrontendEvent::ReleaseThreshold(threshold));
self.save_config();
}
FrontendRequest::SetIncomingPeerNaturalScroll(fp, natural_scroll) => {
self.set_incoming_peer_natural_scroll(fp, natural_scroll);
self.save_config();
}
FrontendRequest::SetIncomingPeerSensitivity(fp, sensitivity) => {
self.set_incoming_peer_sensitivity(fp, sensitivity);
self.save_config();
}
FrontendRequest::SetMdnsDiscovery(enabled) => {
self.config.set_mdns_discovery(enabled);
self.discovery.set_enabled(enabled);
self.notify_frontend(FrontendEvent::MdnsDiscovery(enabled));
self.save_config();
}
FrontendRequest::SetClientClipboardSend(handle, enabled) => {
if self.client_manager.set_clipboard_send(handle, enabled) {
self.broadcast_client(handle);
self.save_config();
}
}
FrontendRequest::SetIncomingPeerClipboardReceive(fp, enabled) => {
self.set_incoming_peer_clipboard_receive(fp, enabled);
self.save_config();
}
FrontendRequest::AddSuppressedApp(value) => {
self.add_suppressed_app(value);
self.save_config();
}
FrontendRequest::RemoveSuppressedApp(value) => {
self.remove_suppressed_app(value);
self.save_config();
}
FrontendRequest::ListRunningApps => {
let apps = frontmost_app::list_running_apps();
self.notify_frontend(FrontendEvent::RunningApps(apps));
}
}
}
fn add_suppressed_app(&mut self, value: String) {
let value = value.trim().to_owned();
if value.is_empty() {
return;
}
let mut suppression = self.config.clipboard_suppression();
let host = suppression.host_mut();
if !host.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
host.push(value);
}
self.commit_suppression(suppression);
}
fn remove_suppressed_app(&mut self, value: String) {
let mut suppression = self.config.clipboard_suppression();
suppression
.host_mut()
.retain(|v| !v.eq_ignore_ascii_case(&value));
self.commit_suppression(suppression);
}
fn commit_suppression(&mut self, suppression: mousehop_ipc::ClipboardSuppression) {
let host = HostKind::current();
let host_list = suppression.host().clone();
{
let mut guard = self.clipboard_suppression.lock().expect("lock");
guard.clear();
for s in &host_list {
guard.insert(host.make_ident(s.clone()));
}
}
self.config.set_clipboard_suppression(suppression);
self.notify_frontend(FrontendEvent::SuppressedAppsUpdated(host_list));
}
fn update_incoming_peer_address(&mut self, addr: SocketAddr, fingerprint: &str) {
let ip = addr.ip().to_string();
let hostname = self.lookup_hostname_for_ip(addr.ip());
let mut keys = self.authorized_keys.write().expect("lock");
let Some(peer) = keys.get_mut(fingerprint) else {
return; };
let mut changed = peer.last_addr.as_deref() != Some(&ip);
if changed {
peer.last_addr = Some(ip);
}
if let Some(h) = hostname {
if peer.last_hostname.as_deref() != Some(h.as_str()) {
peer.last_hostname = Some(h);
changed = true;
}
}
if !changed {
return;
}
let snapshot = keys.clone();
drop(keys);
self.notify_frontend(FrontendEvent::AuthorizedUpdated(snapshot));
self.save_config();
}
fn lookup_hostname_for_ip(&self, target: std::net::IpAddr) -> Option<String> {
self.primary_cache
.borrow()
.iter()
.find_map(|(host, ip)| (*ip == target).then(|| host.clone()))
}
fn set_incoming_peer_natural_scroll(&mut self, fingerprint: String, natural_scroll: bool) {
if let Some(peer) = self
.authorized_keys
.write()
.expect("lock")
.get_mut(&fingerprint)
{
peer.natural_scroll = natural_scroll;
}
let keys = self.authorized_keys.read().expect("lock").clone();
self.emulation.set_incoming_peers(keys.clone());
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
}
fn set_incoming_peer_sensitivity(&mut self, fingerprint: String, sensitivity: f64) {
if let Some(peer) = self
.authorized_keys
.write()
.expect("lock")
.get_mut(&fingerprint)
{
peer.mouse_sensitivity = sensitivity;
}
let keys = self.authorized_keys.read().expect("lock").clone();
self.emulation.set_incoming_peers(keys.clone());
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
}
fn set_incoming_peer_clipboard_receive(
&mut self,
fingerprint: String,
clipboard_receive: bool,
) {
if let Some(peer) = self
.authorized_keys
.write()
.expect("lock")
.get_mut(&fingerprint)
{
peer.clipboard_receive = clipboard_receive;
}
let keys = self.authorized_keys.read().expect("lock").clone();
self.emulation.set_incoming_peers(keys.clone());
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
}
fn save_config(&mut self) {
let clients = self.client_manager.clients();
let clients = clients
.into_iter()
.map(|(c, s)| ConfigClient {
ips: HashSet::from_iter(c.fix_ips),
hostname: c.hostname,
port: c.port,
pos: c.pos,
active: s.active,
enter_hook: c.cmd,
clipboard_send: c.clipboard_send,
})
.collect();
self.config.set_clients(clients);
let authorized_keys = self.authorized_keys.read().expect("lock").clone();
self.config.set_authorized_keys(authorized_keys);
if let Err(e) = self.config.write_back() {
log::warn!("failed to write config: {e}");
}
}
fn handle_config_change(&mut self) {
for h in self.client_manager.registered_clients() {
self.remove_client(h);
}
for c in self.config.clients() {
let handle = self.client_manager.add_with_config(c);
log::info!("added client {handle}");
let (c, s) = self.client_manager.get_state(handle).unwrap();
if s.active {
self.client_manager.deactivate_client(handle);
self.activate_client(handle);
}
self.notify_frontend(FrontendEvent::Created(handle, c, s));
}
let release_bind = self.config.release_bind();
self.capture.set_release_bind(release_bind);
let release_threshold = self.config.release_threshold_px();
self.capture.set_release_threshold(release_threshold);
self.notify_frontend(FrontendEvent::ReleaseThreshold(release_threshold));
let authorized_keys = self.config.authorized_fingerprints();
self.authorized_keys
.write()
.unwrap()
.clone_from(&authorized_keys);
self.emulation.set_incoming_peers(authorized_keys);
self.sync_frontend();
}
async fn handle_frontend_pending(&mut self) {
while let Some(event) = self.pending_frontend_events.pop_front() {
self.frontend_listener.broadcast(event).await;
}
}
async fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event {
EmulationEvent::ConnectionAttempt { fingerprint } => {
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
}
EmulationEvent::Entered {
addr,
pos,
fingerprint,
} => {
if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::DeviceEntered {
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.discovery.set_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_for_handover(),
EmulationEvent::Connected { addr, fingerprint } => {
self.update_incoming_peer_address(addr, &fingerprint);
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
}
EmulationEvent::PeerHello { addr, commit } => {
if let Some(handle) = self.client_manager.get_client(addr) {
self.client_manager.set_peer_commit(handle, Some(commit));
self.broadcast_client(handle);
}
}
EmulationEvent::ClipboardReceived {
addr,
from_fingerprint,
content,
} => {
self.handle_clipboard_received(addr, from_fingerprint, content)
.await;
}
}
}
async fn handle_local_clipboard_event(&mut self, event: Option<input_capture::CaptureEvent>) {
let Some(event) = event else {
return;
};
let input_capture::CaptureEvent::Input(InputEvent::Clipboard(ClipboardEvent::Text(
content,
))) = event
else {
return;
};
let targets = self.client_manager.clipboard_send_targets();
if targets.is_empty() {
log::trace!(
"clipboard captured locally ({} bytes) but no peer has clipboard_send=true; skipping fan-out",
content.len()
);
return;
}
let from_fingerprint = self.public_key_fingerprint.clone();
let hash = clipboard_hash(&content);
self.prune_recent_forwarded();
self.recent_forwarded
.insert((from_fingerprint.clone(), hash), Instant::now());
log::info!(
"broadcasting local clipboard ({} bytes) to {} peer(s)",
content.len(),
targets.len()
);
for handle in targets {
let event = ProtoEvent::Clipboard {
from_fingerprint: from_fingerprint.clone(),
content: content.clone(),
};
if let Err(e) = self.conn.send(event, handle).await {
log::debug!("clipboard send to client {handle} failed: {e}");
}
}
}
async fn handle_clipboard_received(
&mut self,
from_addr: SocketAddr,
from_fingerprint: String,
content: String,
) {
if let Some(monitor) = self.clipboard_monitor.as_ref() {
monitor.update_last_content(content.clone());
}
let hash = clipboard_hash(&content);
self.prune_recent_forwarded();
let key = (from_fingerprint.clone(), hash);
if self.recent_forwarded.contains_key(&key) {
log::debug!(
"skipping clipboard re-fan-out: already forwarded ({}, {} bytes) within {}ms",
&from_fingerprint[..from_fingerprint.len().min(8)],
content.len(),
RECENT_FORWARD_TTL.as_millis()
);
return;
}
let targets = self.client_manager.clipboard_send_targets();
let forward_targets: Vec<ClientHandle> = targets
.into_iter()
.filter(|h| {
self.client_manager
.active_addr(*h)
.map(|a| a.ip() != from_addr.ip())
.unwrap_or(true)
})
.collect();
if forward_targets.is_empty() {
return;
}
self.recent_forwarded.insert(key, Instant::now());
log::info!(
"forwarding clipboard ({} bytes, originator {}) to {} peer(s)",
content.len(),
&from_fingerprint[..from_fingerprint.len().min(8)],
forward_targets.len()
);
for handle in forward_targets {
let event = ProtoEvent::Clipboard {
from_fingerprint: from_fingerprint.clone(),
content: content.clone(),
};
if let Err(e) = self.conn.send(event, handle).await {
log::debug!("clipboard forward to client {handle} failed: {e}");
}
}
}
fn prune_recent_forwarded(&mut self) {
self.recent_forwarded
.retain(|_, ts| ts.elapsed() < RECENT_FORWARD_TTL);
}
fn handle_capture_event(&mut self, event: ICaptureEvent) {
match event {
ICaptureEvent::CaptureBegin(handle) => {
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);
}
ICaptureEvent::PeerCommitUpdated(handle) => {
self.broadcast_client(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.broadcast_client(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(),
));
self.notify_frontend(FrontendEvent::ReleaseThreshold(
self.config.release_threshold_px(),
));
self.notify_frontend(FrontendEvent::MdnsDiscovery(self.config.mdns_discovery()));
let keys = self.authorized_keys.read().expect("lock").clone();
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
let host_list = self.config.clipboard_suppression().host().clone();
self.notify_frontend(FrontendEvent::SuppressedAppsUpdated(host_list));
}
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::DeviceEntered {
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) {
let entry = IncomingPeerConfig {
description: desc,
..IncomingPeerConfig::default()
};
self.authorized_keys
.write()
.expect("lock")
.insert(fp, entry);
let keys = self.authorized_keys.read().expect("lock").clone();
self.emulation.set_incoming_peers(keys.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.emulation.set_incoming_peers(keys.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.broadcast_client(handle);
log::info!("deactivated client {handle}");
}
}
fn activate_client(&mut self, handle: ClientHandle) {
log::debug!("activating client {handle}");
self.resolve(handle);
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);
}
}
if self.client_manager.activate_client(handle) {
self.capture.create(handle, pos, CaptureType::Default);
self.broadcast_client(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.broadcast_client(handle);
}
fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
log::info!("hostname changed: {hostname:?}");
if self.client_manager.set_hostname(handle, hostname.clone()) {
self.resolve(handle);
}
self.broadcast_client(handle);
}
fn update_port(&mut self, handle: ClientHandle, port: u16) {
self.client_manager.set_port(handle, port);
self.broadcast_client(handle);
}
fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
if self.client_manager.set_pos(handle, pos) {
self.deactivate_client(handle);
self.activate_client(handle);
}
self.broadcast_client(handle);
}
fn update_enter_hook(&mut self, handle: ClientHandle, enter_hook: Option<String>) {
self.client_manager.set_enter_hook(handle, enter_hook);
self.broadcast_client(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}"),
}
});
}
}
async fn recv_clipboard(
monitor: &mut Option<ClipboardMonitor>,
) -> Option<input_capture::CaptureEvent> {
match monitor.as_mut() {
Some(m) => m.recv().await,
None => std::future::pending().await,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clipboard_hash_is_deterministic_within_run() {
let h1 = clipboard_hash("hello, world");
let h2 = clipboard_hash("hello, world");
assert_eq!(h1, h2);
}
#[test]
fn clipboard_hash_distinguishes_different_inputs() {
assert_ne!(clipboard_hash("foo"), clipboard_hash("bar"));
assert_ne!(clipboard_hash(""), clipboard_hash("\0"));
}
#[test]
fn recent_forwarded_prune_evicts_expired_entries() {
let mut map: HashMap<(String, u64), Instant> = HashMap::new();
let now = Instant::now();
let stale = now
.checked_sub(Duration::from_secs(2))
.expect("clock far enough from epoch for the test to subtract 2s");
map.insert(("fp_a".into(), 1), stale);
map.insert(("fp_b".into(), 2), now);
map.retain(|_, ts| ts.elapsed() < RECENT_FORWARD_TTL);
assert!(!map.contains_key(&("fp_a".to_string(), 1)));
assert!(map.contains_key(&("fp_b".to_string(), 2)));
}
}