diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1d59d0202..099a04a15 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2304,16 +2304,19 @@ connectMainDesktop(String id, required bool isRDP, bool? forceRelay, String? password, + String? connToken, bool? isSharedPassword}) async { if (isFileTransfer) { await rustDeskWinManager.newFileTransfer(id, password: password, isSharedPassword: isSharedPassword, + connToken: connToken, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, password: password, isSharedPassword: isSharedPassword, + connToken: connToken, forceRelay: forceRelay); } else { await rustDeskWinManager.newRemoteDesktop(id, @@ -2333,6 +2336,7 @@ connect(BuildContext context, String id, bool isRDP = false, bool forceRelay = false, String? password, + String? connToken, bool? isSharedPassword}) async { if (id == '') return; if (!isDesktop || desktopType == DesktopType.main) { @@ -2374,6 +2378,7 @@ connect(BuildContext context, String id, 'password': password, 'isSharedPassword': isSharedPassword, 'forceRelay': forceRelay, + 'connToken': connToken, }); } } else { diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 0b56d9f4c..269d7904d 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -147,12 +147,23 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } + + connectWithToken( + {required bool isFileTransfer, required bool isTcpTunneling}) { + final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); + connect(context, id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + connToken: connToken); + } + // transferFile if (isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => connect(context, id, isFileTransfer: true)), + onPressed: () => + connectWithToken(isFileTransfer: true, isTcpTunneling: false)), ); } // tcpTunneling @@ -160,7 +171,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => connect(context, id, isTcpTunneling: true)), + onPressed: () => + connectWithToken(isFileTransfer: false, isTcpTunneling: true)), ); } // note diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 90fa67ded..493e4ca47 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -774,6 +774,7 @@ class _DesktopHomePageState extends State isRDP: call.arguments['isRDP'], password: call.arguments['password'], forceRelay: call.arguments['forceRelay'], + connToken: call.arguments['connToken'], ); } else if (call.method == kWindowEventMoveTabToNewWindow) { final args = call.arguments.split(','); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index ba1a37fb1..90b8d7dcb 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -58,12 +58,14 @@ class FileManagerPage extends StatefulWidget { required this.password, required this.isSharedPassword, this.tabController, + this.connToken, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; final bool? forceRelay; + final String? connToken; final DesktopTabController? tabController; @override @@ -90,6 +92,7 @@ class _FileManagerPageState extends State isFileTransfer: true, password: widget.password, isSharedPassword: widget.isSharedPassword, + connToken: widget.connToken, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index ca17ac3ff..cc77cdd95 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -48,6 +48,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: params['isSharedPassword'], tabController: tabController, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -56,7 +57,7 @@ class _FileManagerTabPageState extends State { super.initState(); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( + debugPrint( "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); // for simplify, just replace connectionId if (call.method == kWindowEventNewFileTransfer) { @@ -76,6 +77,7 @@ class _FileManagerTabPageState extends State { isSharedPassword: args['isSharedPassword'], tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 5541cb8b3..d6d243c50 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -33,6 +33,7 @@ class PortForwardPage extends StatefulWidget { required this.isRDP, required this.isSharedPassword, this.forceRelay, + this.connToken, }) : super(key: key); final String id; final String? password; @@ -40,6 +41,7 @@ class PortForwardPage extends StatefulWidget { final bool isRDP; final bool? forceRelay; final bool? isSharedPassword; + final String? connToken; @override State createState() => _PortForwardPageState(); @@ -62,6 +64,7 @@ class _PortForwardPageState extends State password: widget.password, isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, + connToken: widget.connToken, isRdp: widget.isRDP); Get.put(_ffi, tag: 'pf_${widget.id}'); debugPrint("Port forward page init success with id ${widget.id}"); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 812f7aa99..f399f7cab 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -48,6 +48,7 @@ class _PortForwardTabPageState extends State { tabController: tabController, isRDP: isRDP, forceRelay: params['forceRelay'], + connToken: params['connToken'], ))); } @@ -82,6 +83,7 @@ class _PortForwardTabPageState extends State { isRDP: isRDP, tabController: tabController, forceRelay: args['forceRelay'], + connToken: args['connToken'], ))); } else if (call.method == "onDestroy") { tabController.clear(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index dc0153da0..efd437e1f 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -395,7 +395,7 @@ class _ConnectionTabPageState extends State { RemoteCountState.find().value = tabController.length; Future _remoteMethodHandler(call, fromWindowId) async { - print( + debugPrint( "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); dynamic returnValue; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f0e4cd75f..8bd0530f1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -375,7 +375,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'plugin_option') { handleOption(evt); } else if (name == "sync_peer_hash_password_to_personal_ab") { - if (desktopType == DesktopType.main || isWeb) { + if (desktopType == DesktopType.main || isWeb || isMobile) { final id = evt['id']; final hash = evt['hash']; if (id != null && hash != null) { @@ -2462,6 +2462,7 @@ class FFI { String? switchUuid, String? password, bool? isSharedPassword, + String? connToken, bool? forceRelay, int? tabWindowId, int? display, @@ -2498,6 +2499,7 @@ class FFI { forceRelay: forceRelay ?? false, password: password ?? '', isSharedPassword: isSharedPassword ?? false, + connToken: connToken, ); } else if (display != null) { if (displays == null) { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index fa35b4fe9..70001ffdf 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -201,6 +201,7 @@ class RustDeskMultiWindowManager { String? switchUuid, bool? isRDP, bool? isSharedPassword, + String? connToken, }) async { var params = { "type": type.index, @@ -217,6 +218,9 @@ class RustDeskMultiWindowManager { if (isSharedPassword != null) { params['isSharedPassword'] = isSharedPassword; } + if (connToken != null) { + params['connToken'] = connToken; + } final msg = jsonEncode(params); // separate window for file transfer is not supported @@ -254,8 +258,13 @@ class RustDeskMultiWindowManager { ); } - Future newFileTransfer(String remoteId, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newFileTransfer( + String remoteId, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.FileTransfer, kWindowEventNewFileTransfer, @@ -264,11 +273,18 @@ class RustDeskMultiWindowManager { password: password, forceRelay: forceRelay, isSharedPassword: isSharedPassword, + connToken: connToken, ); } - Future newPortForward(String remoteId, bool isRDP, - {String? password, bool? isSharedPassword, bool? forceRelay}) async { + Future newPortForward( + String remoteId, + bool isRDP, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { return await newSession( WindowType.PortForward, kWindowEventNewPortForward, @@ -278,6 +294,7 @@ class RustDeskMultiWindowManager { forceRelay: forceRelay, isRDP: isRDP, isSharedPassword: isSharedPassword, + connToken: connToken, ); } diff --git a/src/client.rs b/src/client.rs index 4b86d189b..0b5293d22 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; #[cfg(not(any(target_os = "android", target_os = "linux")))] use ringbuf::{ring_buffer::RbBase, Rb}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ collections::HashMap, @@ -1274,7 +1275,7 @@ impl VideoHandler { } // The source of sent password -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] enum PasswordSource { PersonalAb(Vec), SharedAb(String), @@ -1320,6 +1321,13 @@ impl PasswordSource { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ConnToken { + password: Vec, + password_source: PasswordSource, + session_id: u64, +} + /// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { @@ -1376,6 +1384,7 @@ impl LoginConfigHandler { mut force_relay: bool, adapter_luid: Option, shared_password: Option, + conn_token: Option, ) { let mut id = id; if id.contains("@") { @@ -1419,10 +1428,22 @@ impl LoginConfigHandler { let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; - let mut sid = rand::random(); + + let conn_token = conn_token + .map(|x| serde_json::from_str::(&x).ok()) + .flatten(); + let mut sid = 0; + if let Some(token) = conn_token { + sid = token.session_id; + self.password = token.password; // use as last password + self.password_source = token.password_source; + } if sid == 0 { - // you won the lottery - sid = 1; + sid = rand::random(); + if sid == 0 { + // you won the lottery + sid = 1; + } } self.session_id = sid; self.supported_encoding = Default::default(); @@ -2223,6 +2244,18 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn get_conn_token(&self) -> Option { + if self.password.is_empty() { + return None; + } + serde_json::to_string(&ConnToken { + password: self.password.clone(), + password_source: self.password_source.clone(), + session_id: self.session_id, + }) + .ok() + } } /// Media data. diff --git a/src/flutter.rs b/src/flutter.rs index 69266f51c..a1c9c7e34 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1126,6 +1126,7 @@ pub fn session_add( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER @@ -1180,6 +1181,7 @@ pub fn session_add( force_relay, get_adapter_luid(), shared_password, + conn_token, ); let session = Arc::new(session.clone()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5dfcebacc..7a0c5e874 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -121,6 +121,7 @@ pub fn session_add_sync( force_relay: bool, password: String, is_shared_password: bool, + conn_token: Option, ) -> SyncReturn { if let Err(e) = session_add( &session_id, @@ -132,6 +133,7 @@ pub fn session_add_sync( force_relay, password, is_shared_password, + conn_token, ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -1341,6 +1343,14 @@ pub fn session_close_voice_call(session_id: SessionID) { } } +pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.get_conn_token()) + } else { + SyncReturn(None) + } +} + pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { crate::ui_cm_interface::handle_incoming_voice_call(id, accept); } diff --git a/src/server/connection.rs b/src/server/connection.rs index dbe8b9614..dc184ceac 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -64,9 +64,9 @@ 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 SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub 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())); } @@ -140,13 +140,20 @@ enum MessageInput { BlockOffPlugin(String), } -#[derive(Clone, Debug)] -struct Session { +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct SessionKey { + peer_id: String, name: String, session_id: u64, +} + +#[derive(Clone, Debug)] +struct Session { last_recv_time: Arc>, random_password: String, tfa: bool, + conn_type: AuthConnType, + conn_id: i32, } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1131,6 +1138,7 @@ impl Connection { self.authed_conn_id = Some(self::raii::AuthedConnID::new( self.inner.id(), auth_conn_type, + self.session_key(), )); self.post_conn_audit( json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), @@ -1541,14 +1549,14 @@ impl Connection { 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(), + raii::AuthedConnID::insert_session( + self.session_key(), 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, + conn_type: self.conn_type(), + conn_id: self.inner.id(), }, ); return true; @@ -1570,21 +1578,19 @@ impl Connection { let session = SESSIONS .lock() .unwrap() - .get(&self.lr.my_id) + .get(&self.session_key()) .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() + if !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); + session.conn_id = self.inner.id(); + session.conn_type = self.conn_type(); + raii::AuthedConnID::insert_session(self.session_key(), session); + log::info!("is recent session"); return true; } } @@ -1844,23 +1850,22 @@ impl Connection { let session = SESSIONS .lock() .unwrap() - .get(&self.lr.my_id) + .get(&self.session_key()) .map(|s| s.to_owned()); if let Some(mut session) = session { session.tfa = true; - SESSIONS - .lock() - .unwrap() - .insert(self.lr.my_id.clone(), session); + session.conn_id = self.inner.id(); + session.conn_type = self.conn_type(); + raii::AuthedConnID::insert_session(self.session_key(), session); } else { - SESSIONS.lock().unwrap().insert( - self.lr.my_id.clone(), + raii::AuthedConnID::insert_session( + self.session_key(), 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, + conn_type: self.conn_type(), + conn_id: self.inner.id(), }, ); } @@ -2159,12 +2164,8 @@ impl Connection { _ => {} } if let Some(job_id) = job_id { - self.send(fs::new_error( - job_id, - "one-way-file-transfer-tip", - 0, - )) - .await; + self.send(fs::new_error(job_id, "one-way-file-transfer-tip", 0)) + .await; return true; } } @@ -2399,7 +2400,10 @@ impl Connection { } Some(misc::Union::CloseReason(_)) => { self.on_close("Peer close", true).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::remove_session_if_last_duplication( + self.inner.id(), + self.session_key(), + ); return false; } @@ -3159,7 +3163,7 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - SESSIONS.lock().unwrap().remove(&self.lr.my_id); + raii::AuthedConnID::remove_session_if_last_duplication(self.inner.id(), self.session_key()); } fn read_dir(&mut self, dir: &str, include_hidden: bool) { @@ -3313,6 +3317,26 @@ impl Connection { } } } + + #[inline] + fn conn_type(&self) -> AuthConnType { + if self.file_transfer.is_some() { + AuthConnType::FileTransfer + } else if self.port_forward_socket.is_some() { + AuthConnType::PortForward + } else { + AuthConnType::Remote + } + } + + #[inline] + fn session_key(&self) -> SessionKey { + SessionKey { + peer_id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + session_id: self.lr.session_id, + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3810,15 +3834,18 @@ mod raii { pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { - pub fn new(id: i32, conn_type: AuthConnType) -> Self { - AUTHED_CONNS.lock().unwrap().push((id, conn_type)); + pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { + AUTHED_CONNS + .lock() + .unwrap() + .push((conn_id, conn_type, session_key)); 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) + Self(conn_id, conn_type) } fn check_wake_lock() { @@ -3843,6 +3870,53 @@ mod raii { .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) .count() } + + pub fn remove_session_if_last_duplication(conn_id: i32, key: SessionKey) { + let contains = SESSIONS.lock().unwrap().contains_key(&key); + if contains { + let another = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .any(|c| c.0 != conn_id && c.2 == key && c.1 != AuthConnType::PortForward); + if !another { + // Keep the session if there is another connection with same peer_id and session_id. + SESSIONS.lock().unwrap().remove(&key); + log::info!("remove session"); + } else { + log::info!("skip remove session"); + } + } + } + + pub fn insert_session(key: SessionKey, session: Session) { + let mut insert = true; + if session.conn_type == AuthConnType::PortForward { + // port forward doesn't update last received time + let other_alive_conns = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| { + c.2 == key && c.1 != AuthConnType::PortForward // port forward doesn't remove itself + }) + .map(|c| c.0) + .collect::>(); + let another = SESSIONS.lock().unwrap().get(&key).map(|s| { + other_alive_conns.contains(&s.conn_id) + && s.tfa == session.tfa + && s.conn_type != AuthConnType::PortForward + }) == Some(true); + if another { + insert = false; + log::info!("skip insert session for port forward"); + } + } + if insert { + log::info!("insert session for {:?}", session.conn_type); + SESSIONS.lock().unwrap().insert(key, session); + } + } } impl Drop for AuthedConnID { @@ -3850,7 +3924,7 @@ mod raii { 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); + AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f0829e75e..0296d82bd 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -506,7 +506,7 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let force_relay = args.contains(&"--relay".to_string()); - let mut session: Session = Session { + let session: Session = Session { password: password.clone(), args, server_keyboard_enabled: Arc::new(RwLock::new(true)), @@ -529,7 +529,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, force_relay, None, None); + .initialize(id, conn_type, None, force_relay, None, None, None); Self(session) } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4160561be..321707d3f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1490,6 +1490,10 @@ impl Session { msg.set_misc(misc); self.send(Data::Message(msg)); } + + pub fn get_conn_token(&self) -> Option { + self.lc.read().unwrap().get_conn_token() + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {