use crate::tls::TlsConnector; use parking_lot::Mutex; use rusqlite::Connection; use std::io::{Read, Write}; use std::net::TcpStream; use std::sync::mpsc; use std::thread; use std::time::{Duration, Instant}; use zagan_bet::client::database; use zagan_bet::client::ui::first_contact_lines; static UNSAFE_MODE: Mutex = Mutex::new(false); pub fn set_unsafe_mode(unsafe_mode: bool) { *UNSAFE_MODE.lock() = unsafe_mode; } type ConnectResult = Result<(TlsStream, Option), Box>; pub fn connect( addr: &str, conn: &Connection, ) -> Result<(TlsStream, Option), Box> { let stored_fingerprint = database::get_fingerprint(conn, addr); let unsafe_mode = *UNSAFE_MODE.lock(); let (tx, rx) = mpsc::channel::(); let addr_str = addr.to_string(); let server_name = addr_str .split(':') .next() .unwrap_or("127.0.0.1") .to_string(); thread::spawn(move || { let outcome = (|| -> ConnectResult { let tcp = TcpStream::connect(&addr_str)?; let connector = TlsConnector::new(&server_name, stored_fingerprint.clone(), unsafe_mode)?; let tls_stream = connector.connect(tcp)?; let fp = connector.observed_fingerprint(); Ok((TlsStream { inner: tls_stream }, fp)) })(); let _ = tx.send(outcome); }); // Channel carries (stream, observed fingerprint). let (tls, observed) = match rx.recv_timeout(Duration::from_secs(10)) { Ok(Ok((tls, fp))) => (tls, fp), Ok(Err(e)) => return Err(Box::new(TrustError(format!("{e}")))), Err(mpsc::RecvTimeoutError::Timeout) => { return Err("Connection timed out after 3 seconds".into()); } Err(_) => return Err("Connection attempt failed".into()), }; // TOFU: persist the fingerprint on first contact. if !unsafe_mode { if let Some(fp) = observed { if database::get_fingerprint(conn, addr).is_none() { database::save_fingerprint(conn, addr, &fp)?; for line in first_contact_lines(addr, &fp) { println!("{}", line); } } } } let mut tls = tls; tls.set_read_timeout(Some(Duration::from_secs(10)))?; let start = Instant::now(); tls.write_line("PING")?; let mut response = String::new(); tls.read_line(&mut response)?; let elapsed = start.elapsed(); println!( "\n\rConnected to {} with ping delay: {:.1}ms (TLS)", addr, elapsed.as_secs_f64() * 1000.0 ); if let Some((username, password)) = database::get_credentials(conn, addr) { let auth_msg = format!("AUTH {} {}", username, password); tls.write_line(&auth_msg)?; let mut auth_response = String::new(); tls.read_line(&mut auth_response)?; if auth_response.trim().starts_with("OK") { println!("Authenticated as {}", username); Ok((tls, Some(username))) } else { println!("Authentication failed: {}", auth_response.trim()); Ok((tls, None)) } } else { Ok((tls, None)) } } /// Wrapper so TLS handshake failures surface as errors with their message /// intact (rustls errors already carry the mismatch details). struct TrustError(String); impl std::fmt::Display for TrustError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl std::fmt::Debug for TrustError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self}") } } impl std::error::Error for TrustError {} pub struct TlsStream { inner: crate::tls::TlsStream, } impl TlsStream { pub fn read_line(&mut self, buf: &mut String) -> std::io::Result { self.inner.read_line(buf) } pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { self.inner.write_line(msg) } /// One line if the socket already has one, without blocking. pub fn read_available_line(&mut self) -> std::io::Result> { self.inner.read_available_line() } pub fn set_read_timeout(&self, duration: Option) -> std::io::Result<()> { self.inner.set_read_timeout(duration) } } impl Read for TlsStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.inner.read(buf) } } impl Write for TlsStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.inner.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.inner.flush() } }