diff --git a/src/client/helper.rs b/src/client/helper.rs index e3acf3a44..20acd811a 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -5,7 +5,7 @@ use std::{ use hbb_common::{ log, - message_proto::{video_frame, VideoFrame}, + message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time, }; const MAX_LATENCY: i64 = 500; @@ -115,3 +115,24 @@ pub struct QualityStatus { pub target_bitrate: Option, pub codec_format: Option, } + +#[inline] +pub fn new_voice_call_request(is_connect: bool) -> Message { + let mut req = VoiceCallRequest::new(); + req.is_connect = is_connect; + req.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(req); + msg +} + +#[inline] +pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message { + let mut resp = VoiceCallResponse::new(); + resp.accepted = accepted; + resp.req_timestamp = request_timestamp; + resp.ack_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_response(resp); + msg +} \ No newline at end of file diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d0e72a7e6..8f2b45321 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,38 +1,40 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::num::NonZeroI64; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::{allow_err, message_proto::*, sleep, get_time}; -use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::tokio::sync::mpsc::error::TryRecvError; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; +use hbb_common::{allow_err, get_time, message_proto::*, sleep}; +use hbb_common::{fs, log, Stream}; -use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; -use crate::{client::Data, client::Interface}; use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, + SERVER_KEYBOARD_ENABLED, }; -use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, @@ -41,7 +43,8 @@ pub struct Remote { receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, // Stop sending local audio to remote client. - stop_local_audio_sender: Option>, + stop_voice_call_sender: Option>, + voice_call_request_timestamp: Option, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -83,7 +86,8 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, - stop_local_audio_sender: None, + stop_voice_call_sender: None, + voice_call_request_timestamp: None, } } @@ -217,7 +221,7 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(s) = self.stop_local_audio_sender.take() { + if let Some(s) = self.stop_voice_call_sender.take() { s.send(()).ok(); } } @@ -261,8 +265,15 @@ impl Remote { } } - // Start a local audio recorder, records audio and send to remote - fn start_client_audio(&mut self) -> Option> { + fn stop_voice_call(&mut self) { + let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None); + if let Some(stopper) = voice_call_sender { + let _ = stopper.send(()); + } + } + + // Start a voice call recorder, records audio and send to remote + fn start_voice_call(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -731,19 +742,17 @@ impl Remote { allow_err!(peer.send(&msg).await); } Data::NewVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = true; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); allow_err!(peer.send(&msg).await); } Data::CloseVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = false; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + self.stop_voice_call(); + let msg = new_voice_call_request(false); allow_err!(peer.send(&msg).await); } _ => {} @@ -1238,11 +1247,25 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(request)) => { - // TODO + Some(message::Union::VoiceCallRequest(_request)) => { + // TODO: maybe we will do voice call from the peer. } Some(message::Union::VoiceCallResponse(response)) => { - // TODO + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepts the voice call. + self.handler.on_voice_call_start(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_stop("Refused"); + } + } + } } _ => {} } diff --git a/src/flutter.rs b/src/flutter.rs index b4f1f6bc6..7062d85df 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -394,6 +394,22 @@ impl InvokeUiSession for FlutterHandler { fn switch_back(&self, peer_id: &str) { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + + fn on_voice_call_start(&self) { + self.push_event("on_voice_call_start", [].into()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.push_event("on_voice_call_stop", [("reason", reason)].into()) + } + + fn on_voice_call_waiting(&self) { + self.push_event("on_voice_call_waiting", [].into()); + } + + fn on_voice_call_incoming(&self) { + self.push_event("on_voice_call_incoming", [].into()); + } } /// Create a new remote session with the given id. diff --git a/src/ipc.rs b/src/ipc.rs index d610fb84d..18f618847 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -210,7 +210,10 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, - UrlLink(String) + UrlLink(String), + VoiceCallIncoming, + VoiceCallResponse(bool), + CloseVoiceCall(String), } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 65039f0fe..5a9abba9c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -453,5 +453,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), + ("Refused", "已拒绝") ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index c3acae9cc..1007c71ca 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,11 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; +use crate::{ + client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + common::{get_default_sound_input, set_sound_input}, + video_service, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -32,7 +36,10 @@ 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::sync::{atomic::AtomicI64, mpsc as std_mpsc}; +use std::{ + num::NonZeroI64, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -90,13 +97,19 @@ pub struct Connection { recording: bool, last_test_delay: i64, lock_after_session_end: bool, - show_remote_cursor: bool, // by peer + show_remote_cursor: bool, + // by peer ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer - enable_file_transfer: bool, // by peer - audio_sender: MediaSender, // audio by the remote peer/client - tx_input: std_mpsc::Sender, // handle input messages + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + enable_file_transfer: bool, + // by peer + audio_sender: MediaSender, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages video_ack_required: bool, peer_info: (String, String), server_audit_conn: String, @@ -107,6 +120,8 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + voice_call_request_timestamp: Option, + audio_input_device_before_voice_call: Option, } impl ConnInner { @@ -216,6 +231,8 @@ impl Connection { portable: Default::default(), from_switch: false, audio_sender, + voice_call_request_timestamp: None, + audio_input_device_before_voice_call: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -380,6 +397,12 @@ impl Connection { msg.set_misc(misc); conn.send(msg).await; } + ipc::Data::VoiceCallResponse(accepted) => { + conn.start_voice_call().await; + } + ipc::Data::CloseVoiceCall(_reason) => { + conn.close_voice_call().await; + } _ => {} } }, @@ -650,15 +673,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .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() + .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; @@ -784,7 +807,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -811,7 +834,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -855,7 +878,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -1138,7 +1161,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1302,12 +1325,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); + } } - } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1490,15 +1513,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), + } } } - } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1508,8 +1531,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1527,8 +1550,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1541,12 +1564,7 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - if !self.disable_audio { - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } @@ -1559,7 +1577,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1573,10 +1591,19 @@ impl Connection { } } Some(message::Union::VoiceCallRequest(request)) => { - // TODO + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Call cm. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } } - Some(message::Union::VoiceCallResponse(response)) => { - // TODO + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. } _ => {} } @@ -1584,6 +1611,34 @@ impl Connection { true } + pub async fn start_voice_call(&self) { + if let Some(ts) = conn.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + conn.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + conn.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); + } + } + } 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. + self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + } + } + async fn update_option(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { @@ -1752,13 +1807,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5d6692c3b..eb83890d4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -266,6 +266,22 @@ impl InvokeUiSession for SciterHandler { } fn switch_back(&self, _id: &str) {} + + fn on_voice_call_start(&self) { + self.call("onVoiceCallStart", &make_args!()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.call("onVoiceCallStop", &make_args!(reason)); + } + + fn on_voice_call_waiting(&self) { + self.call("onVoiceCallWaiting", &make_args!()); + } + + fn on_voice_call_incoming(&self) { + self.call("onVoiceCallIncoming", &make_args!()); + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f6827523..a740b373e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,6 +705,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn on_voice_call_start(&self); + fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_waiting(&self); + fn on_voice_call_incoming(&self); } impl Deref for Session {