diff --git a/src/client.rs b/src/client.rs index d77359ef2..4ea23440c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1752,6 +1752,14 @@ pub struct LoginConfigHandler { password: Vec, // remember password for reconnect pub remember: bool, config: PeerConfig, + pub port_forward: (String, i32), + /// Set once per window, before its mappings start: the claim and the + /// login of every accept must agree on it. + pub(crate) port_forward_mux: bool, + /// Held by a port-forward mapping from filling `port_forward` and `hash` + /// until its login is built from them; a window's mappings log in + /// concurrently. + pub(crate) port_forward_login_turn: Arc>, pub version: i64, features: Option, pub session_id: u64, // used for local <-> server communication @@ -2665,7 +2673,6 @@ impl LoginConfigHandler { os_username: String, os_password: String, password: Vec, - port_forward: PortForward, ) -> Message { let my_id = Config::get_id(); let (my_id, pure_id) = if let Some((id, _, _)) = self.other_server.as_ref() { @@ -2762,7 +2769,12 @@ impl LoginConfigHandler { ..Default::default() }), ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()), - ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(port_forward), + ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward { + host: self.port_forward.0.clone(), + port: self.port_forward.1, + multiplex: self.port_forward_mux, + ..Default::default() + }), ConnType::TERMINAL => { let mut terminal = Terminal::new(); terminal.service_id = self.get_option(self.get_key_terminal_service_id()); @@ -3496,7 +3508,6 @@ pub async fn handle_hash( lc: Arc>, password_preset: &str, hash: Hash, - port_forward: PortForward, interface: &impl Interface, peer: &mut Stream, ) -> bool { @@ -3616,7 +3627,7 @@ pub async fn handle_hash( hasher.finalize()[..].into() }; - send_login(lc.clone(), String::new(), String::new(), password, port_forward, peer).await; + send_login(lc.clone(), String::new(), String::new(), password, peer).await; lc.write().unwrap().hash = hash; true } @@ -3654,20 +3665,18 @@ fn try_get_password_from_personal_ab(lc: Arc>, passwo /// * `os_username` - OS username. /// * `os_password` - OS password. /// * `password` - Password. -/// * `port_forward` - Target of a port-forward login; ignored by other types. /// * `peer` - [`Stream`] for communicating with peer. async fn send_login( lc: Arc>, os_username: String, os_password: String, password: Vec, - port_forward: PortForward, peer: &mut Stream, ) { let msg_out = lc .read() .unwrap() - .create_login_msg(os_username, os_password, password, port_forward); + .create_login_msg(os_username, os_password, password); allow_err!(peer.send(&msg_out).await); } @@ -3680,8 +3689,6 @@ async fn send_login( /// * `os_password` - OS password. /// * `password` - Password. /// * `remember` - Whether to remember password. -/// * `port_forward` - Target of a port-forward login; ignored by other types. -/// * `hash` - The challenge this connection was given, if it has one yet. /// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, @@ -3689,20 +3696,8 @@ pub async fn handle_login_from_ui( os_password: String, password: String, remember: bool, - port_forward: PortForward, - hash: Option, peer: &mut Stream, ) { - // The window's password prompt is broadcast to every port-forward - // mapping, and can reach one whose own `Hash` has not arrived. It has - // nothing to answer with: a digest over an empty challenge is refused - // and counted as a failed attempt. The mapping that prompted stores the - // salted password in the shared handler, and `handle_hash` logs this one - // in with it when its `Hash` comes. - let Some(hash) = hash else { - log::info!("login from UI before this connection's hash, waiting for it"); - return; - }; let mut hash_password = if password.is_empty() { let mut password2 = lc.read().unwrap().password.clone(); if password2.is_empty() { @@ -3716,7 +3711,7 @@ pub async fn handle_login_from_ui( lc.write().unwrap().password_source = Default::default(); let mut hasher = Sha256::new(); hasher.update(password); - hasher.update(&hash.salt); + hasher.update(&lc.read().unwrap().hash.salt); let res = hasher.finalize(); lc.write().unwrap().remember = remember; res[..].into() @@ -3724,10 +3719,10 @@ pub async fn handle_login_from_ui( lc.write().unwrap().password = hash_password.clone(); let mut hasher2 = Sha256::new(); hasher2.update(&hash_password[..]); - hasher2.update(&hash.challenge); + hasher2.update(&lc.read().unwrap().hash.challenge); hash_password = hasher2.finalize()[..].to_vec(); - send_login(lc.clone(), os_username, os_password, hash_password, port_forward, peer).await; + send_login(lc.clone(), os_username, os_password, hash_password, peer).await; } async fn send_switch_login_request( @@ -3741,7 +3736,7 @@ async fn send_switch_login_request( lr: hbb_common::protobuf::MessageField::some( lc.read() .unwrap() - .create_login_msg("".to_owned(), "".to_owned(), vec![], Default::default()) + .create_login_msg("".to_owned(), "".to_owned(), vec![]) .login_request() .to_owned(), ), @@ -3774,11 +3769,6 @@ pub trait Interface: Send + Clone + 'static + Sized { async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream); fn get_lch(&self) -> Arc>; - /// A clone whose port-forward login asks for this target. The target - /// travels with the clone, never through the shared - /// `LoginConfigHandler`, so mappings logging in at the same time cannot - /// overwrite each other's target between the accept and the peer's `Hash`. - fn with_port_forward(&self, port_forward: PortForward) -> Self; fn get_id(&self) -> String { self.get_lch().read().unwrap().id.clone() @@ -4081,207 +4071,22 @@ mod retry_tests { } #[cfg(test)] -mod login_scope_tests { +mod port_forward_mux_tests { use super::*; - use hbb_common::{ - tcp::FramedStream, - tokio::{ - self, - time::{timeout, Duration}, - }, - Stream, - }; - fn target(host: &str, port: i32) -> PortForward { - PortForward { - host: host.to_owned(), - port, - ..Default::default() - } - } - - fn hash(challenge: &str) -> Hash { - Hash { - salt: "salt".to_owned(), - challenge: challenge.to_owned(), - ..Default::default() - } - } - - /// What the peer expects for password `pw` under `hash(challenge)`. - fn digest(challenge: &str) -> Vec { - let mut h = Sha256::new(); - h.update("pw"); - h.update("salt"); - let salted = h.finalize(); - let mut h2 = Sha256::new(); - h2.update(&salted[..]); - h2.update(challenge); - h2.finalize()[..].to_vec() - } - - /// (our end, the peer's end) of one connection. - async fn loopback() -> (Stream, Stream) { - let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = l.local_addr().unwrap(); - let client = tokio::net::TcpStream::connect(addr).await.unwrap(); - let (server, _) = l.accept().await.unwrap(); - ( - Stream::Tcp(FramedStream::from(client, addr)), - Stream::Tcp(FramedStream::from(server, addr)), - ) - } - - async fn next_login(peer: &mut Stream) -> LoginRequest { - let bytes = peer.next().await.unwrap().unwrap(); - Message::parse_from_bytes(&bytes) - .unwrap() - .login_request() - .clone() - } - - /// An `Interface` that records the dialogs it was asked to show. - #[derive(Clone, Default)] - struct NoUi(Arc>>); - - #[async_trait] - impl Interface for NoUi { - fn send(&self, _data: Data) {} - fn msgbox(&self, msgtype: &str, _title: &str, _text: &str, _link: &str) { - self.0.lock().unwrap().push(msgtype.to_owned()); - } - fn handle_login_error(&self, _err: &str) -> bool { - false - } - fn handle_peer_info(&self, _pi: PeerInfo) {} - fn set_multiple_windows_session(&self, _sessions: Vec) {} - async fn handle_hash(&self, _pass: &str, _hash: Hash, _peer: &mut Stream) -> bool { - false - } - async fn handle_login_from_ui( - &self, - _os_username: String, - _os_password: String, - _password: String, - _remember: bool, - _peer: &mut Stream, - ) { - } - async fn handle_test_delay(&self, _t: TestDelay, _peer: &mut Stream) {} - fn get_lch(&self) -> Arc> { - Default::default() - } - fn with_port_forward(&self, _port_forward: PortForward) -> Self { - self.clone() - } - } - - /// The target rides in the call, never in the shared handler: two accepts - /// building their logins off one handler cannot see each other's target. #[test] - fn a_port_forward_login_carries_the_callers_target() { + fn a_multiplexed_window_asks_for_the_tunnel() { let mut lc = LoginConfigHandler::default(); lc.conn_type = ConnType::PORT_FORWARD; - let muxed = |host: &str, port: i32| PortForward { - multiplex: true, - ..target(host, port) + let asks = |lc: &LoginConfigHandler| { + lc.create_login_msg(String::new(), String::new(), vec![]) + .login_request() + .port_forward() + .multiplex }; - let a = lc.create_login_msg(String::new(), String::new(), vec![], muxed("a", 1)); - let b = lc.create_login_msg(String::new(), String::new(), vec![], muxed("b", 2)); - let pf = |m: &Message| m.login_request().port_forward().clone(); - assert_eq!((pf(&a).host.as_str(), pf(&a).port), ("a", 1)); - assert_eq!((pf(&b).host.as_str(), pf(&b).port), ("b", 2)); - assert!(pf(&a).multiplex); - } - - /// Each connection answers its own challenge: the `Hash` is a parameter - /// of the login, not a field two accepts could overwrite in the handler. - #[test] - fn a_ui_login_answers_the_challenge_it_was_given() { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let (mut ours, mut peer) = loopback().await; - let lc = Arc::new(RwLock::new(LoginConfigHandler::default())); - for challenge in ["a", "b"] { - handle_login_from_ui( - lc.clone(), - String::new(), - String::new(), - "pw".to_owned(), - false, - Default::default(), - Some(hash(challenge)), - &mut ours, - ) - .await; - assert_eq!(next_login(&mut peer).await.password, digest(challenge)); - } - }); - } - - /// The window's password prompt is broadcast to every mapping, and it can - /// reach one whose own `Hash` has not arrived. That mapping sends nothing: - /// a digest over an empty challenge would only be refused and counted as - /// a failed attempt. It logs in when its `Hash` comes, with the salted - /// password the mapping that prompted stored in the shared handler, and - /// without prompting again. - #[test] - fn a_mapping_still_waiting_for_its_hash_logs_in_when_it_comes() { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - rt.block_on(async { - let (mut a, mut a_peer) = loopback().await; - let (mut b, mut b_peer) = loopback().await; - let lc = Arc::new(RwLock::new(LoginConfigHandler::default())); - lc.write().unwrap().conn_type = ConnType::PORT_FORWARD; - - // A has its hash and answers the prompt. - handle_login_from_ui( - lc.clone(), - String::new(), - String::new(), - "pw".to_owned(), - false, - target("a", 1), - Some(hash("a")), - &mut a, - ) - .await; - assert_eq!(next_login(&mut a_peer).await.password, digest("a")); - - // The same broadcast reaches B, whose hash is still on its way. - handle_login_from_ui( - lc.clone(), - String::new(), - String::new(), - "pw".to_owned(), - false, - target("b", 2), - None, - &mut b, - ) - .await; - assert!( - timeout(Duration::from_millis(200), b_peer.next()) - .await - .is_err(), - "B logged in before it had a challenge" - ); - - // B's hash arrives. - let ui = NoUi::default(); - assert!(handle_hash(lc.clone(), "", hash("b"), target("b", 2), &ui, &mut b).await); - let login = next_login(&mut b_peer).await; - assert_eq!(login.password, digest("b")); - let pf = login.port_forward(); - assert_eq!((pf.host.as_str(), pf.port), ("b", 2)); - assert!(ui.0.lock().unwrap().is_empty(), "B prompted again"); - }); + assert!(!asks(&lc)); + lc.port_forward_mux = true; + assert!(asks(&lc)); } } diff --git a/src/port_forward.rs b/src/port_forward.rs index b5f8348a8..9f51a7523 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -96,11 +96,11 @@ pub async fn listen( tokio::select! { Ok((forward, addr)) = listener.accept() => { log::info!("new connection from {:?}", addr); - // A multiplexed mapping takes the connection on its tunnel, or - // probes for one on its first accept. Everything else, the - // setting off or a peer without the feature, is the raw pipe - // below, as it always was. - let claim = if mux_enabled() { tunnel.claim() } else { Claim::Legacy }; + // A multiplexed window takes the connection on the mapping's + // tunnel, or probes for one on its first accept. Everything + // else, the setting off or a peer without the feature, is the + // raw pipe below, as it always was. + let claim = if lc.read().unwrap().port_forward_mux { tunnel.claim() } else { Claim::Legacy }; match claim { Claim::Muxed(handle) => { if let Err(e) = handle.open(&remote_host, remote_port, forward, Vec::new()) { @@ -116,14 +116,11 @@ pub async fn listen( } Claim::Legacy => {} } - // The target rides with this accept's interface clone, so two - // mappings logging in at once cannot overwrite each other's. - let interface = interface.with_port_forward(login_target(&remote_host, remote_port, false)); let id = id.clone(); let password = password.clone(); let mut forward = Framed::new(forward, BytesCodec::new()); let mut close_port_forward = false; - match connect_and_login(&id, &password, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward).await { + match connect_and_login(&id, &password, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward, &remote_host, remote_port).await { Ok(Some(stream)) => { let interface = interface.clone(); tokio::spawn(async move { @@ -169,6 +166,8 @@ async fn connect_and_login( token: &str, is_rdp: bool, close_port_forward: &mut bool, + remote_host: &str, + remote_port: i32, ) -> ResultType> { let conn_type = if is_rdp { ConnType::RDP @@ -186,6 +185,8 @@ async fn connect_and_login( } let mut buffer = Vec::new(); let mut received = false; + let mut challenge = None; + let mut pending_login = None; let _keep_it = hc_connection(feedback, rendezvous_server, token).await; @@ -203,7 +204,8 @@ async fn connect_and_login( let msg_in = Message::parse_from_bytes(&bytes)?; match msg_in.union { Some(message::Union::Hash(hash)) => { - if !interface.handle_hash(password, hash, &mut stream).await { + challenge = Some(hash.clone()); + if !hash_arrived(&interface, password, hash, pending_login.take(), remote_host, remote_port, &mut stream).await { return Ok(None); } } @@ -234,9 +236,10 @@ async fn connect_and_login( }, d = ui_receiver.recv() => { match d { - Some(Data::Login((os_username, os_password, password, remember))) => { - interface.handle_login_from_ui(os_username, os_password, password, remember, &mut stream).await; - } + Some(Data::Login(login)) => match &challenge { + Some(hash) => login_from_ui(&interface, hash, login, remote_host, remote_port, &mut stream).await, + None => pending_login = Some(login), + }, Some(Data::Message(msg)) => { allow_err!(stream.send(&msg).await); } @@ -259,6 +262,76 @@ async fn connect_and_login( Ok(Some(stream)) } + +/// A mapping's login is built from the window's shared handler: +/// `create_login_msg` reads `port_forward` and `handle_login_from_ui` reads +/// `hash`. Mappings log in concurrently, so each fills them and sends under +/// the window's turn lock, or one login carried another mapping's target or +/// answered another's challenge. +async fn login_with_hash( + interface: &impl Interface, + password: &str, + hash: Hash, + remote_host: &str, + remote_port: i32, + stream: &mut Stream, +) -> bool { + let lc = interface.get_lch(); + let turn = lc.read().unwrap().port_forward_login_turn.clone(); + let _turn = turn.lock().await; + lc.write().unwrap().port_forward = (remote_host.to_owned(), remote_port); + interface.handle_hash(password, hash, stream).await +} + +type UiLogin = (String, String, String, bool); + +/// This connection's `Hash`. The window's password prompt is broadcast to +/// every mapping and can reach this one first, so a password typed while +/// the `Hash` was on its way is kept and answers it now, rather than being +/// dropped in the hope that the mapping which prompted has already stored +/// it in the shared handler. +async fn hash_arrived( + interface: &impl Interface, + password: &str, + hash: Hash, + pending_login: Option, + remote_host: &str, + remote_port: i32, + stream: &mut Stream, +) -> bool { + match pending_login { + Some(login) => { + login_from_ui(interface, &hash, login, remote_host, remote_port, stream).await; + true + } + None => login_with_hash(interface, password, hash, remote_host, remote_port, stream).await, + } +} + +/// The window's password prompt is broadcast to every mapping; this one +/// answers it with its own challenge. +async fn login_from_ui( + interface: &impl Interface, + hash: &Hash, + login: UiLogin, + remote_host: &str, + remote_port: i32, + stream: &mut Stream, +) { + let lc = interface.get_lch(); + let turn = lc.read().unwrap().port_forward_login_turn.clone(); + let _turn = turn.lock().await; + { + let mut lc = lc.write().unwrap(); + lc.port_forward = (remote_host.to_owned(), remote_port); + lc.set_hash(hash.clone()); + } + let (os_username, os_password, password, remember) = login; + interface + .handle_login_from_ui(os_username, os_password, password, remember, stream) + .await; +} + /// The first accept of a multiplexed mapping. It logs in asking for the /// tunnel, and the peer's answer fixes this listener's mode until it closes: /// a peer with the feature gets a tunnel every later accept joins, one @@ -280,10 +353,9 @@ async fn establish_tunnel( remote_host: &str, remote_port: i32, ) -> bool { - let interface = interface.with_port_forward(login_target(remote_host, remote_port, true)); let mut forward = Framed::new(forward, BytesCodec::new()); let mut close_port_forward = false; - match connect_and_login_mux(id, password, ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward).await { + match connect_and_login_mux(id, password, ui_receiver, interface.clone(), &mut forward, key, token, is_rdp, &mut close_port_forward, remote_host, remote_port).await { Ok(Some(outcome)) if outcome.mux => { let handle = tunnel.set_muxed(outcome.stream, interface.clone()); if !outcome.local_eof { @@ -314,11 +386,12 @@ async fn establish_tunnel( false } -/// `connect_and_login` for a mapping that wants the tunnel: the login asks -/// for it, the pre-read stops at one window rather than growing without -/// bound, and a local EOF no longer ends the login, since the tunnel may -/// still be wanted. It reports what the peer answered rather than a raw -/// stream, because the caller's next step depends on it. +/// `connect_and_login` for a mapping that wants the tunnel: the pre-read +/// stops at one window rather than growing without bound, and a local EOF +/// no longer ends the login, since the tunnel may still be wanted. It +/// reports what the peer answered rather than a raw stream, because the +/// caller's next step depends on it. The login itself is the raw pipe's: +/// the window's `port_forward_mux` is what makes it ask for the tunnel. async fn connect_and_login_mux( id: &str, password: &str, @@ -329,6 +402,8 @@ async fn connect_and_login_mux( token: &str, is_rdp: bool, close_port_forward: &mut bool, + remote_host: &str, + remote_port: i32, ) -> ResultType> { let conn_type = if is_rdp { ConnType::RDP @@ -458,18 +533,6 @@ struct LoginOutcome { local_eof: bool, } -/// The target this accept's login asks for. It travels with the interface -/// clone rather than through the shared `LoginConfigHandler`, so mappings -/// logging in at the same time cannot overwrite each other's target. -fn login_target(host: &str, port: i32, multiplex: bool) -> PortForward { - PortForward { - host: host.to_owned(), - port, - multiplex, - ..Default::default() - } -} - fn peer_supports_mux(pi: &PeerInfo) -> bool { pi.features.as_ref().map(|f| f.port_forward_mux).unwrap_or(false) } @@ -482,7 +545,7 @@ fn take_socket(forward: Framed, mut prebuf: Vec) -> ( } /// The controlling side's `enable-port-forward-mux`: on unless set to `N`. -fn mux_enabled() -> bool { +pub fn mux_enabled() -> bool { use hbb_common::config::{keys, option2bool, LocalConfig}; option2bool( keys::OPTION_ENABLE_PORT_FORWARD_MUX, @@ -515,6 +578,175 @@ async fn run_forward(forward: Framed, stream: Stream) -> Ok(()) } +#[cfg(test)] +mod login_tests { + use super::*; + use async_trait::async_trait; + use hbb_common::{ + tcp::FramedStream, + tokio::time::{sleep, Duration}, + }; + use sha2::{Digest, Sha256}; + + /// A window's interface over its shared handler. `handle_hash` can pause + /// before building the login, where the real one looks passwords up. + #[derive(Clone)] + struct Ui { + lc: Arc>, + pause: Duration, + } + + #[async_trait] + impl Interface for Ui { + fn send(&self, _data: Data) {} + fn msgbox(&self, _msgtype: &str, _title: &str, _text: &str, _link: &str) {} + fn handle_login_error(&self, _err: &str) -> bool { + false + } + fn handle_peer_info(&self, _pi: PeerInfo) {} + fn set_multiple_windows_session(&self, _sessions: Vec) {} + async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) -> bool { + sleep(self.pause).await; + crate::client::handle_hash(self.lc.clone(), pass, hash, self, peer).await + } + async fn handle_login_from_ui( + &self, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, + ) { + crate::client::handle_login_from_ui( + self.lc.clone(), + os_username, + os_password, + password, + remember, + peer, + ) + .await + } + async fn handle_test_delay(&self, _t: TestDelay, _peer: &mut Stream) {} + fn get_lch(&self) -> Arc> { + self.lc.clone() + } + } + + fn window() -> Ui { + let mut lc = LoginConfigHandler::default(); + lc.conn_type = ConnType::PORT_FORWARD; + Ui { + lc: Arc::new(RwLock::new(lc)), + pause: Duration::ZERO, + } + } + + /// (our end, the peer's end) of one connection. + async fn loopback() -> (Stream, Stream) { + let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = l.local_addr().unwrap(); + let client = tokio::net::TcpStream::connect(addr).await.unwrap(); + let (server, _) = l.accept().await.unwrap(); + ( + Stream::Tcp(FramedStream::from(client, addr)), + Stream::Tcp(FramedStream::from(server, addr)), + ) + } + + async fn login_at(peer: &mut Stream) -> LoginRequest { + let bytes = peer.next().await.unwrap().unwrap(); + Message::parse_from_bytes(&bytes) + .unwrap() + .login_request() + .clone() + } + + fn target(lr: &LoginRequest) -> (String, i32) { + (lr.port_forward().host.clone(), lr.port_forward().port) + } + + fn hash(challenge: &str) -> Hash { + Hash { + salt: "salt".to_owned(), + challenge: challenge.to_owned(), + ..Default::default() + } + } + + /// What the peer expects for password `pw` under `hash(challenge)`. + fn digest(challenge: &str) -> Vec { + let mut h = Sha256::new(); + h.update("pw"); + h.update("salt"); + let salted = h.finalize(); + let mut h2 = Sha256::new(); + h2.update(&salted[..]); + h2.update(challenge); + h2.finalize()[..].to_vec() + } + + #[test] + fn mappings_logging_in_at_once_each_carry_their_own_target() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + let mut ui = window(); + ui.pause = Duration::from_millis(50); + let (mut a, mut a_peer) = loopback().await; + let (mut b, mut b_peer) = loopback().await; + tokio::join!( + login_with_hash(&ui, "pw", hash("a"), "a", 1, &mut a), + login_with_hash(&ui, "pw", hash("b"), "b", 2, &mut b), + ); + assert_eq!(target(&login_at(&mut a_peer).await), ("a".to_owned(), 1)); + assert_eq!(target(&login_at(&mut b_peer).await), ("b".to_owned(), 2)); + }); + } + + #[test] + fn a_mapping_answers_the_prompt_with_its_own_challenge() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + let ui = window(); + let (mut a, mut a_peer) = loopback().await; + let (mut b, mut b_peer) = loopback().await; + // A's hash arrived last, so it is the one the handler holds. + assert!(login_with_hash(&ui, "pw", hash("a"), "a", 1, &mut a).await); + login_at(&mut a_peer).await; + let typed = (String::new(), String::new(), "pw".to_owned(), false); + login_from_ui(&ui, &hash("b"), typed, "b", 2, &mut b).await; + let lr = login_at(&mut b_peer).await; + assert_eq!(lr.password, digest("b")); + assert_eq!(target(&lr), ("b".to_owned(), 2)); + }); + } + + #[test] + fn a_password_typed_before_this_connections_hash_answers_it_when_it_comes() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + let ui = window(); + let (mut b, mut b_peer) = loopback().await; + // The prompt's password reached B before its hash, and no other + // mapping has stored it in the handler yet. + let typed = (String::new(), String::new(), "pw".to_owned(), false); + assert!(hash_arrived(&ui, "", hash("b"), Some(typed), "b", 2, &mut b).await); + let lr = login_at(&mut b_peer).await; + assert_eq!(lr.password, digest("b")); + assert_eq!(target(&lr), ("b".to_owned(), 2)); + }); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/port_forward_mux.rs b/src/port_forward_mux.rs index 63e060bf6..1f5319943 100644 --- a/src/port_forward_mux.rs +++ b/src/port_forward_mux.rs @@ -1355,9 +1355,6 @@ mod tests { fn handle_login_error(&self, _err: &str) -> bool { false } - fn with_port_forward(&self, _port_forward: PortForward) -> Self { - self.clone() - } fn handle_peer_info(&self, _pi: PeerInfo) {} fn set_multiple_windows_session(&self, _sessions: Vec) {} async fn handle_hash(&self, _pass: &str, _hash: Hash, _peer: &mut Stream) -> bool { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 38d213fda..e7c8e7516 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -57,13 +57,6 @@ const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15; pub struct Session { pub password: String, pub args: Vec, - /// Per clone, set by `Interface::with_port_forward`: the target a - /// port-forward login asks for. - pub port_forward: PortForward, - /// The `Hash` this connection was challenged with. A session's clones - /// share it, as they share the connection; a port-forward accept's clone - /// gets its own, since every accept is a connection of its own. - pub login_hash: Arc>>, pub lc: Arc>, pub sender: Arc>>>, pub thread: Arc>>>, @@ -1875,16 +1868,8 @@ impl Interface for Session { } } - fn with_port_forward(&self, port_forward: PortForward) -> Self { - let mut scoped = self.clone(); - scoped.port_forward = port_forward; - scoped.login_hash = Default::default(); - scoped - } - async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) -> bool { - *self.login_hash.write().unwrap() = Some(hash.clone()); - handle_hash(self.lc.clone(), pass, hash, self.port_forward.clone(), self, peer).await + handle_hash(self.lc.clone(), pass, hash, self, peer).await } async fn handle_login_from_ui( @@ -1895,15 +1880,12 @@ impl Interface for Session { remember: bool, peer: &mut Stream, ) { - let hash = self.login_hash.read().unwrap().clone(); handle_login_from_ui( self.lc.clone(), os_username, os_password, password, remember, - self.port_forward.clone(), - hash, peer, ) .await; @@ -1962,6 +1944,7 @@ pub async fn io_loop(handler: Session, round: u32) { let key = crate::get_key(false).await; #[cfg(not(any(target_os = "android", target_os = "ios")))] if handler.is_port_forward() { + handler.lc.write().unwrap().port_forward_mux = crate::port_forward::mux_enabled(); if handler.is_rdp() { let port = handler .get_option("rdp_port".to_owned())