From 5d6b5049c2a34a3f8abe725d4a871ba84a0dd714 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 24 Aug 2026 14:48:12 +0200 Subject: TLS transport and games framework on client and server --- src/bin/client/connection.rs | 136 +++++- src/bin/client/database.rs | 26 +- src/bin/client/games/manager.rs | 127 ++++++ src/bin/client/games/mod.rs | 3 + src/bin/client/games/slots.rs | 62 +++ src/bin/client/games/traits.rs | 30 ++ src/bin/client/macros.rs | 2 +- src/bin/client/main.rs | 14 +- src/bin/client/mod.rs | 3 +- src/bin/client/repl.rs | 990 +++++++++++++++++++++++++++------------- src/bin/client/tls.rs | 150 ++++++ 11 files changed, 1191 insertions(+), 352 deletions(-) create mode 100644 src/bin/client/games/manager.rs create mode 100644 src/bin/client/games/mod.rs create mode 100644 src/bin/client/games/slots.rs create mode 100644 src/bin/client/games/traits.rs create mode 100644 src/bin/client/tls.rs (limited to 'src/bin/client') diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs index b07bf01..c5d506d 100644 --- a/src/bin/client/connection.rs +++ b/src/bin/client/connection.rs @@ -1,46 +1,76 @@ -use std::io::{BufRead, BufReader, Write}; +use crate::database; +use native_tls::TlsConnector; +use rusqlite::Connection; +use std::io::{Read, Write}; use std::net::TcpStream; use std::sync::mpsc; +use std::sync::Arc; +use std::sync::Mutex; use std::thread; use std::time::{Duration, Instant}; -use rusqlite::Connection; -use crate::database; -pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option), Box> { +static UNSAFE_MODE: Mutex = Mutex::new(false); + +pub fn set_unsafe_mode(unsafe_mode: bool) { + let mut mode = UNSAFE_MODE.lock().unwrap(); + *mode = unsafe_mode; +} + +pub fn connect( + addr: &str, + conn: &Connection, +) -> Result<(TlsStream, Option), Box> { let (tx, rx) = mpsc::channel(); let addr_str = addr.to_string(); - thread::spawn(move || { - match TcpStream::connect(&addr_str) { - Ok(s) => tx.send(Ok(s)).unwrap(), - Err(e) => tx.send(Err(e)).unwrap(), - } + let server_name = addr_str + .split(':') + .next() + .unwrap_or("127.0.0.1") + .to_string(); + thread::spawn(move || match TcpStream::connect(&addr_str) { + Ok(s) => tx.send(Ok(s)).unwrap(), + Err(e) => tx.send(Err(e)).unwrap(), }); match rx.recv_timeout(Duration::from_secs(3)) { Ok(Ok(s)) => { - let mut ping_writer = s.try_clone().unwrap(); - let mut ping_reader = BufReader::new(s.try_clone().unwrap()); + let unsafe_mode = *UNSAFE_MODE.lock().unwrap(); + + let connector = if unsafe_mode { + TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build()? + } else { + TlsConnector::builder().build()? + }; + + let tls_stream = connector.connect(&server_name, s)?; + let mut tls = TlsStream::new(tls_stream); + let start = Instant::now(); - writeln!(ping_writer, "PING").unwrap(); + tls.write_line("PING")?; let mut response = String::new(); - ping_reader.read_line(&mut response).unwrap(); + tls.read_line(&mut response)?; let elapsed = start.elapsed(); - println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed); - - // Check for credentials and auto-auth + println!( + "\n\rConnected to {} with ping delay: {:?} (TLS)", + addr, elapsed + ); + if let Some((username, password)) = database::get_credentials(conn, addr) { let auth_msg = format!("AUTH {} {}", username, password); - writeln!(ping_writer, "{}", auth_msg).unwrap(); + tls.write_line(&auth_msg)?; let mut auth_response = String::new(); - ping_reader.read_line(&mut auth_response).unwrap(); + tls.read_line(&mut auth_response)?; if auth_response.trim().starts_with("OK") { println!("Authenticated as {}", username); - Ok((s, Some(username))) + Ok((tls, Some(username))) } else { println!("Authentication failed: {}", auth_response.trim()); - Ok((s, None)) + Ok((tls, None)) } } else { - Ok((s, None)) + Ok((tls, None)) } } Ok(Err(e)) => Err(Box::new(e)), @@ -48,3 +78,67 @@ pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option Err("Connection attempt failed".into()), } } + +pub struct TlsStream { + stream: Arc>>, +} + +unsafe impl Send for TlsStream {} +unsafe impl Sync for TlsStream {} + +impl TlsStream { + pub fn new(stream: native_tls::TlsStream) -> Self { + Self { + stream: Arc::new(Mutex::new(stream)), + } + } + + pub fn read_line(&mut self, buf: &mut String) -> std::io::Result { + let mut byte_buf = [0u8; 1024]; + let mut stream = self.stream.lock().unwrap(); + loop { + let n = stream.read(&mut byte_buf)?; + if n == 0 { + return Ok(0); + } + let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); + buf.push_str(chunk); + if buf.contains('\n') { + return Ok(n); + } + } + } + + pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { + let mut stream = self.stream.lock().unwrap(); + stream.write_all(msg.as_bytes())?; + stream.write_all(b"\n")?; + stream.flush()?; + Ok(()) + } + + pub fn try_clone(&self) -> std::io::Result { + Ok(TlsStream { + stream: Arc::clone(&self.stream), + }) + } +} + +impl Read for TlsStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let mut stream = self.stream.lock().unwrap(); + stream.read(buf) + } +} + +impl Write for TlsStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut stream = self.stream.lock().unwrap(); + stream.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + let mut stream = self.stream.lock().unwrap(); + stream.flush() + } +} diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs index da069a7..a274f31 100644 --- a/src/bin/client/database.rs +++ b/src/bin/client/database.rs @@ -8,7 +8,8 @@ pub fn init_db() -> Connection { last_used DATETIME DEFAULT CURRENT_TIMESTAMP )", [], - ).unwrap(); + ) + .unwrap(); conn.execute( "CREATE TABLE IF NOT EXISTS credentials ( host TEXT PRIMARY KEY, @@ -16,12 +17,15 @@ pub fn init_db() -> Connection { password TEXT NOT NULL )", [], - ).unwrap(); + ) + .unwrap(); conn } pub fn get_last_host(conn: &Connection) -> Option { - let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap(); + let mut stmt = conn + .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1") + .unwrap(); let mut rows = stmt.query([]).unwrap(); if let Ok(Some(row)) = rows.next() { row.get(0).ok() @@ -34,10 +38,16 @@ pub fn save_host(conn: &Connection, host: &str) { conn.execute( "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", [host], - ).unwrap(); + ) + .unwrap(); } -pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), Box> { +pub fn save_credentials( + conn: &Connection, + host: &str, + username: &str, + password: &str, +) -> Result<(), Box> { conn.execute( "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", (host, username, password), @@ -46,7 +56,9 @@ pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: } pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { - let mut stmt = conn.prepare("SELECT username, password FROM credentials WHERE host = ?1").unwrap(); + let mut stmt = conn + .prepare("SELECT username, password FROM credentials WHERE host = ?1") + .unwrap(); let mut rows = stmt.query([host]).unwrap(); if let Ok(Some(row)) = rows.next() { let username: String = row.get(0).unwrap(); @@ -55,4 +67,4 @@ pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String) } else { None } -} \ No newline at end of file +} diff --git a/src/bin/client/games/manager.rs b/src/bin/client/games/manager.rs new file mode 100644 index 0000000..6959214 --- /dev/null +++ b/src/bin/client/games/manager.rs @@ -0,0 +1,127 @@ +use crate::games::slots::SlotsGame; +use crate::games::traits::{Game, GameError, GamePhase, GameState}; +use std::collections::HashMap; +use std::io::Write; + +pub struct GameManager { + current_session: Option, + pending_game_type: Option, + games: HashMap>, +} + +impl GameManager { + pub fn new() -> Self { + let mut games: HashMap> = HashMap::new(); + games.insert("slots".to_string(), Box::new(SlotsGame::new())); + + Self { + current_session: None, + pending_game_type: None, + games, + } + } + + pub fn get_available_games(&self) -> Vec<(&str, &str)> { + self.games + .iter() + .map(|(k, v)| (k.as_str(), v.get_name())) + .collect() + } + + pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> { + if self.current_session.is_some() { + return Err(GameError::InvalidAction); + } + + if !self.games.contains_key(game_type) { + return Err(GameError::InvalidAction); + } + + self.pending_game_type = Some(game_type.to_string()); + + self.current_session = Some(GameState { + game_type: game_type.to_string(), + bet_amount: 10, + phase: GamePhase::WaitingToStart, + data: HashMap::new(), + }); + + Ok(()) + } + + pub fn start_game( + &mut self, + stream: &mut crate::connection::TlsStream, + ) -> Result<(), GameError> { + let game_type = self + .pending_game_type + .take() + .ok_or(GameError::InvalidAction)?; + + let state = self + .current_session + .as_mut() + .ok_or(GameError::InvalidAction)?; + + if state.phase != GamePhase::WaitingToStart { + return Err(GameError::InvalidAction); + } + + let bet_amount = state.bet_amount; + + let writer = stream; + writeln!(writer, "PLAY_START {} {}", game_type, bet_amount) + .map_err(|_| GameError::NetworkError)?; + + Ok(()) + } + + pub fn get_current_game(&self) -> Option<&dyn Game> { + self.current_session + .as_ref() + .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref())) + } + + pub fn get_current_state(&self) -> Option<&GameState> { + self.current_session.as_ref() + } + + pub fn get_current_render_lines(&self) -> Option { + let session = self.current_session.as_ref()?; + let game = self.games.get(&session.game_type)?; + Some(game.render_lines(session)) + } + + pub fn is_in_game(&self) -> bool { + self.current_session.is_some() + } + + pub fn is_game_started(&self) -> bool { + self.current_session + .as_ref() + .map(|s| s.phase != GamePhase::WaitingToStart) + .unwrap_or(false) + } + + pub fn end_game(&mut self) { + self.current_session = None; + self.pending_game_type = None; + } + + pub fn adjust_bet(&mut self, delta: i64) -> Result { + if let Some(ref session) = self.current_session { + if session.phase != GamePhase::WaitingToStart { + return Err(GameError::InvalidAction); + } + } + + if let Some(ref mut session) = self.current_session { + let current_bet = session.bet_amount as i64; + let new_bet = (current_bet + delta).max(10) as u64; + session.bet_amount = new_bet; + Ok(new_bet) + } else { + Err(GameError::InvalidAction) + } + } +} diff --git a/src/bin/client/games/mod.rs b/src/bin/client/games/mod.rs new file mode 100644 index 0000000..aa74574 --- /dev/null +++ b/src/bin/client/games/mod.rs @@ -0,0 +1,3 @@ +pub mod manager; +pub mod slots; +pub mod traits; diff --git a/src/bin/client/games/slots.rs b/src/bin/client/games/slots.rs new file mode 100644 index 0000000..2f9d63e --- /dev/null +++ b/src/bin/client/games/slots.rs @@ -0,0 +1,62 @@ +use crate::games::traits::{Game, GameState}; + +pub struct SlotsGame; + +impl SlotsGame { + pub fn new() -> Self { + Self + } + + fn render_reels(&self, reels: &str) -> String { + if reels.is_empty() { + "[ ? ] [ ? ] [ ? ]".to_string() + } else { + let chars: Vec = reels.chars().collect(); + format!( + "[ {} ] [ {} ] [ {} ]\r", + chars.get(0).unwrap_or(&'?'), + chars.get(1).unwrap_or(&'?'), + chars.get(2).unwrap_or(&'?') + ) + } + } +} + +impl Game for SlotsGame { + fn get_name(&self) -> &'static str { + "Slots" + } + + fn render(&self, state: &GameState) -> String { + let reels = state.data.get("reels").cloned().unwrap_or_default(); + + let mut output = format!("\n=== SLOTS ===\r\n"); + output += &format!("Bet: {} PLN\r\n", state.bet_amount); + output += &format!("{}\r\n", self.render_reels(&reels)); + output += "\nPress [SPACE] to spin!\r\n"; + output += "\nControls: [+/-] Adjust Bet [Q] Quit\r\n"; + output + } + + fn render_lines(&self, state: &GameState) -> usize { + self.render(state).lines().count() + } + + fn get_help(&self) -> String { + "SLOTS GAME\n\ + =========\n\ + Match symbols to win!\n\ + \n\ + Payouts:\n\ + Three of a kind: 5x-100x bet\n\ + Two of a kind: 2x-20x bet\n\ + \n\ + Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ + \n\ + Controls:\n\ + SPACE - Spin the reels\n\ + +/- - Adjust bet amount\n\ + Q - Exit the game" + .to_string() + } +} diff --git a/src/bin/client/games/traits.rs b/src/bin/client/games/traits.rs new file mode 100644 index 0000000..19f2638 --- /dev/null +++ b/src/bin/client/games/traits.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub enum GameError { + InvalidAction, + NetworkError, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum GamePhase { + WaitingToStart, +} + +#[derive(Debug, Clone)] +pub struct GameState { + pub game_type: String, + pub bet_amount: u64, + pub phase: GamePhase, + pub data: HashMap, +} + +pub trait Game: Send + Sync { + fn get_name(&self) -> &'static str; + + fn render(&self, state: &GameState) -> String; + + fn render_lines(&self, state: &GameState) -> usize; + + fn get_help(&self) -> String; +} diff --git a/src/bin/client/macros.rs b/src/bin/client/macros.rs index e45f2b3..cda73e3 100644 --- a/src/bin/client/macros.rs +++ b/src/bin/client/macros.rs @@ -14,4 +14,4 @@ macro_rules! printsame { ($($arg:tt)*) => { print!("\r{}", format_args!($($arg)*)) }; -} \ No newline at end of file +} diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs index 2ff6f55..c95880d 100644 --- a/src/bin/client/main.rs +++ b/src/bin/client/main.rs @@ -1,12 +1,22 @@ #[macro_use] mod macros; -mod repl; -mod database; mod connection; +mod database; +mod games; +mod repl; use repl::ClientState; fn main() { + let args: Vec = std::env::args().collect(); + let unsafe_mode = args.iter().any(|arg| arg == "--unsafe"); + + if unsafe_mode { + println!("WARNING: Running in UNSAFE mode - certificate verification disabled!"); + } + + connection::set_unsafe_mode(unsafe_mode); + let mut state = ClientState::new(); if let Some(host) = state.addr.clone() { state.connect_to(&host); diff --git a/src/bin/client/mod.rs b/src/bin/client/mod.rs index 938d862..21d7f27 100644 --- a/src/bin/client/mod.rs +++ b/src/bin/client/mod.rs @@ -1,4 +1,5 @@ pub mod database; -pub mod connection; +mod connection; +mod games; pub mod platform; pub mod repl; diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index a0e5b5e..194860b 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs @@ -1,23 +1,32 @@ -use std::io::{BufRead, BufReader, Write, stdout}; -use std::net::TcpStream; use rusqlite::Connection; +use std::io::{stdout, BufRead, BufReader, Write}; +use std::sync::{Arc, Mutex}; use std::time::Instant; -use crate::connection; +use crate::connection::TlsStream; use crate::database; +use crate::games::manager::GameManager; -use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show, Hide}, execute}; +use crossterm::{ + cursor::{Hide, MoveToColumn, Show}, + event::{read, Event, KeyCode, KeyModifiers}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, +}; -use sha2::{Sha256, Digest}; +use sha2::{Digest, Sha256}; pub struct ClientState { - pub stream: Option, + pub stream: Option, pub conn: Connection, pub addr: Option, pub default_port: u16, pub history: Vec, pub history_index: usize, pub logged_in_user: Option, + pub game_manager: GameManager, + cleanup_done: Arc>, + in_game_mode: bool, } impl ClientState { @@ -37,12 +46,86 @@ impl ClientState { history: vec![], history_index: 0, logged_in_user: None, + game_manager: GameManager::new(), + cleanup_done: Arc::new(Mutex::new(false)), + in_game_mode: false, + } + } + + pub fn enter_game_mode(&mut self) { + self.in_game_mode = true; + } + + pub fn exit_game_mode(&mut self) { + self.in_game_mode = false; + self.game_manager.end_game(); + } + + pub fn handle_game_key(&mut self, key_code: KeyCode) -> bool { + if !self.game_manager.is_in_game() { + return false; + } + + match key_code { + KeyCode::Char('q') => { + self.exit_game_mode(); + println!("\nGame exited."); + return true; + } + KeyCode::Char(' ') => { + let should_start = !self.game_manager.is_game_started(); + if should_start { + if let Some(mut stream) = self.stream.take() { + match self.game_manager.start_game(&mut stream) { + Ok(()) => { + self.play_spin_animation(&mut stream); + } + Err(e) => println!("\nError starting game: {:?}", e), + } + self.stream = Some(stream); + } + } + return true; + } + KeyCode::Char('+') => { + // Increase bet + match self.game_manager.adjust_bet(10) { + Ok(_new_bet) => { + if let Some(state) = self.game_manager.get_current_state() { + if let Some(game) = self.game_manager.get_current_game() { + let lines = + self.game_manager.get_current_render_lines().unwrap_or(8); + redraw_game_lines(&game.render(state), lines); + } + } + } + Err(e) => println!("\nError: {:?}", e), + } + return true; + } + KeyCode::Char('-') => { + // Decrease bet + match self.game_manager.adjust_bet(-10) { + Ok(_new_bet) => { + if let Some(state) = self.game_manager.get_current_state() { + if let Some(game) = self.game_manager.get_current_game() { + let lines = + self.game_manager.get_current_render_lines().unwrap_or(8); + redraw_game_lines(&game.render(state), lines); + } + } + } + Err(e) => println!("\nError: {:?}", e), + } + return true; + } + _ => return false, } } pub fn connect_to(&mut self, addr: &str) { self.addr = Some(addr.to_string()); - match connection::connect(addr, &self.conn) { + match crate::connection::connect(addr, &self.conn) { Ok((s, authed_user)) => { self.stream = Some(s); self.logged_in_user = authed_user; @@ -53,6 +136,24 @@ impl ClientState { } pub fn run(&mut self) { + // Set up cleanup handlers + let cleanup_done = self.cleanup_done.clone(); + + // Set up panic handler + let panic_cleanup = cleanup_done.clone(); + std::panic::set_hook(Box::new(move |_| { + restore_terminal(&panic_cleanup); + })); + + // Set up Ctrl+C handler + let ctrlc_cleanup = cleanup_done.clone(); + if let Err(e) = ctrlc::set_handler(move || { + restore_terminal(&ctrlc_cleanup); + std::process::exit(0); + }) { + eprintln!("Warning: Could not set Ctrl+C handler: {}", e); + } + enable_raw_mode().unwrap(); 'outer: loop { let mut current_input = String::new(); @@ -64,308 +165,446 @@ impl ClientState { Event::Key(key) => { // Only handle key press events, not release events if key.kind == crossterm::event::KeyEventKind::Press { - 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); + // Handle game mode keys first + if self.game_manager.is_in_game() && self.handle_game_key(key.code) { + // Game key was handled, continue loop + if !self.game_manager.is_in_game() { + // Game ended, redraw command prompt + redraw(""); } - "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()); + continue; + } + + // Handle normal command mode keys + 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); } } - "disconnect" => { - self.stream = None; - self.logged_in_user = 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() == "PONG" { - let elapsed = start.elapsed(); - println!("\nPong ({:?})", elapsed); + 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.stream = None; - println!("\nConnection lost"); - } + self.history[self.history_index].clone() + }; + redraw(¤t_input); } } - "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; + KeyCode::Enter => { + let line = current_input.trim().to_string(); + if !line.is_empty() { + self.history.push(line.clone()); } - let password = read_password("Password: "); - if password.is_empty() { - continue 'outer; + let line_trimmed = current_input.trim().to_string(); + if !line_trimmed.is_empty() { + self.history.push(line_trimmed.clone()); } - let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); - 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 { - let resp = response.trim(); - if resp == format!("REGISTERED {}", username) { - println!("\nSuccessfully registered as {}", username); - } else if resp.starts_with("ERROR ") { - println!("\nRegistration failed: {}", &resp[6..]); - } else { - println!("\n{}", resp); + + // Process normal commands + let parts: Vec<&str> = line_trimmed.splitn(3, ' ').collect(); + match parts[0] { + "play" => { + if self.stream.is_none() { + println!("\nNot connected. Use 'connect :' first."); + continue 'outer; + } + if self.logged_in_user.is_none() { + println!("\nNot logged in. Use 'login' first."); + continue 'outer; + } + if parts.len() < 2 { + let games = self.game_manager.get_available_games(); + println!("\nAvailable games:"); + for (gt, name) in games { + println!(" - {} ({})", gt, name); + } + println!("\nUsage: play "); + continue 'outer; + } + let game_type = parts[1]; + + if self.game_manager.is_in_game() { + println!("\nAlready in a game. Use 'quit' to exit current game."); + continue 'outer; + } + + match self.game_manager.enter_game(game_type) { + Ok(()) => { + self.enter_game_mode(); + if let Some(game) = + self.game_manager.get_current_game() + { + if let Some(state) = + self.game_manager.get_current_state() + { + redraw_game(&game.render(state)); + } + } + } + Err(e) => { + println!("\nFailed to enter game: {:?}", e); + } + } } - } - } - "login" => { - if self.stream.is_none() { - println!("\nNot connected. Use 'connect :' first."); - continue 'outer; - } - if self.logged_in_user.is_some() { - println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap()); - continue 'outer; - } - loop { - let username = read_line("\nUsername: "); - if username.is_empty() { - break; + "games" => { + let games = self.game_manager.get_available_games(); + println!("\nAvailable games:"); + for (gt, name) in games { + println!(" - {} ({})", gt, name); + } } - let password = read_password("Password: "); - if password.is_empty() { - break; + "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); } - let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); - 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; + "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()); + } } - let mut response = String::new(); - if let Err(_) = response_reader.read_line(&mut response) { + "disconnect" => { self.stream = None; - println!("\nConnection lost"); - break; - } else { - let resp = response.trim(); - if resp.starts_with("OK") { - // Save credentials - if let Some(addr) = &self.addr { - if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { - println!("Failed to save credentials: {}", e); + self.logged_in_user = 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() == "PONG" { + 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_password("Password: "); + if password.is_empty() { + continue 'outer; + } + let hashed = format!( + "{:x}", + Sha256::digest(password.as_bytes()) + ); + 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 { + let resp = response.trim(); + if resp == format!("REGISTERED {}", username) { + println!( + "\nSuccessfully registered as {}", + username + ); + } else if resp.starts_with("ERROR ") { + println!( + "\nRegistration failed: {}", + &resp[6..] + ); + } else { + println!("\n{}", resp); + } + } + } + "login" => { + if self.stream.is_none() { + println!("\nNot connected. Use 'connect :' first."); + continue 'outer; + } + if self.logged_in_user.is_some() { + println!( + "\nAlready logged in as {}", + self.logged_in_user.as_ref().unwrap() + ); + continue 'outer; + } + loop { + let username = read_line("\nUsername: "); + if username.is_empty() { + break; + } + let password = read_password("Password: "); + if password.is_empty() { + break; + } + let hashed = format!( + "{:x}", + Sha256::digest(password.as_bytes()) + ); + 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(); + if resp.starts_with("OK") { + // Save credentials + if let Some(addr) = &self.addr { + if let Err(e) = + database::save_credentials( + &self.conn, addr, &username, + &hashed, + ) + { + println!("Failed to save credentials: {}", e); + } + } + self.logged_in_user = + Some(username.clone()); + println!("\nLogged in as {}", username); + break; + } else if resp.starts_with("ERROR ") { + println!( + "\nAuthentication failed: {}", + &resp[6..] + ); + println!( + "Try again or press Enter to cancel." + ); + } else { + println!("\n{}", resp); } } - self.logged_in_user = Some(username.clone()); - println!("\nLogged in as {}", username); - break; - } else if resp.starts_with("ERROR ") { - println!("\nAuthentication failed: {}", &resp[6..]); - println!("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_password("Password: "); + if password.is_empty() { + continue 'outer; + } + let hashed = format!( + "{:x}", + Sha256::digest(password.as_bytes()) + ); + if let Some(addr) = &self.addr { + if let Err(e) = database::save_credentials( + &self.conn, addr, &username, &hashed, + ) { + println!("Failed to save credentials: {}", e); + } else { + println!("Credentials saved for {}", addr); + } + } + } + "logout" => { + self.logged_in_user = None; + println!("\nLogged out"); + } + "info" => { + 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 response_reader_stream = s.try_clone().unwrap(); + let mut response_reader = + BufReader::new(response_reader_stream); + if let Err(_) = writeln!(writer, "INFO") { + 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{}", resp); + let resp = response.trim(); + if resp.starts_with("INFO ") { + let parts: Vec<&str> = + resp.split_whitespace().collect(); + if parts.len() == 4 { + println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); + } else { + println!("\n{}", resp); + } + } else if resp.starts_with("ERROR ") { + println!("\nError: {}", &resp[6..]); + } else { + println!("\n{}", resp); + } } } - } - } - "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_password("Password: "); - if password.is_empty() { - continue 'outer; - } - let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); - if let Some(addr) = &self.addr { - if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { - println!("Failed to save credentials: {}", e); - } else { - println!("Credentials saved for {}", addr); + "help" => { + if self.game_manager.is_in_game() { + if let Some(game) = + self.game_manager.get_current_game() + { + println!("\n{}", game.get_help()); + } + } else { + help(); + } } + "quit" => { + if self.game_manager.is_in_game() { + self.game_manager.end_game(); + println!("\nGame exited."); + } else { + restore_terminal(&self.cleanup_done); + break 'outer; + } + } + _ => help(), } + break; } - "logout" => { - self.logged_in_user = None; - println!("\nLogged out"); - } - "info" => { - 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 response_reader_stream = s.try_clone().unwrap(); - let mut response_reader = BufReader::new(response_reader_stream); - if let Err(_) = writeln!(writer, "INFO") { - self.stream = None; - println!("\nConnection lost"); - continue 'outer; + KeyCode::Backspace => { + if !current_input.is_empty() { + current_input.pop(); + execute!( + stdout(), + MoveToColumn(2), + Clear(ClearType::UntilNewLine) + ) + .unwrap(); + print!("{}", current_input); + stdout().flush().unwrap(); } - let mut response = String::new(); - if let Err(_) = response_reader.read_line(&mut response) { - self.stream = None; - println!("\nConnection lost"); + } + KeyCode::Char(c) => { + if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { + restore_terminal(&self.cleanup_done); + std::process::exit(0); + } else if key.modifiers.contains(KeyModifiers::CONTROL) + && c == 'd' + { + restore_terminal(&self.cleanup_done); + std::process::exit(0); + } else if c == '\t' { + self.handle_tab_completion(&mut current_input); } else { - let resp = response.trim(); - if resp.starts_with("INFO ") { - let parts: Vec<&str> = resp.split_whitespace().collect(); - if parts.len() == 4 { - println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); - } else { - println!("\n{}", resp); - } - } else if resp.starts_with("ERROR ") { - println!("\nError: {}", &resp[6..]); - } else { - println!("\n{}", resp); - } + current_input.push(c); + print!("{}", c); + stdout().flush().unwrap(); } } - "help" => { - help(); + KeyCode::Tab => { + self.handle_tab_completion(&mut current_input); } - "quit" => break 'outer, - _ => help(), - } - break; - } - KeyCode::Backspace => { - if !current_input.is_empty() { - current_input.pop(); - execute!(stdout(), MoveToColumn(2), - Clear(ClearType::UntilNewLine)).unwrap(); - print!("{}", current_input); - stdout().flush().unwrap(); + _ => {} } } - KeyCode::Char(c) => { - if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { - execute!(stdout(), Show).unwrap(); - return; - } else if c == '\t' { - self.handle_tab_completion(&mut current_input); - } else { - current_input.push(c); - print!("{}", c); - stdout().flush().unwrap(); - } - } - KeyCode::Tab => { - self.handle_tab_completion(&mut current_input); - } - _ => {} - } } - } _ => {} } } } - disable_raw_mode().unwrap(); - execute!(stdout(), Show).unwrap(); + restore_terminal(&self.cleanup_done); } fn handle_tab_completion(&mut self, current_input: &mut String) { @@ -387,6 +626,77 @@ impl ClientState { } // else do nothing } + + fn play_spin_animation(&mut self, stream: &mut TlsStream) { + use std::io::Write; + use std::thread; + use std::time::Duration; + + let symbols = ['C', 'L', 'O', 'G', 'D', 'S']; + let frames = 20; + let delay = Duration::from_millis(50); + + for _ in 0..frames { + let reels: String = (0..3) + .map(|_| symbols[rand::random::() % symbols.len()]) + .collect(); + + execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); + print!( + "[ {} ] [ {} ] [ {} ]", + reels.chars().nth(0).unwrap(), + reels.chars().nth(1).unwrap(), + reels.chars().nth(2).unwrap() + ); + stdout().flush().unwrap(); + + thread::sleep(delay); + } + + let mut response_str = String::new(); + let mut reader = std::io::BufReader::new(stream.try_clone().unwrap()); + if reader.read_line(&mut response_str).is_ok() { + let response = response_str.trim(); + + if response.starts_with("GAME_RESULT") { + let parts: Vec<&str> = response.split_whitespace().collect(); + if parts.len() >= 7 { + let reels = parts[5]; + let win_type = parts[6]; + + execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); + print!( + "[ {} ] [ {} ] [ {} ]", + reels.chars().nth(0).unwrap_or('?'), + reels.chars().nth(1).unwrap_or('?'), + reels.chars().nth(2).unwrap_or('?') + ); + println!(); + + if win_type == "jackpot" { + println!("\n🎉 JACKPOT! 🎉"); + } else if win_type == "two" { + println!("\n✨ Two of a kind! ✨"); + } else { + println!("\nNo win."); + } + + println!("Winnings: {} PLN", parts[2]); + println!("New Balance: {} PLN", parts[4]); + stdout().flush().unwrap(); + } + } + } + } +} + +fn restore_terminal(cleanup_done: &Arc>) { + let mut done = cleanup_done.lock().unwrap(); + if !*done { + let _ = disable_raw_mode(); + let _ = execute!(stdout(), Show); + *done = true; + } } fn get_commands() -> Vec { @@ -400,13 +710,15 @@ fn get_commands() -> Vec { "setcred".to_string(), "logout".to_string(), "info".to_string(), + "play".to_string(), "help".to_string(), "quit".to_string(), ] } fn help() { - println!("\nAvailable commands:\r + println!( + "\nAvailable commands:\r connect : - connect to the server\r send - send a message to the server\r disconnect - disconnect from the server\r @@ -416,8 +728,38 @@ fn help() { setcred - set credentials for a host\r logout - logout from the server\r info - show login info\r + play [bet] - start a game (e.g., play slots 50)\r help - show this message\r - quit - quit the program"); + quit - quit the program" + ); +} + +fn redraw_game(game_output: &str) { + execute!( + stdout(), + Clear(ClearType::CurrentLine), + Clear(ClearType::FromCursorDown), + MoveToColumn(0) + ) + .unwrap(); + println!("{}", game_output); + stdout().flush().unwrap(); +} + +fn redraw_game_lines(game_output: &str, lines: usize) { + use crossterm::cursor::MoveUp; + if lines > 0 { + execute!(stdout(), MoveUp(lines as u16)).unwrap(); + } + execute!( + stdout(), + Clear(ClearType::CurrentLine), + Clear(ClearType::FromCursorDown), + MoveToColumn(0) + ) + .unwrap(); + println!("{}", game_output); + stdout().flush().unwrap(); } fn redraw(current_input: &str) { @@ -431,36 +773,40 @@ fn read_line(prompt: &str) -> String { execute!(stdout(), Show).unwrap(); printsame!("{}", prompt); stdout().flush().unwrap(); - + let mut input = String::new(); let prompt_len = prompt.len(); - + loop { match read().unwrap() { Event::Key(key) => { if key.kind == crossterm::event::KeyEventKind::Press { match key.code { - KeyCode::Enter => { - println!(); - break; - } - KeyCode::Backspace => { - if !input.is_empty() { - input.pop(); - // Move cursor back and clear from current position to end of line - execute!(stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), - Clear(ClearType::UntilNewLine)).unwrap(); - stdout().flush().unwrap(); + KeyCode::Enter => { + println!(); + break; + } + KeyCode::Backspace => { + if !input.is_empty() { + input.pop(); + // Move cursor back and clear from current position to end of line + execute!( + stdout(), + MoveToColumn((prompt_len + input.len() + 1) as u16), + Clear(ClearType::UntilNewLine) + ) + .unwrap(); + stdout().flush().unwrap(); + } + } + KeyCode::Char(c) => { + input.push(c); + print!("{}", c); + stdout().flush().unwrap(); + } + _ => {} } } - KeyCode::Char(c) => { - input.push(c); - print!("{}", c); - stdout().flush().unwrap(); - } - _ => {} - } - } } _ => {} } @@ -472,36 +818,40 @@ fn read_password(prompt: &str) -> String { execute!(stdout(), Show).unwrap(); print!("{}", prompt); stdout().flush().unwrap(); - + let mut input = String::new(); let prompt_len = prompt.len(); - + loop { match read().unwrap() { Event::Key(key) => { if key.kind == crossterm::event::KeyEventKind::Press { match key.code { - KeyCode::Enter => { - println!(); - break; - } - KeyCode::Backspace => { - if !input.is_empty() { - input.pop(); - // Move cursor back and clear from current position to end of line - execute!(stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), - Clear(ClearType::UntilNewLine)).unwrap(); - stdout().flush().unwrap(); + KeyCode::Enter => { + println!(); + break; + } + KeyCode::Backspace => { + if !input.is_empty() { + input.pop(); + // Move cursor back and clear from current position to end of line + execute!( + stdout(), + MoveToColumn((prompt_len + input.len() + 1) as u16), + Clear(ClearType::UntilNewLine) + ) + .unwrap(); + stdout().flush().unwrap(); + } + } + KeyCode::Char(c) => { + input.push(c); + print!("*"); + stdout().flush().unwrap(); + } + _ => {} } } - KeyCode::Char(c) => { - input.push(c); - print!("*"); - stdout().flush().unwrap(); - } - _ => {} - } - } } _ => {} } diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs new file mode 100644 index 0000000..8ab5c7c --- /dev/null +++ b/src/bin/client/tls.rs @@ -0,0 +1,150 @@ +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +pub struct TlsConnector { + connector: rustls::ClientConnection, + server_name: String, +} + +impl TlsConnector { + pub fn new(server_name: &str) -> Result> { + let config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier)) + .with_no_client_auth(); + + let connector = + rustls::ClientConnection::new(std::sync::Arc::new(config), server_name.try_into()?); + + Ok(Self { + connector, + server_name: server_name.to_string(), + }) + } + + pub fn connect( + &self, + stream: TcpStream, + ) -> Result> { + let mut conn = rustls::ClientConnection::new( + std::sync::Arc::new( + rustls::ClientConfig::builder() + .dangerous() + .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT) + .with_no_client_auth(), + ), + self.server_name.clone().try_into()?, + )?; + + conn.complete_io(&mut std::io::BufReader::new(&stream))?; + + Ok(TlsClientStream { + stream, + conn: std::sync::Arc::new(conn), + }) + } +} + +struct InsecureVerifier; + +impl rustls::client::danger::ServerCertVerifier for InsecureVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ECDSA_NISTP521_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA256_LEGACY, + rustls::SignatureScheme::RSA_PSS_SHA384_LEGACY, + rustls::SignatureScheme::RSA_PSS_SHA512_LEGACY, + ] + } +} + +pub struct TlsClientStream { + stream: TcpStream, + conn: std::sync::Arc, +} + +impl TlsClientStream { + pub fn read_line(&mut self, buf: &mut String) -> std::io::Result { + let mut byte_buf = [0u8; 1024]; + loop { + let mut tls_reader = std::io::BufReader::new(self.conn.clone()); + let n = tls_reader.read(&mut byte_buf)?; + if n == 0 { + return Ok(0); + } + let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); + buf.push_str(chunk); + if buf.contains('\n') { + return Ok(n); + } + } + } + + pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { + use std::io::Write; + self.conn.writer().write_all(msg.as_bytes())?; + self.conn.writer().write_all(b"\n")?; + self.conn.writer().flush()?; + Ok(()) + } + + pub fn try_clone(&self) -> std::io::Result { + let new_stream = self.stream.try_clone()?; + let mut conn = rustls::ClientConnection::new( + std::sync::Arc::new( + rustls::ClientConfig::builder() + .dangerous() + .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT) + .with_no_client_auth(), + ), + self.server_name.clone().try_into().unwrap(), + )?; + + conn.complete_io(&mut std::io::BufReader::new(&new_stream))?; + + Ok(TlsClientStream { + stream: new_stream, + conn: std::sync::Arc::new(conn), + }) + } +} -- cgit v1.2.3