mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-20 11:23:14 +03:00
client: send the clipboard only to a peer that has accepted the login
The clipboard listener is one per process, started by the first session to log in, and its broadcast went to every session - one whose login was still waiting on a password, 2FA or the peer's consent included. What the user copied meanwhile went to a machine that had not admitted them; the peer drops it unread before authorization, but it is in that peer's hands. A session now records the PeerInfo that answers its login, the moment it is in, and is passed over until then - and again from the first act of every connection round, so a reconnect waiting on its password is passed over too. connection_round_state cannot stand in for that: it says Connected as soon as the transport is up. The unauthenticated cap on the other side is how this came up: a clipboard over 128 KiB ended such a login with "Reset by the peer". The small case had always gone through quietly. A test puts two sessions in the table, one with its PeerInfo in and one without, and broadcasts: the first receives, the second not until its PeerInfo is in too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
This commit is contained in:
@@ -149,6 +149,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) {
|
||||
self.handler.logged_in.store(false, Ordering::SeqCst);
|
||||
#[cfg(target_os = "windows")]
|
||||
let _file_clip_context_holder = {
|
||||
// `is_port_forward()` will not reach here, but we still check it for clarity.
|
||||
@@ -1450,6 +1451,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.handler.logged_in.store(true, Ordering::SeqCst);
|
||||
self.handler.handle_peer_info(pi);
|
||||
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
||||
self.check_clipboard_file_context();
|
||||
|
||||
@@ -1438,6 +1438,10 @@ fn send_clipboard_msg_impl(msg: Message, _is_file: bool, except_session_id: Opti
|
||||
if !s.is_default() {
|
||||
continue;
|
||||
}
|
||||
// Nothing goes to a peer that has not accepted the login yet.
|
||||
if !s.logged_in.load(Ordering::SeqCst) {
|
||||
continue;
|
||||
}
|
||||
if let Some(except_session_id) = except_session_id {
|
||||
if s.lc.read().unwrap().session_id == except_session_id {
|
||||
continue;
|
||||
@@ -2381,3 +2385,64 @@ pub(super) mod async_tasks {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hbb_common::tokio::sync::mpsc;
|
||||
|
||||
/// A default-connection session as `session_add` builds it, with the io loop's channel in
|
||||
/// place so that what the session sends can be read back.
|
||||
fn session(id: &str) -> (SessionID, FlutterSession, mpsc::UnboundedReceiver<Data>) {
|
||||
let session: Session<FlutterHandler> = Session {
|
||||
server_keyboard_enabled: Arc::new(RwLock::new(true)),
|
||||
server_clipboard_enabled: Arc::new(RwLock::new(true)),
|
||||
..Default::default()
|
||||
};
|
||||
session.lc.write().unwrap().initialize(
|
||||
id.to_owned(),
|
||||
ConnType::DEFAULT_CONN,
|
||||
None,
|
||||
false,
|
||||
get_adapter_luid(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
*session.sender.write().unwrap() = Some(tx);
|
||||
let session = Arc::new(session);
|
||||
let session_id = SessionID::new_v4();
|
||||
sessions::insert_session(session_id, ConnType::DEFAULT_CONN, session.clone());
|
||||
(session_id, session, rx)
|
||||
}
|
||||
|
||||
// The clipboard listener is shared by every session and starts with the first login, so a
|
||||
// change reaches sessions still waiting on a password or the peer's consent: those get
|
||||
// nothing until their PeerInfo is in.
|
||||
#[test]
|
||||
fn clipboard_goes_only_to_a_peer_that_accepted_the_login() {
|
||||
let (accepted_id, accepted, mut accepted_rx) = session("clipboard-gate-accepted");
|
||||
let (pending_id, pending, mut pending_rx) = session("clipboard-gate-pending");
|
||||
accepted.logged_in.store(true, Ordering::SeqCst);
|
||||
|
||||
let mut msg = Message::new();
|
||||
msg.set_clipboard(Clipboard {
|
||||
content: b"copied while one login is still pending".to_vec().into(),
|
||||
..Default::default()
|
||||
});
|
||||
send_clipboard_msg_impl(msg.clone(), false, None);
|
||||
assert!(matches!(accepted_rx.try_recv(), Ok(Data::Message(_))));
|
||||
assert!(
|
||||
pending_rx.try_recv().is_err(),
|
||||
"a login the peer has not accepted was sent the clipboard"
|
||||
);
|
||||
|
||||
pending.logged_in.store(true, Ordering::SeqCst);
|
||||
send_clipboard_msg_impl(msg, false, None);
|
||||
assert!(matches!(pending_rx.try_recv(), Ok(Data::Message(_))));
|
||||
|
||||
sessions::remove_session_by_session_id(&accepted_id);
|
||||
sessions::remove_session_by_session_id(&pending_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::SystemTime,
|
||||
@@ -66,6 +66,9 @@ pub struct Session<T: InvokeUiSession> {
|
||||
pub server_clipboard_enabled: Arc<RwLock<bool>>,
|
||||
pub last_change_display: Arc<Mutex<ChangeDisplayRecord>>,
|
||||
pub connection_round_state: Arc<Mutex<ConnectionRoundState>>,
|
||||
// The peer's PeerInfo is in. `connection_round_state` is Connected once the transport is,
|
||||
// while the login it carries may still be waiting on a password or the peer's consent.
|
||||
pub logged_in: Arc<AtomicBool>,
|
||||
pub printer_names: Arc<RwLock<HashMap<i32, String>>>,
|
||||
// Indicate whether the session is reconnected.
|
||||
// Used to auto start file transfer after reconnection.
|
||||
|
||||
Reference in New Issue
Block a user