diff --git a/src/client.rs b/src/client.rs index 73cf466eb..5b059a7cc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1753,6 +1753,10 @@ pub struct LoginConfigHandler { pub remember: bool, config: PeerConfig, pub port_forward: (String, i32), + /// 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 @@ -1792,6 +1796,10 @@ impl Deref for LoginConfigHandler { } impl LoginConfigHandler { + pub(crate) fn set_hash(&mut self, hash: Hash) { + self.hash = hash; + } + /// Initialize the login config handler. /// /// # Arguments diff --git a/src/port_forward.rs b/src/port_forward.rs index 392ed3c67..0cc640663 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -92,12 +92,11 @@ pub async fn listen( tokio::select! { Ok((forward, addr)) = listener.accept() => { log::info!("new connection from {:?}", addr); - lc.write().unwrap().port_forward = (remote_host.clone(), remote_port); 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 { @@ -143,6 +142,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 @@ -160,6 +161,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; @@ -177,7 +180,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); } } @@ -208,9 +212,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); } @@ -233,6 +238,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; +} + async fn run_forward(forward: Framed, stream: Stream) -> ResultType<()> { log::info!("new port forwarding connection started"); let mut forward = forward; @@ -257,3 +332,172 @@ 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)); + }); + } +}