use super::{input_service::*, *}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; #[cfg(target_os = "android")] use crate::keyboard::client::map_key_to_control_key; #[cfg(target_os = "linux")] use crate::platform::linux::is_x11; #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] use crate::platform::linux_desktop_manager; #[cfg(any(target_os = "windows", target_os = "linux"))] use crate::platform::WallPaperRemover; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ client::{ new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, }, common::{get_default_sound_input, set_sound_input}, display_service, ipc, privacy_mode, video_service, VERSION, }; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use cidr_utils::cidr::IpCidr; #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::Config, fs, fs::can_enable_overwrite_detection, futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, password_security::{self as password, ApproveMode}, sleep, timeout, tokio::{ net::TcpStream, sync::mpsc, time::{self, Duration, Instant, Interval}, }, tokio_util::codec::{BytesCodec, Framed}, }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; use serde_derive::Serialize; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; use std::{ num::NonZeroI64, path::PathBuf, sync::{atomic::AtomicI64, mpsc as std_mpsc}, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; #[cfg(all(windows, feature = "virtual_display_driver"))] use crate::virtual_display_manager; #[cfg(not(any(target_os = "ios")))] use std::collections::HashSet; pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); static ref AUTHED_CONNS: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } #[cfg(any(target_os = "windows", target_os = "linux"))] lazy_static::lazy_static! { static ref WALLPAPER_REMOVER: Arc>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); #[cfg(not(any(target_os = "android", target_os = "ios")))] pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref PLUGIN_BLOCK_INPUT_TXS: Arc>>> = Default::default(); static ref PLUGIN_BLOCK_INPUT_TX_RX: (Arc>>, Arc>>) = { let (tx, rx) = std_mpsc::channel(); (Arc::new(Mutex::new(tx)), Arc::new(Mutex::new(rx))) }; } // Block input is required for some special cases, such as privacy mode. #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn plugin_block_input(peer: &str, block: bool) -> bool { if let Some(tx) = PLUGIN_BLOCK_INPUT_TXS.lock().unwrap().get(peer) { let _ = tx.send(if block { MessageInput::BlockOnPlugin(peer.to_string()) } else { MessageInput::BlockOffPlugin(peer.to_string()) }); match PLUGIN_BLOCK_INPUT_TX_RX .1 .lock() .unwrap() .recv_timeout(std::time::Duration::from_millis(3_000)) { Ok(b) => b == block, Err(..) => { log::error!("plugin_block_input timeout"); false } } } else { false } } #[derive(Clone, Default)] pub struct ConnInner { id: i32, tx: Option, tx_video: Option, } enum MessageInput { #[cfg(not(any(target_os = "android", target_os = "ios")))] Mouse((MouseEvent, i32)), #[cfg(not(any(target_os = "android", target_os = "ios")))] Key((KeyEvent, bool)), #[cfg(not(any(target_os = "android", target_os = "ios")))] Pointer((PointerDeviceEvent, i32)), BlockOn, BlockOff, #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] BlockOnPlugin(String), #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] BlockOffPlugin(String), } #[derive(Clone, Debug)] struct Session { name: String, session_id: u64, last_recv_time: Arc>, random_password: String, tfa: bool, } #[cfg(not(any(target_os = "android", target_os = "ios")))] struct StartCmIpcPara { rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, rx_desktop_ready: mpsc::Receiver<()>, tx_cm_stream_ready: mpsc::Sender<()>, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum AuthConnType { Remote, FileTransfer, PortForward, } pub struct Connection { inner: ConnInner, display_idx: usize, stream: super::Stream, server: super::ServerPtrWeak, hash: Hash, read_jobs: Vec, timer: Interval, file_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, tx_to_cm: mpsc::UnboundedSender, authorized: bool, require_2fa: Option, keyboard: bool, clipboard: bool, audio: bool, file: bool, restart: bool, recording: bool, block_input: bool, last_test_delay: Option, network_delay: u32, lock_after_session_end: bool, show_remote_cursor: bool, // by peer ip: String, // by peer disable_keyboard: bool, // by peer disable_clipboard: bool, // by peer disable_audio: bool, // by peer #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] enable_file_transfer: bool, // by peer audio_sender: Option, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, server_audit_conn: String, server_audit_file: String, lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, file_transferred: bool, #[cfg(windows)] portable: PortableState, from_switch: bool, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, options_in_login: Option, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: HashSet, #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] linux_headless_handle: LinuxHeadlessHandle, closed: bool, delay_response_instant: Instant, #[cfg(not(any(target_os = "android", target_os = "ios")))] start_cm_ipc_para: Option, auto_disconnect_timer: Option<(Instant, u64)>, authed_conn_id: Option, file_remove_log_control: FileRemoveLogControl, #[cfg(feature = "gpucodec")] supported_encoding_flag: (bool, Option), } impl ConnInner { pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { Self { id, tx, tx_video } } } impl Subscriber for ConnInner { #[inline] fn id(&self) -> i32 { self.id } #[inline] fn send(&mut self, msg: Arc) { // Send SwitchDisplay on the same channel as VideoFrame to avoid send order problems. let tx_by_video = match &msg.union { Some(message::Union::VideoFrame(_)) => true, Some(message::Union::Misc(misc)) => match &misc.union { Some(misc::Union::SwitchDisplay(_)) => true, _ => false, }, _ => false, }; let tx = if tx_by_video { self.tx_video.as_mut() } else { self.tx.as_mut() }; tx.map(|tx| { allow_err!(tx.send((Instant::now(), msg))); }); } } const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(1); const SEC30: Duration = Duration::from_secs(30); const H1: Duration = Duration::from_secs(3600); const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); impl Connection { pub async fn start( addr: SocketAddr, stream: super::Stream, id: i32, server: super::ServerPtrWeak, ) { let _raii_id = raii::ConnectionID::new(id); let hash = Hash { salt: Config::get_salt(), challenge: Config::get_auto_password(6), ..Default::default() }; let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed let tx_from_cm = tx_from_cm_holder.clone(); let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, _rx_input) = std_mpsc::channel(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); #[cfg(not(any(target_os = "android", target_os = "ios")))] let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); #[cfg(not(any(target_os = "android", target_os = "ios")))] let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1); #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] let linux_headless_handle = LinuxHeadlessHandle::new(_rx_cm_stream_ready, _tx_desktop_ready); #[cfg(not(any(target_os = "android", target_os = "ios")))] let tx_cloned = tx.clone(); let mut conn = Self { inner: ConnInner { id, tx: Some(tx), tx_video: Some(tx_video), }, require_2fa: crate::auth_2fa::get_2fa(None), display_idx: *display_service::PRIMARY_DISPLAY_IDX, stream, server, hash, read_jobs: Vec::new(), timer: time::interval(SEC30), file_timer: time::interval(SEC30), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), tx_to_cm, authorized: false, keyboard: Connection::permission("enable-keyboard"), clipboard: Connection::permission("enable-clipboard"), audio: Connection::permission("enable-audio"), // to-do: make sure is the option correct here file: Connection::permission("enable-file-transfer"), restart: Connection::permission("enable-remote-restart"), recording: Connection::permission("enable-record-session"), block_input: Connection::permission("enable-block-input"), last_test_delay: None, network_delay: 0, lock_after_session_end: false, show_remote_cursor: false, ip: "".to_owned(), disable_audio: false, #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] enable_file_transfer: false, disable_clipboard: false, disable_keyboard: false, tx_input, video_ack_required: false, server_audit_conn: "".to_owned(), server_audit_file: "".to_owned(), lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, file_transferred: false, #[cfg(windows)] portable: Default::default(), from_switch: false, audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, options_in_login: None, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: Default::default(), #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] linux_headless_handle, closed: false, delay_response_instant: Instant::now(), #[cfg(not(any(target_os = "android", target_os = "ios")))] start_cm_ipc_para: Some(StartCmIpcPara { rx_to_cm, tx_from_cm, rx_desktop_ready, tx_cm_stream_ready, }), auto_disconnect_timer: None, authed_conn_id: None, file_remove_log_control: FileRemoveLogControl::new(id), #[cfg(feature = "gpucodec")] supported_encoding_flag: (false, None), }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { conn.closed = true; // sleep to ensure msg got received. sleep(1.).await; return; } #[cfg(target_os = "android")] start_channel(rx_to_cm, tx_from_cm); if !conn.keyboard { conn.send_permission(Permission::Keyboard, false).await; } if !conn.clipboard { conn.send_permission(Permission::Clipboard, false).await; } if !conn.audio { conn.send_permission(Permission::Audio, false).await; } if !conn.file { conn.send_permission(Permission::File, false).await; } if !conn.restart { conn.send_permission(Permission::Restart, false).await; } if !conn.recording { conn.send_permission(Permission::Recording, false).await; } if !conn.block_input { conn.send_permission(Permission::BlockInput, false).await; } let mut test_delay_timer = time::interval(TEST_DELAY_TIMEOUT); let mut last_recv_time = Instant::now(); conn.stream.set_send_timeout( if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() { SEND_TIMEOUT_OTHER } else { SEND_TIMEOUT_VIDEO }, ); #[cfg(not(any(target_os = "android", target_os = "ios")))] std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned)); let mut second_timer = time::interval(Duration::from_secs(1)); loop { tokio::select! { // biased; // video has higher priority // causing test_delay_timer failed while transferring big file Some(data) = rx_from_cm.recv() => { match data { ipc::Data::Authorize => { conn.require_2fa.take(); conn.send_logon_response().await; if conn.port_forward_socket.is_some() { break; } } ipc::Data::Close => { conn.chat_unanswered = false; // seen conn.file_transferred = false; //seen conn.send_close_reason_no_retry("").await; conn.on_close("connection manager", true).await; break; } ipc::Data::CmErr(e) => { if e != "expected" { // cm closed before connection conn.on_close(&format!("connection manager error: {}", e), false).await; break; } } ipc::Data::ChatMessage{text} => { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { text, ..Default::default() }); let mut msg_out = Message::new(); msg_out.set_misc(misc); conn.send(msg_out).await; conn.chat_unanswered = false; } ipc::Data::SwitchPermission{name, enabled} => { log::info!("Change permission {} -> {}", name, enabled); if &name == "keyboard" { conn.keyboard = enabled; conn.send_permission(Permission::Keyboard, enabled).await; if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); } } else if &name == "clipboard" { conn.clipboard = enabled; conn.send_permission(Permission::Clipboard, enabled).await; if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, conn.inner.clone(), conn.clipboard_enabled() && conn.peer_keyboard_enabled()); } } else if &name == "audio" { conn.audio = enabled; conn.send_permission(Permission::Audio, enabled).await; if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( super::audio_service::NAME, conn.inner.clone(), conn.audio_enabled()); } } else if &name == "file" { conn.file = enabled; conn.send_permission(Permission::File, enabled).await; } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; } else if &name == "recording" { conn.recording = enabled; conn.send_permission(Permission::Recording, enabled).await; } else if &name == "block_input" { conn.block_input = enabled; conn.send_permission(Permission::BlockInput, enabled).await; } } ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] ipc::Data::ClipboardFile(clip) => { allow_err!(conn.stream.send(&clip_2_msg(clip)).await); } ipc::Data::PrivacyModeState((_, state, impl_key)) => { let msg_out = match state { privacy_mode::PrivacyModeState::OffSucceeded => { crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, impl_key, ) } privacy_mode::PrivacyModeState::OffByPeer => { crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffByPeer, impl_key, ) } privacy_mode::PrivacyModeState::OffUnknown => { crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffUnknown, impl_key, ) } }; conn.send(msg_out).await; } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { if let Err(e) = portable_client::start_portable_service(portable_client::StartPara::Direct) { log::error!("Failed to start portable service from cm: {:?}", e); } } ipc::Data::SwitchSidesBack => { let mut misc = Misc::new(); misc.set_switch_back(SwitchBack::default()); let mut msg = Message::new(); msg.set_misc(misc); conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { log::debug!("Close the voice call from the ipc."); conn.close_voice_call().await; // Notify the peer that we closed the voice call. let msg = new_voice_call_request(false); conn.send(msg).await; } _ => {} } }, res = conn.stream.next() => { if let Some(res) = res { match res { Err(err) => { conn.on_close(&err.to_string(), true).await; break; }, Ok(bytes) => { last_recv_time = Instant::now(); *conn.last_recv_time.lock().unwrap() = Instant::now(); if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { if !conn.on_message(msg_in).await { break; } } } } } else { conn.on_close("Reset by the peer", true).await; break; } }, _ = conn.file_timer.tick() => { if !conn.read_jobs.is_empty() { conn.send_to_cm(ipc::Data::FileTransferLog(("transfer".to_string(), fs::serialize_transfer_jobs(&conn.read_jobs)))); match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { Ok(log) => { if !log.is_empty() { conn.send_to_cm(ipc::Data::FileTransferLog(("transfer".to_string(), log))); } } Err(err) => { conn.on_close(&err.to_string(), false).await; break; } } } else { conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } } Ok(conns) = hbbs_rx.recv() => { if conns.contains(&id) { conn.send_close_reason_no_retry("Closed manually by web console").await; conn.on_close("web console", true).await; break; } } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { video_service::notify_video_frame_fetched(id, Some(instant.into())); } if let Err(err) = conn.stream.send(&value as &Message).await { conn.on_close(&err.to_string(), false).await; break; } }, Some((instant, value)) = rx.recv() => { let latency = instant.elapsed().as_millis() as i64; let msg: &Message = &value; if latency > 1000 { match &msg.union { Some(message::Union::AudioFrame(_)) => { // log::info!("audio frame latency {}", instant.elapsed().as_secs_f32()); continue; } _ => {} } } match &msg.union { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { conn.send_close_reason_no_retry("").await; conn.on_close("stop service", false).await; break; } _ => {}, } } Some(message::Union::PeerInfo(..)) => { conn.refresh_video_display(None); } _ => {} } if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; break; } }, _ = second_timer.tick() => { #[cfg(windows)] conn.portable_check(); if let Some((instant, minute)) = conn.auto_disconnect_timer.as_ref() { if instant.elapsed().as_secs() > minute * 60 { conn.send_close_reason_no_retry("Connection failed due to inactivity").await; conn.on_close("auto disconnect", true).await; break; } } conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count(); #[cfg(feature = "gpucodec")] conn.update_supported_encoding(); } _ = test_delay_timer.tick() => { if last_recv_time.elapsed() >= SEC30 { conn.on_close("Timeout", true).await; break; } let mut qos = video_service::VIDEO_QOS.lock().unwrap(); if conn.last_test_delay.is_none() { conn.last_test_delay = Some(Instant::now()); let mut msg_out = Message::new(); msg_out.set_test_delay(TestDelay{ last_delay: conn.network_delay, target_bitrate: qos.bitrate(), ..Default::default() }); conn.inner.send(msg_out.into()); } qos.user_delay_response_elapsed(conn.inner.id(), conn.delay_response_instant.elapsed().as_millis()); } } } if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { let _ = Self::turn_off_privacy_to_msg(id); } } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::plugin::handle_listen_event( crate::plugin::EVENT_ON_CONN_CLOSE_SERVER.to_owned(), conn.lr.my_id.clone(), ); video_service::notify_video_frame_fetched(id, None); if conn.authorized { password::update_temporary_password(); } if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { conn.on_close(&err.to_string(), false).await; } conn.post_conn_audit(json!({ "action": "close", })); if let Some(s) = conn.server.upgrade() { let mut s = s.write().unwrap(); s.remove_connection(&conn.inner); #[cfg(not(any(target_os = "android", target_os = "ios")))] try_stop_record_cursor_pos(); } conn.on_close("End", true).await; log::info!("#{} connection loop exited", id); } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { let mut block_input_mode = false; #[cfg(any(target_os = "windows", target_os = "macos"))] { rdev::set_mouse_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); rdev::set_keyboard_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); } #[cfg(target_os = "macos")] reset_input_ondisconn(); loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { MessageInput::Mouse((msg, id)) => { handle_mouse(&msg, id); } MessageInput::Key((mut msg, press)) => { // todo: press and down have similar meanings. if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) { msg.down = true; } handle_key(&msg); if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) { msg.down = false; handle_key(&msg); } } MessageInput::Pointer((msg, id)) => { handle_pointer(&msg, id); } MessageInput::BlockOn => { let (ok, msg) = crate::platform::block_input(true); if ok { block_input_mode = true; } else { Self::send_block_input_error( &tx, back_notification::BlockInputState::BlkOnFailed, msg, ); } } MessageInput::BlockOff => { let (ok, msg) = crate::platform::block_input(false); if ok { block_input_mode = false; } else { Self::send_block_input_error( &tx, back_notification::BlockInputState::BlkOffFailed, msg, ); } } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] MessageInput::BlockOnPlugin(_peer) => { let (ok, _msg) = crate::platform::block_input(true); if ok { block_input_mode = true; } let _r = PLUGIN_BLOCK_INPUT_TX_RX .0 .lock() .unwrap() .send(block_input_mode); } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] MessageInput::BlockOffPlugin(_peer) => { let (ok, _msg) = crate::platform::block_input(false); if ok { block_input_mode = false; } let _r = PLUGIN_BLOCK_INPUT_TX_RX .0 .lock() .unwrap() .send(block_input_mode); } }, Err(err) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if block_input_mode { let _ = crate::platform::block_input(true); } if std_mpsc::RecvTimeoutError::Disconnected == err { break; } } } } #[cfg(target_os = "linux")] clear_remapped_keycode(); log::info!("Input thread exited"); } async fn try_port_forward_loop( &mut self, rx_from_cm: &mut mpsc::UnboundedReceiver, ) -> ResultType<()> { let mut last_recv_time = Instant::now(); if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); loop { tokio::select! { Some(data) = rx_from_cm.recv() => { match data { ipc::Data::Close => { bail!("Close requested from connection manager"); } ipc::Data::CmErr(e) => { log::error!("Connection manager error: {e}"); bail!("{e}"); } _ => {} } } res = forward.next() => { if let Some(res) = res { last_recv_time = Instant::now(); self.stream.send_bytes(res?.into()).await?; } else { bail!("Forward reset by the peer"); } }, res = self.stream.next() => { if let Some(res) = res { last_recv_time = Instant::now(); timeout(SEND_TIMEOUT_OTHER, forward.send(res?)).await??; } else { bail!("Stream reset by the peer"); } }, _ = self.timer.tick() => { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } } Ok(conns) = hbbs_rx.recv() => { if conns.contains(&self.inner.id) { // todo: check reconnect bail!("Closed manually by the web console"); } } } } } Ok(()) } async fn send_permission(&mut self, permission: Permission, enabled: bool) { let mut misc = Misc::new(); misc.set_permission_info(PermissionInfo { permission: permission.into(), enabled, ..Default::default() }); let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; } async fn check_privacy_mode_on(&mut self) -> bool { if privacy_mode::is_in_privacy_mode() { self.send_login_error("Someone turns on privacy mode, exit") .await; false } else { true } } async fn check_whitelist(&mut self, addr: &SocketAddr) -> bool { let whitelist: Vec = Config::get_option("whitelist") .split(",") .filter(|x| !x.is_empty()) .map(|x| x.to_owned()) .collect(); if !whitelist.is_empty() && whitelist .iter() .filter(|x| x == &"0.0.0.0") .next() .is_none() && whitelist .iter() .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) .next() .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; Self::post_alarm_audit( AlarmAuditType::IpWhitelist, //"ip whitelist", json!({ "ip":addr.ip() }), ); return false; } true } async fn on_open(&mut self, addr: SocketAddr) -> bool { log::debug!("#{} Connection opened from {}.", self.inner.id, addr); if !self.check_whitelist(&addr).await { return false; } self.ip = addr.ip().to_string(); let mut msg_out = Message::new(); msg_out.set_hash(self.hash.clone()); self.send(msg_out).await; self.get_api_server(); self.post_conn_audit(json!({ "ip": addr.ip(), "action": "new", })); true } fn get_api_server(&mut self) { self.server_audit_conn = crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), "conn".to_owned(), ); self.server_audit_file = crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), "file".to_owned(), ); } fn post_conn_audit(&self, v: Value) { if self.server_audit_conn.is_empty() { return; } let url = self.server_audit_conn.clone(); let mut v = v; v["id"] = json!(Config::get_id()); v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); v["conn_id"] = json!(self.inner.id); v["session_id"] = json!(self.lr.session_id); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); } fn post_file_audit( &self, r#type: FileAuditType, path: &str, files: Vec<(String, i64)>, info: Value, ) { if self.server_audit_file.is_empty() { return; } let url = self.server_audit_file.clone(); let file_num = files.len(); let mut files = files; files.sort_by(|a, b| b.1.cmp(&a.1)); files.truncate(10); let is_file = files.len() == 1 && files[0].0.is_empty(); let mut info = info; info["ip"] = json!(self.ip.clone()); info["name"] = json!(self.lr.my_name.clone()); info["num"] = json!(file_num); info["files"] = json!(files); let v = json!({ "id":json!(Config::get_id()), "uuid":json!(crate::encode64(hbb_common::get_uuid())), "peer_id":json!(self.lr.my_id), "type": r#type as i8, "path":path, "is_file":is_file, "info":json!(info).to_string(), }); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); } pub fn post_alarm_audit(typ: AlarmAuditType, info: Value) { let url = crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), "alarm".to_owned(), ); if url.is_empty() { return; } let mut v = Value::default(); v["id"] = json!(Config::get_id()); v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); v["typ"] = json!(typ as i8); v["info"] = serde_json::Value::String(info.to_string()); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); } #[inline] async fn post_audit_async(url: String, v: Value) -> ResultType { crate::post_request(url, v.to_string(), "").await } async fn send_logon_response(&mut self) { if self.authorized { return; } if self.require_2fa.is_some() && !self.is_recent_session(true) { self.send_login_error(crate::client::REQUIRE_2FA).await; return; } let (conn_type, auth_conn_type) = if self.file_transfer.is_some() { (1, AuthConnType::FileTransfer) } else if self.port_forward_socket.is_some() { (2, AuthConnType::PortForward) } else { (0, AuthConnType::Remote) }; self.authed_conn_id = Some(self::raii::AuthedConnID::new( self.inner.id(), auth_conn_type, )); self.post_conn_audit( json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), ); #[allow(unused_mut)] let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), version: VERSION.to_owned(), ..Default::default() }; #[cfg(not(target_os = "android"))] { pi.hostname = whoami::hostname(); pi.platform = whoami::platform().to_string(); } #[cfg(target_os = "android")] { pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.platform = "Android".into(); } #[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))] let platform_additions = serde_json::Map::new(); #[cfg(any( target_os = "windows", target_os = "linux", all(target_os = "macos", feature = "unix-file-copy-paste") ))] let mut platform_additions = serde_json::Map::new(); #[cfg(target_os = "linux")] { if crate::platform::current_is_wayland() { platform_additions.insert("is_wayland".into(), json!(true)); } #[cfg(feature = "linux_headless")] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] if crate::platform::is_headless_allowed() { if linux_desktop_manager::is_headless() { platform_additions.insert("headless".into(), json!(true)); } } } #[cfg(target_os = "windows")] { platform_additions.insert( "is_installed".into(), json!(crate::platform::is_installed()), ); #[cfg(feature = "virtual_display_driver")] if crate::platform::is_installed() { let virtual_displays = virtual_display_manager::get_virtual_displays(); if !virtual_displays.is_empty() { platform_additions.insert("virtual_displays".into(), json!(&virtual_displays)); } } platform_additions.insert( "supported_privacy_mode_impl".into(), json!(privacy_mode::get_supported_privacy_mode_impl()), ); } #[cfg(any( target_os = "windows", all( any(target_os = "linux", target_os = "macos"), feature = "unix-file-copy-paste" ) ))] { platform_additions.insert("has_file_clipboard".into(), json!(true)); } #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] if !platform_additions.is_empty() { pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); } let supported_encoding = scrap::codec::Encoder::supported_encoding(); log::info!("peer info supported_encoding: {:?}", supported_encoding); pi.encoding = Some(supported_encoding).into(); if self.port_forward_socket.is_some() { let mut msg_out = Message::new(); res.set_peer_info(pi); msg_out.set_login_response(res); self.send(msg_out).await; return; } #[cfg(target_os = "linux")] if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { let dtype = crate::platform::linux::get_display_server(); if dtype != crate::platform::linux::DISPLAY_SERVER_X11 && dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND { let msg = if crate::platform::linux::is_login_screen_wayland() { crate::client::LOGIN_SCREEN_WAYLAND.to_owned() } else { format!( "Unsupported display server type \"{}\", x11 or wayland expected", dtype ) }; res.set_error(msg); let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; return; } } #[allow(unused_mut)] let mut sas_enabled = false; #[cfg(windows)] if crate::platform::is_root() { sas_enabled = true; } #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.file_transfer.is_some() { if crate::platform::is_prelogin() || self.tx_to_cm.send(ipc::Data::Test).is_err() { username = "".to_owned(); } } self.authorized = true; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] PLUGIN_BLOCK_INPUT_TXS .lock() .unwrap() .insert(self.lr.my_id.clone(), self.tx_input.clone()); pi.username = username; pi.sas_enabled = sas_enabled; pi.features = Some(Features { privacy_mode: privacy_mode::is_privacy_mode_supported(), ..Default::default() }) .into(); let mut sub_service = false; if self.file_transfer.is_some() { res.set_peer_info(pi); } else { #[cfg(not(any(target_os = "android", target_os = "ios")))] { pi.resolutions = Some(SupportedResolutions { resolutions: display_service::try_get_displays() .map(|displays| { displays .get(self.display_idx) .map(|d| crate::platform::resolutions(&d.name())) .unwrap_or(vec![]) }) .unwrap_or(vec![]), ..Default::default() }) .into(); } try_activate_screen(); if let Some(msg_out) = super::video_service::is_inited_msg() { self.send(msg_out).await; } match super::display_service::update_get_sync_displays().await { Err(err) => { res.set_error(format!("{}", err)); } Ok(displays) => { // For compatibility with old versions, we need to send the displays to the peer. // But the displays may be updated later, before creating the video capturer. pi.displays = displays.clone(); pi.current_display = self.display_idx as _; res.set_peer_info(pi); sub_service = true; #[cfg(target_os = "linux")] { // use rdp_input when uinput is not available in wayland. Ex: flatpak if !is_x11() && !crate::is_server() { let _ = setup_rdp_input().await; } } } } self.on_remote_authorized(); } let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; if let Some(o) = self.options_in_login.take() { self.update_options(&o).await; } if let Some((dir, show_hidden)) = self.file_transfer.clone() { let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { &dir } else { "" }; self.read_dir(dir, show_hidden); } else if sub_service { if let Some(s) = self.server.upgrade() { let mut noperms = Vec::new(); if !self.peer_keyboard_enabled() && !self.show_remote_cursor { noperms.push(NAME_CURSOR); } if !self.show_remote_cursor { noperms.push(NAME_POS); } if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { noperms.push(super::audio_service::NAME); } let mut s = s.write().unwrap(); #[cfg(not(any(target_os = "android", target_os = "ios")))] let _h = try_start_record_cursor_pos(); self.auto_disconnect_timer = Self::get_auto_disconenct_timer(); s.try_add_primay_video_service(); s.add_connection(self.inner.clone(), &noperms); } } } fn on_remote_authorized(&self) { self.update_codec_on_login(); #[cfg(any(target_os = "windows", target_os = "linux"))] if !Config::get_option("allow-remove-wallpaper").is_empty() { // multi connections set once let mut wallpaper = WALLPAPER_REMOVER.lock().unwrap(); if wallpaper.is_none() { match crate::platform::WallPaperRemover::new() { Ok(remover) => { *wallpaper = Some(remover); } Err(e) => { log::info!("create wallpaper remover failed: {:?}", e); } } } } } fn peer_keyboard_enabled(&self) -> bool { self.keyboard && !self.disable_keyboard } fn clipboard_enabled(&self) -> bool { self.clipboard && !self.disable_clipboard } fn audio_enabled(&self) -> bool { self.audio && !self.disable_audio } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] fn file_transfer_enabled(&self) -> bool { self.file && self.enable_file_transfer } fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { self.send_to_cm(ipc::Data::Login { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), port_forward: self.port_forward_address.clone(), peer_id, name, authorized, keyboard: self.keyboard, clipboard: self.clipboard, audio: self.audio, file: self.file, file_transfer_enabled: self.file, restart: self.restart, recording: self.recording, block_input: self.block_input, from_switch: self.from_switch, }); } #[inline] fn send_to_cm(&mut self, data: ipc::Data) { self.tx_to_cm.send(data).ok(); } #[inline] fn send_fs(&mut self, data: ipc::FS) { self.send_to_cm(ipc::Data::FS(data)); } async fn send_login_error(&mut self, err: T) { let mut msg_out = Message::new(); let mut res = LoginResponse::new(); res.set_error(err.to_string()); msg_out.set_login_response(res); self.send(msg_out).await; } #[inline] pub fn send_block_input_error( s: &Sender, state: back_notification::BlockInputState, details: String, ) { let mut misc = Misc::new(); let mut back_notification = BackNotification { details, ..Default::default() }; back_notification.set_block_input_state(state); misc.set_back_notification(back_notification); let mut msg_out = Message::new(); msg_out.set_misc(misc); s.send((Instant::now(), Arc::new(msg_out))).ok(); } #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] fn input_mouse(&self, msg: MouseEvent, conn_id: i32) { self.tx_input.send(MessageInput::Mouse((msg, conn_id))).ok(); } #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] fn input_pointer(&self, msg: PointerDeviceEvent, conn_id: i32) { self.tx_input .send(MessageInput::Pointer((msg, conn_id))) .ok(); } #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] fn input_key(&self, msg: KeyEvent, press: bool) { // to-do: if is the legacy mode, and the key is function key "LockScreen". // Switch to the primary display. self.tx_input.send(MessageInput::Key((msg, press))).ok(); } fn validate_one_password(&self, password: String) -> bool { if password.len() == 0 { return false; } let mut hasher = Sha256::new(); hasher.update(password); hasher.update(&self.hash.salt); let mut hasher2 = Sha256::new(); hasher2.update(&hasher.finalize()[..]); hasher2.update(&self.hash.challenge); hasher2.finalize()[..] == self.lr.password[..] } fn validate_password(&mut self) -> bool { if password::temporary_enabled() { let password = password::temporary_password(); if self.validate_one_password(password.clone()) { SESSIONS.lock().unwrap().insert( self.lr.my_id.clone(), Session { name: self.lr.my_name.clone(), session_id: self.lr.session_id, last_recv_time: self.last_recv_time.clone(), random_password: password, tfa: false, }, ); return true; } } if password::permanent_enabled() { if self.validate_one_password(Config::get_permanent_password()) { return true; } } false } fn is_recent_session(&mut self, tfa: bool) -> bool { SESSIONS .lock() .unwrap() .retain(|_, s| s.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT); let session = SESSIONS .lock() .unwrap() .get(&self.lr.my_id) .map(|s| s.to_owned()); // last_recv_time is a mutex variable shared with connection, can be updated lively. if let Some(mut session) = session { if session.name == self.lr.my_name && session.session_id == self.lr.session_id && !self.lr.password.is_empty() && (tfa && session.tfa || !tfa && self.validate_one_password(session.random_password.clone())) { session.last_recv_time = self.last_recv_time.clone(); SESSIONS .lock() .unwrap() .insert(self.lr.my_id.clone(), session); return true; } } false } pub fn permission(enable_prefix_option: &str) -> bool { #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { let access_mode = Config::get_option("access-mode"); if access_mode == "full" { return true; } else if access_mode == "view" { return false; } } return Config::get_option(enable_prefix_option).is_empty(); } fn update_codec_on_login(&self) { use scrap::codec::{Encoder, EncodingUpdate::*}; if let Some(o) = self.lr.clone().option.as_ref() { if let Some(q) = o.supported_decoding.clone().take() { Encoder::update(Update(self.inner.id(), q)); } else { Encoder::update(NewOnlyVP9(self.inner.id())); } } else { Encoder::update(NewOnlyVP9(self.inner.id())); } } async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { self.options_in_login = Some(o.clone()); } self.video_ack_required = lr.video_ack_required; } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_start_cm_ipc(&mut self) { if let Some(p) = self.start_cm_ipc_para.take() { tokio::spawn(async move { #[cfg(windows)] let tx_from_cm_clone = p.tx_from_cm.clone(); if let Err(err) = start_ipc( p.rx_to_cm, p.tx_from_cm, p.rx_desktop_ready, p.tx_cm_stream_ready, ) .await { log::error!("ipc to connection manager exit: {}", err); #[cfg(windows)] if !crate::platform::is_prelogin() { allow_err!(tx_from_cm_clone.send(Data::CmErr(err.to_string()))); } } }); #[cfg(all(windows, feature = "flutter"))] std::thread::spawn(|| { if crate::is_server() && !crate::check_process("--tray", false) { crate::platform::run_as_user(vec!["--tray"]).ok(); } }); } } async fn on_message(&mut self, msg: Message) -> bool { if let Some(message::Union::LoginRequest(lr)) = msg.union { self.handle_login_request_without_validation(&lr).await; if self.authorized { return true; } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { if !Connection::permission("enable-file-transfer") { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; return false; } self.file_transfer = Some((ft.dir, ft.show_hidden)); } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { self.send_login_error("No permission of IP tunneling").await; sleep(1.).await; return false; } let mut is_rdp = false; if pf.host == "RDP" && pf.port == 0 { pf.host = "localhost".to_owned(); pf.port = 3389; is_rdp = true; } if pf.host.is_empty() { pf.host = "localhost".to_owned(); } let mut addr = format!("{}:{}", pf.host, pf.port); self.port_forward_address = addr.clone(); match timeout(3000, TcpStream::connect(&addr)).await { Ok(Ok(sock)) => { self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new())); } _ => { if is_rdp { addr = "RDP".to_owned(); } self.send_login_error(format!( "Failed to access remote {}, please make sure if it is open", addr )) .await; return false; } } } _ => { if !self.check_privacy_mode_on().await { return false; } } } #[cfg(not(any(target_os = "android", target_os = "ios")))] self.try_start_cm_ipc(); #[cfg(any( feature = "flatpak", feature = "appimage", not(all(target_os = "linux", feature = "linux_headless")) ))] let err_msg = "".to_owned(); #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] let err_msg = self .linux_headless_handle .try_start_desktop(lr.os_login.as_ref()); // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY { self.send_login_error(err_msg).await; return true; } if !hbb_common::is_ip_str(&lr.username) && !hbb_common::is_domain_port_str(&lr.username) && lr.username != Config::get_id() { self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) .await; return false; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") { self.send_login_error(crate::client::LOGIN_MSG_NO_PASSWORD_ACCESS) .await; } return true; } else if password::approve_mode() == ApproveMode::Password && !password::has_valid_password() { self.send_login_error("Connection not allowed").await; return false; } else if self.is_recent_session(false) { if err_msg.is_empty() { #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] self.linux_headless_handle.wait_desktop_cm_ready().await; self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); self.send_logon_response().await; if self.port_forward_socket.is_some() { return false; } } else { self.send_login_error(err_msg).await; } } else if lr.password.is_empty() { if err_msg.is_empty() { self.try_start_cm(lr.my_id, lr.my_name, false); } else { self.send_login_error( crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, ) .await; } } else { let (failure, res) = self.check_failure(0).await; if !res { return true; } if !self.validate_password() { self.update_failure(failure, false, 0); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) .await; self.try_start_cm(lr.my_id, lr.my_name, false); } else { self.send_login_error( crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, ) .await; } } else { self.update_failure(failure, true, 0); if err_msg.is_empty() { #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] self.linux_headless_handle.wait_desktop_cm_ready().await; self.send_logon_response().await; self.try_start_cm(lr.my_id, lr.my_name, true); if self.port_forward_socket.is_some() { return false; } } else { self.send_login_error(err_msg).await; } } } } else if let Some(message::Union::Auth2fa(tfa)) = msg.union { let (failure, res) = self.check_failure(1).await; if !res { return true; } if let Some(totp) = self.require_2fa.as_ref() { if let Ok(code) = totp.generate_current() { if tfa.code == code { self.update_failure(failure, true, 1); self.require_2fa.take(); self.send_logon_response().await; let session = SESSIONS .lock() .unwrap() .get(&self.lr.my_id) .map(|s| s.to_owned()); if let Some(mut session) = session { session.tfa = true; SESSIONS .lock() .unwrap() .insert(self.lr.my_id.clone(), session); } else { SESSIONS.lock().unwrap().insert( self.lr.my_id.clone(), Session { name: self.lr.my_name.clone(), session_id: self.lr.session_id, last_recv_time: self.last_recv_time.clone(), random_password: "".to_owned(), tfa: true, }, ); } } else { self.update_failure(failure, false, 1); self.send_login_error(crate::client::LOGIN_MSG_2FA_WRONG) .await; } } } } else if let Some(message::Union::TestDelay(t)) = msg.union { if t.from_client { let mut msg_out = Message::new(); msg_out.set_test_delay(t); self.inner.send(msg_out.into()); } else { if let Some(tm) = self.last_test_delay { self.last_test_delay = None; let new_delay = tm.elapsed().as_millis() as u32; video_service::VIDEO_QOS .lock() .unwrap() .user_network_delay(self.inner.id(), new_delay); self.network_delay = new_delay; } self.delay_response_instant = Instant::now(); } } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { #[cfg(feature = "flutter")] if let Some(lr) = _s.lr.clone().take() { self.handle_login_request_without_validation(&lr).await; SWITCH_SIDES_UUID .lock() .unwrap() .retain(|_, v| v.0.elapsed() < Duration::from_secs(10)); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { if let Some((_instant, uuid_old)) = uuid_old { if uuid == uuid_old { self.from_switch = true; self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); self.send_logon_response().await; #[cfg(not(any(target_os = "android", target_os = "ios")))] self.try_start_cm_ipc(); } } } } } else if self.authorized { match msg.union { Some(message::Union::MouseEvent(me)) => { #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) { log::debug!("call_main_service_pointer_input fail:{}", e); } #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.peer_keyboard_enabled() { if is_left_up(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); } else { MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); } self.input_mouse(me, self.inner.id()); } self.update_auto_disconnect_timer(); } Some(message::Union::PointerDeviceEvent(pde)) => { #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = match pde.union { Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union { Some(touch_event::Union::PanStart(pan_start)) => { call_main_service_pointer_input( "touch", 4, pan_start.x, pan_start.y, ) } Some(touch_event::Union::PanUpdate(pan_update)) => { call_main_service_pointer_input( "touch", 5, pan_update.x, pan_update.y, ) } Some(touch_event::Union::PanEnd(pan_end)) => { call_main_service_pointer_input("touch", 6, pan_end.x, pan_end.y) } _ => Ok(()), }, _ => Ok(()), } { log::debug!("call_main_service_pointer_input fail:{}", e); } #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.peer_keyboard_enabled() { MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); self.input_pointer(pde, self.inner.id()); } self.update_auto_disconnect_timer(); } #[cfg(any(target_os = "ios"))] Some(message::Union::KeyEvent(..)) => {} #[cfg(any(target_os = "android"))] Some(message::Union::KeyEvent(mut me)) => { let is_press = (me.press || me.down) && !crate::is_modifier(&me); let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) } Ok(KeyboardMode::Translate) => { if let Some(key_event::Union::Chr(code)) = me.union { Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF)) } else { None } } _ => None, } .filter(crate::keyboard::is_modifier); if let Some(key) = key { if is_press { self.pressed_modifiers.insert(key); } else { self.pressed_modifiers.remove(&key); } } let mut modifiers = vec![]; for key in self.pressed_modifiers.iter() { if let Some(control_key) = map_key_to_control_key(key) { modifiers.push(EnumOrUnknown::new(control_key)); } } me.modifiers = modifiers; let encode_result = me.write_to_bytes(); match encode_result { Ok(data) => { let result = call_main_service_key_event(&data); if let Err(e) = result { log::debug!("call_main_service_key_event fail: {}", e); } } Err(e) => { log::debug!("encode key event fail: {}", e); } } } #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(message::Union::KeyEvent(me)) => { if self.peer_keyboard_enabled() { if is_enter(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); } // handle all down as press // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which // make sure all key are released let is_press = if cfg!(target_os = "linux") { (me.press || me.down) && !crate::is_modifier(&me) } else { me.press }; let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) } Ok(KeyboardMode::Translate) => { if let Some(key_event::Union::Chr(code)) = me.union { Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF)) } else { None } } _ => None, } .filter(crate::keyboard::is_modifier); if let Some(key) = key { if is_press { self.pressed_modifiers.insert(key); } else { self.pressed_modifiers.remove(&key); } } if is_press { match me.union { Some(key_event::Union::Unicode(_)) | Some(key_event::Union::Seq(_)) => { self.input_key(me, false); } _ => { self.input_key(me, true); } } } else { self.input_key(me, false); } } self.update_auto_disconnect_timer(); } Some(message::Union::Clipboard(_cb)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard { update_clipboard(_cb, None); } } Some(message::Union::Cliprdr(_clip)) => { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] if let Some(clip) = msg_2_clip(_clip) { log::debug!("got clipfile from client peer"); self.send_to_cm(ipc::Data::ClipboardFile(clip)) } } Some(message::Union::FileAction(fa)) => { if self.file_transfer.is_some() { match fa.union { Some(file_action::Union::ReadDir(rd)) => { self.read_dir(&rd.path, rd.include_hidden); } Some(file_action::Union::AllFiles(f)) => { match fs::get_recursive_files(&f.path, f.include_hidden) { Err(err) => { self.send(fs::new_error(f.id, err, -1)).await; } Ok(files) => { self.send(fs::new_dir(f.id, f.path, files)).await; } } } Some(file_action::Union::Send(s)) => { // server to client let id = s.id; let od = can_enable_overwrite_detection(get_version_number( &self.lr.version, )); let path = s.path.clone(); match fs::TransferJob::new_read( id, "".to_string(), path.clone(), s.file_num, s.include_hidden, false, od, ) { Err(err) => { self.send(fs::new_error(id, err, 0)).await; } Ok(mut job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; let mut files = job.files().to_owned(); job.is_remote = true; job.conn_id = self.inner.id(); self.read_jobs.push(job); self.file_timer = time::interval(MILLI1); self.post_file_audit( FileAuditType::RemoteSend, &s.path, files .drain(..) .map(|f| (f.name, f.size as _)) .collect(), json!({}), ); } } self.file_transferred = true; } Some(file_action::Union::Receive(r)) => { // client to server // note: 1.1.10 introduced identical file detection, which breaks original logic of send/recv files // whenever got send/recv request, check peer version to ensure old version of rustdesk let od = can_enable_overwrite_detection(get_version_number( &self.lr.version, )); self.send_fs(ipc::FS::NewWrite { path: r.path.clone(), id: r.id, file_num: r.file_num, files: r .files .to_vec() .drain(..) .map(|f| (f.name, f.modified_time)) .collect(), overwrite_detection: od, total_size: r.total_size, conn_id: self.inner.id(), }); self.post_file_audit( FileAuditType::RemoteReceive, &r.path, r.files .to_vec() .drain(..) .map(|f| (f.name, f.size as _)) .collect(), json!({}), ); self.file_transferred = true; } Some(file_action::Union::RemoveDir(d)) => { self.send_fs(ipc::FS::RemoveDir { path: d.path.clone(), id: d.id, recursive: d.recursive, }); self.file_remove_log_control.on_remove_dir(d); } Some(file_action::Union::RemoveFile(f)) => { self.send_fs(ipc::FS::RemoveFile { path: f.path.clone(), id: f.id, file_num: f.file_num, }); self.file_remove_log_control.on_remove_file(f); } Some(file_action::Union::Create(c)) => { self.send_fs(ipc::FS::CreateDir { path: c.path.clone(), id: c.id, }); self.send_to_cm(ipc::Data::FileTransferLog(( "create_dir".to_string(), serde_json::to_string(&FileActionLog { id: c.id, conn_id: self.inner.id(), path: c.path, dir: true, }) .unwrap_or_default(), ))); } Some(file_action::Union::Cancel(c)) => { self.send_fs(ipc::FS::CancelWrite { id: c.id }); if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) { self.send_to_cm(ipc::Data::FileTransferLog(( "transfer".to_string(), fs::serialize_transfer_job(job, false, true, ""), ))); } fs::remove_job(c.id, &mut self.read_jobs); } Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { job.confirm(&r); } } _ => {} } } } Some(message::Union::FileResponse(fr)) => match fr.union { Some(file_response::Union::Block(block)) => { self.send_fs(ipc::FS::WriteBlock { id: block.id, file_num: block.file_num, data: block.data, compressed: block.compressed, }); } Some(file_response::Union::Done(d)) => { self.send_fs(ipc::FS::WriteDone { id: d.id, file_num: d.file_num, }); } Some(file_response::Union::Digest(d)) => self.send_fs(ipc::FS::CheckDigest { id: d.id, file_num: d.file_num, file_size: d.file_size, last_modified: d.last_modified, is_upload: true, }), Some(file_response::Union::Error(e)) => { self.send_fs(ipc::FS::WriteError { id: e.id, file_num: e.file_num, err: e.error, }); } _ => {} }, Some(message::Union::Misc(misc)) => match misc.union { Some(misc::Union::SwitchDisplay(s)) => { self.handle_switch_display(s).await; } Some(misc::Union::CaptureDisplays(displays)) => { let add = displays.add.iter().map(|d| *d as usize).collect::>(); let sub = displays.sub.iter().map(|d| *d as usize).collect::>(); let set = displays.set.iter().map(|d| *d as usize).collect::>(); self.capture_displays(&add, &sub, &set).await; } #[cfg(all(windows, feature = "virtual_display_driver"))] Some(misc::Union::ToggleVirtualDisplay(t)) => { self.toggle_virtual_display(t).await; } Some(misc::Union::TogglePrivacyMode(t)) => { self.toggle_privacy_mode(t).await; } Some(misc::Union::ChatMessage(c)) => { self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); self.chat_unanswered = true; self.update_auto_disconnect_timer(); } Some(misc::Union::Option(o)) => { self.update_options(&o).await; } Some(misc::Union::RefreshVideo(r)) => { if r { // Refresh all videos. // Compatibility with old versions and sciter(remote). self.refresh_video_display(None); } self.update_auto_disconnect_timer(); } Some(misc::Union::RefreshVideoDisplay(display)) => { self.refresh_video_display(Some(display as usize)); self.update_auto_disconnect_timer(); } Some(misc::Union::VideoReceived(_)) => { video_service::notify_video_frame_fetched( self.inner.id, Some(Instant::now().into()), ); } Some(misc::Union::CloseReason(_)) => { self.on_close("Peer close", true).await; SESSIONS.lock().unwrap().remove(&self.lr.my_id); return false; } Some(misc::Union::RestartRemoteDevice(_)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.restart { // force_reboot, not work on linux vm and macos 14 #[cfg(any(target_os = "linux", target_os = "windows"))] match system_shutdown::force_reboot() { Ok(_) => log::info!("Restart by the peer"), Err(e) => log::error!("Failed to restart: {}", e), } #[cfg(any(target_os = "linux", target_os = "macos"))] match system_shutdown::reboot() { Ok(_) => log::info!("Restart by the peer"), Err(e) => log::error!("Failed to restart: {}", e), } } } #[cfg(windows)] Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { self.handle_elevation_request(portable_client::StartPara::Direct) .await; } Some(elevation_request::Union::Logon(r)) => { self.handle_elevation_request(portable_client::StartPara::Logon( r.username, r.password, )) .await; } _ => {} }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { // Drop the audio sender previously. drop(std::mem::replace(&mut self.audio_sender, None)); self.audio_sender = Some(start_audio_thread()); self.audio_sender .as_ref() .map(|a| allow_err!(a.send(MediaData::AudioFormat(format)))); } } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { crate::run_me(vec![ "--connect", &self.lr.my_id, "--switch_uuid", uuid.to_string().as_ref(), ]) .ok(); self.on_close("switch sides", false).await; return false; } } #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::ChangeResolution(r)) => self.change_resolution(&r), #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::PluginRequest(p)) => { let msg = crate::plugin::handle_client_event(&p.id, &self.lr.my_id, &p.content); self.send(msg).await; } Some(misc::Union::AutoAdjustFps(fps)) => video_service::VIDEO_QOS .lock() .unwrap() .user_auto_adjust_fps(self.inner.id(), fps), Some(misc::Union::ClientRecordStatus(status)) => video_service::VIDEO_QOS .lock() .unwrap() .user_record(self.inner.id(), status), _ => {} }, Some(message::Union::AudioFrame(frame)) => { if !self.disable_audio { if let Some(sender) = &self.audio_sender { allow_err!(sender.send(MediaData::AudioFrame(Box::new(frame)))); } else { log::warn!( "Processing audio frame without the voice call audio sender." ); } } } Some(message::Union::VoiceCallRequest(request)) => { if request.is_connect { self.voice_call_request_timestamp = Some( NonZeroI64::new(request.req_timestamp) .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { self.close_voice_call().await; } } Some(message::Union::VoiceCallResponse(_response)) => { // TODO: Maybe we can do a voice call from cm directly. } _ => {} } } true } fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { if remove { if failure.0 != 0 { LOGIN_FAILURES[i].lock().unwrap().remove(&self.ip); } return; } if failure.0 == time { failure.1 += 1; failure.2 += 1; } else { failure.0 = time; failure.1 = 1; failure.2 += 1; } LOGIN_FAILURES[i] .lock() .unwrap() .insert(self.ip.clone(), failure); } async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) { let failure = LOGIN_FAILURES[i] .lock() .unwrap() .get(&self.ip) .map(|x| x.clone()) .unwrap_or((0, 0, 0)); let time = (get_time() / 60_000) as i32; let res = if failure.2 > 30 { self.send_login_error("Too many wrong attempts").await; Self::post_alarm_audit( AlarmAuditType::ExceedThirtyAttempts, json!({ "ip": self.ip, "id": self.lr.my_id.clone(), "name": self.lr.my_name.clone(), }), ); false } else if time == failure.0 && failure.1 > 6 { self.send_login_error("Please try 1 minute later").await; Self::post_alarm_audit( AlarmAuditType::SixAttemptsWithinOneMinute, json!({ "ip": self.ip, "id": self.lr.my_id.clone(), "name": self.lr.my_name.clone(), }), ); false } else { true }; ((failure, time), res) } fn refresh_video_display(&self, display: Option) { video_service::refresh(); self.server.upgrade().map(|s| { s.read().unwrap().set_video_service_opt( display, video_service::OPTION_REFRESH, super::service::SERVICE_OPTION_VALUE_TRUE, ); }); } async fn handle_switch_display(&mut self, s: SwitchDisplay) { #[cfg(windows)] if portable_client::running() && *CONN_COUNT.lock().unwrap() > 1 && s.display != (*display_service::PRIMARY_DISPLAY_IDX as i32) { log::info!("Switch to non-primary display is not supported in the elevated mode when there are multiple connections."); let mut msg_out = Message::new(); let res = MessageBox { msgtype: "nook-nocancel-hasclose".to_owned(), title: "Prompt".to_owned(), text: "switch_display_elevated_connections_tip".to_owned(), link: "".to_owned(), ..Default::default() }; msg_out.set_message_box(res); self.send(msg_out).await; return; } let display_idx = s.display as usize; if self.display_idx != display_idx { if let Some(server) = self.server.upgrade() { self.switch_display_to(display_idx, server.clone()); #[cfg(not(any(target_os = "android", target_os = "ios")))] if s.width != 0 && s.height != 0 { self.change_resolution(&Resolution { width: s.width, height: s.height, ..Default::default() }); } } // Send display changed message. // 1. For compatibility with old versions ( < 1.2.4 ). // 2. Sciter version. // 3. Update `SupportedResolutions`. if let Some(msg_out) = video_service::make_display_changed_msg(self.display_idx, None) { self.send(msg_out).await; } } } fn switch_display_to(&mut self, display_idx: usize, server: Arc>) { let new_service_name = video_service::get_service_name(display_idx); let old_service_name = video_service::get_service_name(self.display_idx); let mut lock = server.write().unwrap(); if display_idx != *display_service::PRIMARY_DISPLAY_IDX { if !lock.contains(&new_service_name) { lock.add_service(Box::new(video_service::new(display_idx))); } } // For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately. // Unnecessary capturers will be removed then. if !crate::common::is_support_multi_ui_session(&self.lr.version) { lock.subscribe(&old_service_name, self.inner.clone(), false); } lock.subscribe(&new_service_name, self.inner.clone(), true); self.display_idx = display_idx; } #[cfg(windows)] async fn handle_elevation_request(&mut self, para: portable_client::StartPara) { let mut err; if !self.keyboard { err = "No permission".to_string(); } else { err = "No need to elevate".to_string(); if !crate::platform::is_installed() && !portable_client::running() { err = portable_client::start_portable_service(para) .err() .map_or("".to_string(), |e| e.to_string()); } } let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); msg.set_misc(misc); self.send(msg).await; self.update_auto_disconnect_timer(); } async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { #[cfg(windows)] if portable_client::running() && (add.len() > 0 || set.len() > 1) { log::info!("Capturing multiple displays is not supported in the elevated mode."); let mut msg_out = Message::new(); let res = MessageBox { msgtype: "nook-nocancel-hasclose".to_owned(), title: "Prompt".to_owned(), text: "capture_display_elevated_connections_tip".to_owned(), link: "".to_owned(), ..Default::default() }; msg_out.set_message_box(res); self.send(msg_out).await; return; } if let Some(sever) = self.server.upgrade() { let mut lock = sever.write().unwrap(); for display in add.iter() { let service_name = video_service::get_service_name(*display); if !lock.contains(&service_name) { lock.add_service(Box::new(video_service::new(*display))); } } for display in set.iter() { let service_name = video_service::get_service_name(*display); if !lock.contains(&service_name) { lock.add_service(Box::new(video_service::new(*display))); } } if !add.is_empty() { lock.capture_displays(self.inner.clone(), add, true, false); } else if !sub.is_empty() { lock.capture_displays(self.inner.clone(), sub, false, true); } else { lock.capture_displays(self.inner.clone(), set, true, true); } drop(lock); } } #[cfg(all(windows, feature = "virtual_display_driver"))] async fn toggle_virtual_display(&mut self, t: ToggleVirtualDisplay) { let make_msg = |text: String| { let mut msg_out = Message::new(); let res = MessageBox { msgtype: "nook-nocancel-hasclose".to_owned(), title: "Virtual display".to_owned(), text, link: "".to_owned(), ..Default::default() }; msg_out.set_message_box(res); msg_out }; if t.on { if !virtual_display_manager::is_virtual_display_supported() { self.send(make_msg("idd_not_support_under_win10_2004_tip".to_string())) .await; } else { if let Err(e) = virtual_display_manager::plug_in_index_modes(t.display as _, Vec::new()) { log::error!("Failed to plug in virtual display: {}", e); self.send(make_msg(format!( "Failed to plug in virtual display: {}", e ))) .await; } } } else { let indices = if t.display == -1 { virtual_display_manager::get_virtual_displays() } else { vec![t.display as _] }; if let Err(e) = virtual_display_manager::plug_out_peer_request(&indices) { log::error!("Failed to plug out virtual display {:?}: {}", &indices, e); self.send(make_msg(format!( "Failed to plug out virtual displays: {}", e ))) .await; } } } async fn toggle_privacy_mode(&mut self, t: TogglePrivacyMode) { if t.on { self.turn_on_privacy(t.impl_key).await; } else { self.turn_off_privacy(t.impl_key).await; } } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn change_resolution(&mut self, r: &Resolution) { if self.keyboard { if let Ok(displays) = display_service::try_get_displays() { if let Some(display) = displays.get(self.display_idx) { let name = display.name(); #[cfg(all(windows, feature = "virtual_display_driver"))] if let Some(_ok) = virtual_display_manager::change_resolution_if_is_virtual_display( &name, r.width as _, r.height as _, ) { return; } display_service::set_last_changed_resolution( &name, (display.width() as _, display.height() as _), (r.width, r.height), ); if let Err(e) = crate::platform::change_resolution(&name, r.width as _, r.height as _) { log::error!( "Failed to change resolution '{}' to ({},{}): {:?}", &name, r.width, r.height, e ); } } } } } pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); log::debug!("Backup the sound input device {}", audio_input_device); self.audio_input_device_before_voice_call = Some(audio_input_device); // Switch to default input device let default_sound_device = get_default_sound_input(); if let Some(device) = default_sound_device { set_sound_input(device); } self.send_to_cm(Data::StartVoiceCall); } else { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } self.send(msg).await; } else { log::warn!("Possible a voice call attack."); } } pub async fn close_voice_call(&mut self) { // Restore to the prior audio device. if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); } // Notify the connection manager that the voice call has been closed. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } async fn update_options(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { if o.custom_image_quality > 0 { image_quality = o.custom_image_quality; } else { image_quality = -1; } } else { image_quality = q.value(); } if image_quality > 0 { video_service::VIDEO_QOS .lock() .unwrap() .user_image_quality(self.inner.id(), image_quality); } } if o.custom_fps > 0 { video_service::VIDEO_QOS .lock() .unwrap() .user_custom_fps(self.inner.id(), o.custom_fps as _); } if let Some(q) = o.supported_decoding.clone().take() { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Update(self.inner.id(), q)); } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; } } #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Ok(q) = o.show_remote_cursor.enum_value() { if q != BoolOption::NotSet { self.show_remote_cursor = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { s.write().unwrap().subscribe( NAME_CURSOR, self.inner.clone(), self.peer_keyboard_enabled() || self.show_remote_cursor, ); s.write().unwrap().subscribe( NAME_POS, self.inner.clone(), self.show_remote_cursor, ); } } } if let Ok(q) = o.disable_audio.enum_value() { if q != BoolOption::NotSet { self.disable_audio = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { s.write().unwrap().subscribe( super::audio_service::NAME, self.inner.clone(), self.audio_enabled(), ); } } } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; self.send_to_cm(ipc::Data::ClipboardFileEnabled( self.file_transfer_enabled(), )); } } if let Ok(q) = o.disable_clipboard.enum_value() { if q != BoolOption::NotSet { self.disable_clipboard = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), self.clipboard_enabled() && self.peer_keyboard_enabled(), ); } } } if let Ok(q) = o.disable_keyboard.enum_value() { if q != BoolOption::NotSet { self.disable_keyboard = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), self.clipboard_enabled() && self.peer_keyboard_enabled(), ); s.write().unwrap().subscribe( NAME_CURSOR, self.inner.clone(), self.peer_keyboard_enabled() || self.show_remote_cursor, ); } } } // For compatibility with old versions ( < 1.2.4 ). if hbb_common::get_version_number(&self.lr.version) < hbb_common::get_version_number("1.2.4") { if let Ok(q) = o.privacy_mode.enum_value() { if self.keyboard { match q { BoolOption::Yes => { self.turn_on_privacy("".to_owned()).await; } BoolOption::No => { self.turn_off_privacy("".to_owned()).await; } _ => {} } } } } if let Ok(q) = o.block_input.enum_value() { if self.keyboard && self.block_input { match q { BoolOption::Yes => { self.tx_input.send(MessageInput::BlockOn).ok(); } BoolOption::No => { self.tx_input.send(MessageInput::BlockOff).ok(); } _ => {} } } else { if q != BoolOption::NotSet { let state = if q == BoolOption::Yes { back_notification::BlockInputState::BlkOnFailed } else { back_notification::BlockInputState::BlkOffFailed }; if let Some(tx) = &self.inner.tx { Self::send_block_input_error(tx, state, "No permission".to_string()); } } } } } async fn turn_on_privacy(&mut self, impl_key: String) { let msg_out = if !privacy_mode::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), impl_key, ) } else { let is_pre_privacy_on = privacy_mode::is_in_privacy_mode(); let pre_impl_key = privacy_mode::get_cur_impl_key(); let turn_on_res = privacy_mode::turn_on_privacy(&impl_key, self.inner.id); if is_pre_privacy_on { if let Some(pre_impl_key) = pre_impl_key { if !privacy_mode::is_current_privacy_mode_impl(&pre_impl_key) { let off_msg = crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, pre_impl_key, ); self.send(off_msg).await; } } } match turn_on_res { Some(Ok(res)) => { if res { let err_msg = privacy_mode::check_privacy_mode_err( self.inner.id, self.display_idx, 5_000, ); if err_msg.is_empty() { crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOnSucceeded, impl_key, ) } else { log::error!( "Check privacy mode failed: {}, turn off privacy mode.", &err_msg ); let _ = Self::turn_off_privacy_to_msg(self.inner.id); crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOnFailed, err_msg, impl_key, ) } } else { crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOnFailedPlugin, impl_key, ) } } Some(Err(e)) => { log::error!("Failed to turn on privacy mode. {}", e); if !privacy_mode::is_in_privacy_mode() { let _ = Self::turn_off_privacy_to_msg( privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, ); } crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOnFailed, e.to_string(), impl_key, ) } None => crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOffFailed, "Not supported".to_string(), impl_key, ), } }; self.send(msg_out).await; } async fn turn_off_privacy(&mut self, impl_key: String) { let msg_out = if !privacy_mode::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, // This error message is used for magnifier. It is ok to use it here. "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), impl_key, ) } else { Self::turn_off_privacy_to_msg(self.inner.id) }; self.send(msg_out).await; } pub fn turn_off_privacy_to_msg(_conn_id: i32) -> Message { let impl_key = "".to_owned(); match privacy_mode::turn_off_privacy(_conn_id, None) { Some(Ok(_)) => crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, impl_key, ), Some(Err(e)) => { log::error!("Failed to turn off privacy mode {}", e); crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOffFailed, e.to_string(), impl_key, ) } None => crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOffFailed, "Not supported".to_string(), impl_key, ), } } async fn on_close(&mut self, reason: &str, lock: bool) { if self.closed { return; } self.closed = true; log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] let data = if self.chat_unanswered || self.file_transferred && cfg!(feature = "flutter") { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } // The `reason` should be consistent with `check_if_retry` if not empty async fn send_close_reason_no_retry(&mut self, reason: &str) { let mut misc = Misc::new(); if reason.is_empty() { misc.set_close_reason("Closed manually by the peer".to_string()); } else { misc.set_close_reason(reason.to_string()); } let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; SESSIONS.lock().unwrap().remove(&self.lr.my_id); } fn read_dir(&mut self, dir: &str, include_hidden: bool) { let dir = dir.to_string(); self.send_fs(ipc::FS::ReadDir { dir, include_hidden, }); } #[inline] async fn send(&mut self, msg: Message) { allow_err!(self.stream.send(&msg).await); } pub fn alive_conns() -> Vec { ALIVE_CONNS.lock().unwrap().clone() } #[cfg(windows)] fn portable_check(&mut self) { if self.portable.is_installed || self.file_transfer.is_some() || self.port_forward_socket.is_some() || !self.keyboard { return; } let running = portable_client::running(); let show_elevation = !running; self.send_to_cm(ipc::Data::DataPortableService( ipc::DataPortableService::CmShowElevation(show_elevation), )); if self.authorized { let p = &mut self.portable; if Some(running) != p.last_running { p.last_running = Some(running); let mut misc = Misc::new(); misc.set_portable_service_running(running); let mut msg = Message::new(); msg.set_misc(misc); self.inner.send(msg.into()); } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if p.last_uac != uac { p.last_uac = uac; if !uac || !running { let mut misc = Misc::new(); misc.set_uac(uac); let mut msg = Message::new(); msg.set_misc(misc); self.inner.send(msg.into()); } } let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED .lock() .unwrap() .clone(); if p.last_foreground_window_elevated != foreground_window_elevated { p.last_foreground_window_elevated = foreground_window_elevated; if !foreground_window_elevated || !running { let mut misc = Misc::new(); misc.set_foreground_window_elevated(foreground_window_elevated); let mut msg = Message::new(); msg.set_misc(misc); self.inner.send(msg.into()); } } } } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn release_pressed_modifiers(&mut self) { for modifier in self.pressed_modifiers.iter() { rdev::simulate(&rdev::EventType::KeyRelease(*modifier)).ok(); } self.pressed_modifiers.clear(); } fn get_auto_disconenct_timer() -> Option<(Instant, u64)> { if Config::get_option("allow-auto-disconnect") == "Y" { let mut minute: u64 = Config::get_option("auto-disconnect-timeout") .parse() .unwrap_or(10); if minute == 0 { minute = 10; } Some((Instant::now(), minute)) } else { None } } fn update_auto_disconnect_timer(&mut self) { self.auto_disconnect_timer .as_mut() .map(|t| t.0 = Instant::now()); } #[cfg(feature = "gpucodec")] fn update_supported_encoding(&mut self) { let not_use = Some(scrap::gpucodec::GpuEncoder::not_use()); if !self.authorized || self.supported_encoding_flag.0 && self.supported_encoding_flag.1 == not_use { return; } let mut misc: Misc = Misc::new(); let supported_encoding = scrap::codec::Encoder::supported_encoding(); log::info!("update supported encoding: {:?}", supported_encoding); misc.set_supported_encoding(supported_encoding); let mut msg = Message::new(); msg.set_misc(misc); self.inner.send(msg.into()); self.supported_encoding_flag = (true, not_use); } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { SWITCH_SIDES_UUID .lock() .unwrap() .insert(id, (tokio::time::Instant::now(), uuid)); } #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, mut _rx_desktop_ready: mpsc::Receiver<()>, tx_stream_ready: mpsc::Sender<()>, ) -> ResultType<()> { use hbb_common::anyhow::anyhow; loop { if !crate::platform::is_prelogin() { break; } sleep(1.).await; } let mut stream = None; if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); } else { #[allow(unused_mut)] #[allow(unused_assignments)] let mut args = vec!["--cm"]; #[allow(unused_mut)] #[cfg(target_os = "linux")] let mut user = None; // Cm run as user, wait until desktop session is ready. #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() { let mut username = linux_desktop_manager::get_username(); loop { if !username.is_empty() { break; } let _res = timeout(1_000, _rx_desktop_ready.recv()).await; username = linux_desktop_manager::get_username(); } let uid = { let output = run_cmds(&format!("id -u {}", &username))?; let output = output.trim(); if output.is_empty() || !output.parse::().is_ok() { bail!("Invalid username {}", &username); } output.to_string() }; user = Some((uid, username)); args = vec!["--cm-no-ui"]; } let run_done; if crate::platform::is_root() { let mut res = Ok(None); for _ in 0..10 { #[cfg(not(target_os = "linux"))] { log::debug!("Start cm"); res = crate::platform::run_as_user(args.clone()); } #[cfg(target_os = "linux")] { log::debug!("Start cm"); res = crate::platform::run_as_user( args.clone(), user.clone(), None::<(&str, &str)>, ); } if res.is_ok() { break; } sleep(1.).await; } if let Some(task) = res? { super::CHILD_PROCESS.lock().unwrap().push(task); } run_done = true; } else { run_done = false; } if !run_done { log::debug!("Start cm"); super::CHILD_PROCESS .lock() .unwrap() .push(crate::run_me(args)?); } for _ in 0..20 { sleep(0.3).await; if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); break; } } if stream.is_none() { bail!("Failed to connect to connection manager"); } } let _res = tx_stream_ready.send(()).await; let mut stream = stream.ok_or(anyhow!("none stream"))?; loop { tokio::select! { res = stream.next() => { match res { Err(err) => { return Err(err.into()); } Ok(Some(data)) => { match data { ipc::Data::ClickTime(_)=> { let ct = CLICK_TIME.load(Ordering::SeqCst); let data = ipc::Data::ClickTime(ct); stream.send(&data).await?; } _ => { tx_from_cm.send(data)?; } } } _ => {} } } res = rx_to_cm.recv() => { match res { Some(data) => { if let Data::FS(ipc::FS::WriteBlock{id, file_num, data, compressed}) = data { stream.send(&Data::FS(ipc::FS::WriteBlock{id, file_num, data: Bytes::new(), compressed})).await?; stream.send_raw(data).await?; } else { stream.send(&data).await?; } } None => { bail!("expected"); } } } } } } // in case screen is sleep and blank, here to activate it fn try_activate_screen() { #[cfg(windows)] std::thread::spawn(|| { mouse_move_relative(-6, -6); std::thread::sleep(std::time::Duration::from_millis(30)); mouse_move_relative(6, 6); }); } pub enum AlarmAuditType { IpWhitelist = 0, ExceedThirtyAttempts = 1, SixAttemptsWithinOneMinute = 2, } pub enum FileAuditType { RemoteSend = 0, RemoteReceive = 1, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct FileActionLog { id: i32, conn_id: i32, path: String, dir: bool, } struct FileRemoveLogControl { conn_id: i32, instant: Instant, removed_files: Vec, removed_dirs: Vec, } impl FileRemoveLogControl { fn new(conn_id: i32) -> Self { FileRemoveLogControl { conn_id, instant: Instant::now(), removed_files: vec![], removed_dirs: vec![], } } fn on_remove_file(&mut self, f: FileRemoveFile) -> Option { self.instant = Instant::now(); self.removed_files.push(f.clone()); Some(ipc::Data::FileTransferLog(( "remove".to_string(), serde_json::to_string(&FileActionLog { id: f.id, conn_id: self.conn_id, path: f.path, dir: false, }) .unwrap_or_default(), ))) } fn on_remove_dir(&mut self, d: FileRemoveDir) -> Option { self.instant = Instant::now(); let direct_child = |parent: &str, child: &str| { PathBuf::from(child).parent().map(|x| x.to_path_buf()) == Some(PathBuf::from(parent)) }; self.removed_files .retain(|f| !direct_child(&f.path, &d.path)); self.removed_dirs .retain(|x| !direct_child(&d.path, &x.path)); if !self .removed_dirs .iter() .any(|x| direct_child(&x.path, &d.path)) { self.removed_dirs.push(d.clone()); } Some(ipc::Data::FileTransferLog(( "remove".to_string(), serde_json::to_string(&FileActionLog { id: d.id, conn_id: self.conn_id, path: d.path, dir: true, }) .unwrap_or_default(), ))) } fn on_timer(&mut self) -> Vec { if self.instant.elapsed().as_secs() < 1 { return vec![]; } let mut v: Vec = vec![]; self.removed_files .drain(..) .map(|f| { v.push(ipc::Data::FileTransferLog(( "remove".to_string(), serde_json::to_string(&FileActionLog { id: f.id, conn_id: self.conn_id, path: f.path, dir: false, }) .unwrap_or_default(), ))); }) .count(); self.removed_dirs .drain(..) .map(|d| { v.push(ipc::Data::FileTransferLog(( "remove".to_string(), serde_json::to_string(&FileActionLog { id: d.id, conn_id: self.conn_id, path: d.path, dir: true, }) .unwrap_or_default(), ))); }) .count(); v } } fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { use crate::platform::{get_wakelock, WakeLock}; let (tx, rx) = std::sync::mpsc::channel::<(usize, usize)>(); std::thread::spawn(move || { let mut wakelock: Option = None; let mut last_display = false; loop { match rx.recv() { Ok((conn_count, remote_count)) => { if conn_count == 0 { wakelock = None; log::info!("drop wakelock"); } else { let mut display = remote_count > 0; if let Some(_w) = wakelock.as_mut() { if display != last_display { #[cfg(any(target_os = "windows", target_os = "macos"))] { log::info!("set wakelock display to {display}"); if let Err(e) = _w.set_display(display) { log::error!( "failed to set wakelock display to {display}: {e:?}" ); } } } } else { if cfg!(target_os = "linux") { display = true; } wakelock = Some(get_wakelock(display)); } last_display = display; } } Err(e) => { log::error!("wakelock receive error: {e:?}"); break; } } } }); tx } #[cfg(windows)] pub struct PortableState { pub last_uac: bool, pub last_foreground_window_elevated: bool, pub last_running: Option, pub is_installed: bool, } #[cfg(windows)] impl Default for PortableState { fn default() -> Self { Self { is_installed: crate::platform::is_installed(), last_uac: Default::default(), last_foreground_window_elevated: Default::default(), last_running: Default::default(), } } } impl Drop for Connection { fn drop(&mut self) { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.release_pressed_modifiers(); } } #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] struct LinuxHeadlessHandle { pub is_headless_allowed: bool, pub is_headless: bool, pub wait_ipc_timeout: u64, pub rx_cm_stream_ready: mpsc::Receiver<()>, pub tx_desktop_ready: mpsc::Sender<()>, } #[cfg(all(target_os = "linux", feature = "linux_headless"))] #[cfg(not(any(feature = "flatpak", feature = "appimage")))] impl LinuxHeadlessHandle { pub fn new(rx_cm_stream_ready: mpsc::Receiver<()>, tx_desktop_ready: mpsc::Sender<()>) -> Self { let is_headless_allowed = crate::platform::is_headless_allowed(); let is_headless = is_headless_allowed && linux_desktop_manager::is_headless(); Self { is_headless_allowed, is_headless, wait_ipc_timeout: 10_000, rx_cm_stream_ready, tx_desktop_ready, } } pub fn try_start_desktop(&mut self, os_login: Option<&OSLogin>) -> String { if self.is_headless_allowed { match os_login { Some(os_login) => { linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password) } None => linux_desktop_manager::try_start_desktop("", ""), } } else { "".to_string() } } pub async fn wait_desktop_cm_ready(&mut self) { if self.is_headless { self.tx_desktop_ready.send(()).await.ok(); let _res = timeout(self.wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; } } } extern "C" fn connection_shutdown_hook() { // https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout // Please make sure there is no print in the call stack #[cfg(any(target_os = "windows", target_os = "linux"))] { *WALLPAPER_REMOVER.lock().unwrap() = None; } } mod raii { use super::*; pub struct ConnectionID(i32); impl ConnectionID { pub fn new(id: i32) -> Self { ALIVE_CONNS.lock().unwrap().push(id); Self(id) } } impl Drop for ConnectionID { fn drop(&mut self) { let mut active_conns_lock = ALIVE_CONNS.lock().unwrap(); active_conns_lock.retain(|&c| c != self.0); video_service::VIDEO_QOS .lock() .unwrap() .on_connection_close(self.0); } } pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { pub fn new(id: i32, conn_type: AuthConnType) -> Self { AUTHED_CONNS.lock().unwrap().push((id, conn_type)); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); Self(id, conn_type) } fn check_wake_lock() { let conn_count = AUTHED_CONNS.lock().unwrap().len(); let remote_count = AUTHED_CONNS .lock() .unwrap() .iter() .filter(|c| c.1 == AuthConnType::Remote) .count(); allow_err!(WAKELOCK_SENDER .lock() .unwrap() .send((conn_count, remote_count))); } } impl Drop for AuthedConnID { fn drop(&mut self) { if self.1 == AuthConnType::Remote { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); } AUTHED_CONNS.lock().unwrap().retain(|&c| c.0 != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() .iter() .filter(|c| c.1 == AuthConnType::Remote) .count(); if remote_count == 0 { #[cfg(any(target_os = "windows", target_os = "linux"))] { *WALLPAPER_REMOVER.lock().unwrap() = None; } #[cfg(not(any(target_os = "android", target_os = "ios")))] display_service::reset_resolutions(); #[cfg(all(windows, feature = "virtual_display_driver"))] let _ = virtual_display_manager::reset_all(); } Self::check_wake_lock(); } } }