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; use zagan_bet::version::{is_newer, parse_hello, VERSION}; 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 10 seconds".into()); } Err(_) => return Err("Connection attempt failed".into()), }; // TOFU: persist the fingerprint on first contact. if !unsafe_mode { if let Some(fp) = observed.as_deref() { 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)))?; // Version lockstep: the first line after TLS is the handshake, and no // credentials are sent until the server accepts it. let start = Instant::now(); tls.write_line(&format!("HELLO {VERSION}"))?; let mut response = String::new(); if tls.read_line(&mut response)? == 0 { return Err("server closed the connection during HELLO".into()); } let elapsed = start.elapsed(); let response = response.trim(); if let Some(message) = response.strip_prefix("ERROR") { return Err(message.trim().to_string().into()); } let Some(hello) = parse_hello(response) else { return Err(format!("unexpected HELLO reply: {response}").into()); }; println!( "\n\rConnected to {} with ping delay: {:.1}ms (TLS)", addr, elapsed.as_secs_f64() * 1000.0 ); if is_newer(&hello.latest, VERSION) { println!( "A newer version is available: {} ({})", hello.latest, hello.url ); } // Credentials are stored per server identity, so any address presenting // this certificate key can log in with the same stored account. let identity = observed .clone() .or_else(|| database::get_fingerprint(conn, addr)); if let Some(identity) = identity { if let Some((username, password)) = database::get_credentials(conn, &identity) { 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); return Ok((tls, Some(username))); } println!("Authentication failed: {}", auth_response.trim()); } } 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() } }