use std::io::{BufRead, BufReader, Write, stdout}; use std::net::TcpStream; use rusqlite::Connection; use std::time::Instant; use bcrypt::hash_with_salt; use crate::connection; use crate::database; const SALT: [u8; 16] = *b"fixedsalt1234567"; pub fn hash_password(password: &str) -> String { let result = hash_with_salt(password, 10, SALT).unwrap(); result.to_string() } use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; macro_rules! println { () => { print!("\r\n") }; ($($arg:tt)*) => { print!("\r{}\n", format_args!($($arg)*)) }; } pub struct ClientState { pub stream: Option, pub conn: Connection, pub addr: Option, pub default_port: u16, pub history: Vec, pub history_index: usize, } impl ClientState { pub fn new() -> Self { let conn = database::init_db(); let default_ip = "127.0.0.1".to_string(); let default_port = 11525; let last_host = database::get_last_host(&conn); let default_host = format!("{}:{}", default_ip, default_port); let addr = last_host.or(Some(default_host)); Self { stream: None, conn, addr, default_port, history: vec![], history_index: 0, } } pub fn connect_to(&mut self, addr: &str) { self.addr = Some(addr.to_string()); match connection::connect(addr, &self.conn) { Ok(s) => { self.stream = Some(s); database::save_host(&self.conn, addr); } Err(e) => println!("Failed to connect: {}", e), } } pub fn run(&mut self) { enable_raw_mode().unwrap(); 'outer: loop { let mut current_input = String::new(); self.history_index = self.history.len(); redraw(""); loop { match read().unwrap() { Event::Key(key) => match key.code { KeyCode::Up => { if self.history_index > 0 { self.history_index -= 1; current_input = self.history[self.history_index].clone(); redraw(¤t_input); } } KeyCode::Down => { if self.history_index < self.history.len() { self.history_index += 1; current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() }; redraw(¤t_input); } } KeyCode::Enter => { let line = current_input.trim().to_string(); if !line.is_empty() { self.history.push(line.clone()); } // Process command let parts: Vec<&str> = line.splitn(2, ' ').collect(); match parts[0] { "connect" => { let addr_str = if parts.len() < 2 { if let Some(h) = &self.addr { h.clone() } else { println!("\nNo host specified and no previous connection. Usage: connect :"); continue 'outer; } } else { let mut a = parts[1].to_string(); if !a.contains(':') { a = format!("{}:{}", a, self.default_port); } a }; self.connect_to(&addr_str); } "send" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } if parts.len() < 2 { println!("\nUsage: send "); continue 'outer; } let msg = parts[1]; let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { println!("\nEcho: {}", response.trim()); } } "disconnect" => { self.stream = None; println!("\nDisconnected"); } "ping" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let start = Instant::now(); let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "PING") { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { if response.trim() == "PING" { let elapsed = start.elapsed(); println!("\nPong ({:?})", elapsed); } else { self.stream = None; println!("\nConnection lost"); } } } "register" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } let username = read_line("\nUsername: "); if username.is_empty() { continue 'outer; } let password = read_line("Password: "); if password.is_empty() { continue 'outer; } let hashed = hash_password(&password); let msg = format!("REGISTER {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); continue 'outer; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); } else { println!("\n{}", response.trim()); } } "login" => { if self.stream.is_none() { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } loop { let username = read_line("\nUsername: "); if username.is_empty() { break; } let password = read_line("Password: "); if password.is_empty() { break; } let hashed = hash_password(&password); let msg = format!("AUTH {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); let mut writer = writer_stream; let response_reader_stream = s.try_clone().unwrap(); let mut response_reader = BufReader::new(response_reader_stream); if let Err(_) = writeln!(writer, "{}", msg) { self.stream = None; println!("\nConnection lost"); break; } let mut response = String::new(); if let Err(_) = response_reader.read_line(&mut response) { self.stream = None; println!("\nConnection lost"); break; } else { let resp = response.trim(); println!("\n{}", resp); if resp.starts_with("OK") { // Save credentials if let Some(addr) = &self.addr { if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { println!("Failed to save credentials: {}", e); } } break; } else { println!("Authentication failed, try again or press Enter to cancel."); } } } } "setcred" => { if self.addr.is_none() { println!("\nNo host connected. Use 'connect :' first."); continue 'outer; } let username = read_line("\nUsername: "); if username.is_empty() { continue 'outer; } let password = read_line("Password: "); if password.is_empty() { continue 'outer; } if let Some(addr) = &self.addr { if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { println!("Failed to save credentials: {}", e); } else { println!("Credentials saved for {}", addr); } } } "help" => { help(); } "quit" => break 'outer, _ => help(), } break; } KeyCode::Backspace => { if !current_input.is_empty() { current_input.pop(); redraw(¤t_input); } } KeyCode::Char(c) => { if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); return; } else { current_input.push(c); redraw(¤t_input); } } _ => {} } _ => {} } } } disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); } } fn redraw(current_input: &str) { execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); print!("> {}", current_input); stdout().flush().unwrap(); execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap(); } fn read_line(prompt: &str) -> String { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); print!("{}", prompt); stdout().flush().unwrap(); let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); enable_raw_mode().unwrap(); execute!(stdout(), crossterm::cursor::Hide).unwrap(); input.trim().to_string() } fn help() { println!("Available commands: connect :, send , disconnect, ping, login, register, setcred, help, quit"); }