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 --- Cargo.toml | 7 +- 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 ++++++ src/bin/server/args.rs | 11 +- src/bin/server/database.rs | 26 +- src/bin/server/games/mod.rs | 4 + src/bin/server/games/registry.rs | 55 +++ src/bin/server/games/session.rs | 77 +++ src/bin/server/games/slots.rs | 179 +++++++ src/bin/server/games/traits.rs | 67 +++ src/bin/server/handler.rs | 177 ++++++- src/bin/server/main.rs | 34 +- src/bin/server/tls.rs | 96 ++++ src/bin/windows-test/main.rs | 61 --- 23 files changed, 1906 insertions(+), 431 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 create mode 100644 src/bin/server/games/mod.rs create mode 100644 src/bin/server/games/registry.rs create mode 100644 src/bin/server/games/session.rs create mode 100644 src/bin/server/games/slots.rs create mode 100644 src/bin/server/games/traits.rs create mode 100644 src/bin/server/tls.rs delete mode 100644 src/bin/windows-test/main.rs diff --git a/Cargo.toml b/Cargo.toml index ce86a04..9f72128 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,9 @@ once_cell = "1.18" hmac = "0.12" sha2 = "0.10" base64 = "0.21" -ctrlc = "3.4" \ No newline at end of file +ctrlc = "3.4" +serde = { version = "1.0", features = ["derive"] } +uuid = { version = "1.0", features = ["v4"] } +native-tls = "0.2" +rustls = "0.21" +rustls-pemfile = "1.0" \ No newline at end of file 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), + }) + } +} diff --git a/src/bin/server/args.rs b/src/bin/server/args.rs index 31c0acc..fc844ea 100644 --- a/src/bin/server/args.rs +++ b/src/bin/server/args.rs @@ -1,12 +1,14 @@ pub struct Args { pub ip: String, pub port: u16, + pub unsafe_mode: bool, } pub fn parse_args() -> Args { let args: Vec = std::env::args().collect(); let mut ip = "127.0.0.1".to_string(); let mut port = 11525; + let mut unsafe_mode = false; let mut i = 1; while i < args.len() { if args[i] == "-p" { @@ -32,10 +34,17 @@ pub fn parse_args() -> Args { eprintln!("-i requires an IP address"); std::process::exit(1); } + } else if args[i] == "--unsafe" { + unsafe_mode = true; + i += 1; } else { eprintln!("Unknown argument: {}", args[i]); std::process::exit(1); } } - Args { ip, port } + Args { + ip, + port, + unsafe_mode, + } } diff --git a/src/bin/server/database.rs b/src/bin/server/database.rs index 0fac387..1653071 100644 --- a/src/bin/server/database.rs +++ b/src/bin/server/database.rs @@ -11,14 +11,22 @@ pub fn init_db() -> Connection { balance INTEGER DEFAULT 1000 )", [], - ).unwrap(); + ) + .unwrap(); // Add column if not exists (for migration) let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []); - let _ = conn.execute("ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", []); + let _ = conn.execute( + "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", + [], + ); conn } -pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box> { +pub fn add_user( + conn: &Connection, + username: &str, + hashed_password: &str, +) -> Result<(), Box> { loop { let account_id: u32 = rand::random::(); match conn.execute( @@ -33,7 +41,9 @@ pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Res } pub fn get_hashed_password(conn: &Connection, username: &str) -> Option { - let mut stmt = conn.prepare("SELECT hashed_password FROM users WHERE username = ?1").unwrap(); + let mut stmt = conn + .prepare("SELECT hashed_password FROM users WHERE username = ?1") + .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { row.get(0).ok() @@ -43,7 +53,9 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option } pub fn get_account_id(conn: &Connection, username: &str) -> Option { - let mut stmt = conn.prepare("SELECT account_id FROM users WHERE username = ?1").unwrap(); + let mut stmt = conn + .prepare("SELECT account_id FROM users WHERE username = ?1") + .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { Some(row.get::<_, u32>(0).ok()?) @@ -53,7 +65,9 @@ pub fn get_account_id(conn: &Connection, username: &str) -> Option { } pub fn get_balance(conn: &Connection, username: &str) -> Option { - let mut stmt = conn.prepare("SELECT balance FROM users WHERE username = ?1").unwrap(); + let mut stmt = conn + .prepare("SELECT balance FROM users WHERE username = ?1") + .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { Some(row.get::<_, i64>(0).ok()?) diff --git a/src/bin/server/games/mod.rs b/src/bin/server/games/mod.rs new file mode 100644 index 0000000..4c62760 --- /dev/null +++ b/src/bin/server/games/mod.rs @@ -0,0 +1,4 @@ +pub mod registry; +pub mod session; +pub mod slots; +pub mod traits; diff --git a/src/bin/server/games/registry.rs b/src/bin/server/games/registry.rs new file mode 100644 index 0000000..e77a138 --- /dev/null +++ b/src/bin/server/games/registry.rs @@ -0,0 +1,55 @@ +#![allow(dead_code)] + +use crate::games::traits::Game; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +pub type GameRef = Arc; + +pub struct GameRegistry { + games: Arc>>, +} + +impl GameRegistry { + pub fn new() -> Self { + Self { + games: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn register_game(&self, game: G) { + let game_type = game.game_type().to_string(); + let mut games = self.games.lock().unwrap(); + games.insert(game_type, Arc::new(game)); + } + + pub fn get_game(&self, game_type: &str) -> Option { + let games = self.games.lock().unwrap(); + games.get(game_type).cloned() + } + + pub fn list_games(&self) -> Vec { + let games = self.games.lock().unwrap(); + games.keys().cloned().collect() + } + + pub fn game_exists(&self, game_type: &str) -> bool { + let games = self.games.lock().unwrap(); + games.contains_key(game_type) + } +} + +// Global registry instance +use crate::games::slots::SlotsGame; +use once_cell::sync::Lazy; + +static GAME_REGISTRY: Lazy = Lazy::new(|| { + let registry = GameRegistry::new(); + // Register games here + registry.register_game(SlotsGame::new()); + registry +}); + +pub fn get_game_registry() -> &'static GameRegistry { + &GAME_REGISTRY +} diff --git a/src/bin/server/games/session.rs b/src/bin/server/games/session.rs new file mode 100644 index 0000000..595a745 --- /dev/null +++ b/src/bin/server/games/session.rs @@ -0,0 +1,77 @@ +#![allow(dead_code)] + +use crate::games::traits::{GameError, GameState}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct GameSession { + pub session_id: String, + pub username: String, + pub game_type: String, + pub bet_amount: u64, + pub state: GameState, +} + +pub struct SessionManager { + sessions: Arc>>, +} + +impl SessionManager { + pub fn new() -> Self { + Self { + sessions: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn create_session(&self, username: &str, game_type: String, state: GameState) -> String { + let session_id = Uuid::new_v4().to_string(); + let session = GameSession { + session_id: session_id.clone(), + username: username.to_string(), + game_type, + bet_amount: state.bet_amount, + state, + }; + + let mut sessions = self.sessions.lock().unwrap(); + sessions.insert(session_id.clone(), session); + session_id + } + + pub fn get_session(&self, session_id: &str) -> Result { + let sessions = self.sessions.lock().unwrap(); + sessions + .get(session_id) + .cloned() + .ok_or(GameError::SessionNotFound) + } + + pub fn update_session(&self, session_id: &str, new_state: GameState) -> Result<(), GameError> { + let mut sessions = self.sessions.lock().unwrap(); + if let Some(session) = sessions.get_mut(session_id) { + session.state = new_state; + Ok(()) + } else { + Err(GameError::SessionNotFound) + } + } + + pub fn remove_session(&self, session_id: &str) -> Result<(), GameError> { + let mut sessions = self.sessions.lock().unwrap(); + sessions + .remove(session_id) + .ok_or(GameError::SessionNotFound) + .map(|_| ()) + } + + pub fn get_user_sessions(&self, username: &str) -> Vec { + let sessions = self.sessions.lock().unwrap(); + sessions + .values() + .filter(|s| s.username == username) + .cloned() + .collect() + } +} diff --git a/src/bin/server/games/slots.rs b/src/bin/server/games/slots.rs new file mode 100644 index 0000000..d84e21f --- /dev/null +++ b/src/bin/server/games/slots.rs @@ -0,0 +1,179 @@ +use crate::games::traits::{Game, GameError, GamePhase, GameResult, GameState}; +use rand::Rng; +use std::collections::HashMap; + +pub struct SlotsGame { + symbols: Vec, + payouts: HashMap, +} + +impl SlotsGame { + pub fn new() -> Self { + let symbols = vec!['C', 'L', 'O', 'G', 'D', 'S']; + let mut payouts = HashMap::new(); + + payouts.insert("CCC".to_string(), 5); + payouts.insert("LLL".to_string(), 10); + payouts.insert("OOO".to_string(), 15); + payouts.insert("GGG".to_string(), 20); + payouts.insert("DDD".to_string(), 50); + payouts.insert("SSS".to_string(), 100); + + payouts.insert("CC".to_string(), 2); + payouts.insert("LL".to_string(), 3); + payouts.insert("OO".to_string(), 4); + payouts.insert("GG".to_string(), 5); + payouts.insert("DD".to_string(), 10); + payouts.insert("SS".to_string(), 20); + + Self { symbols, payouts } + } + + fn spin_reels(&self) -> Vec { + let mut rng = rand::thread_rng(); + (0..3) + .map(|_| self.symbols[rng.gen_range(0..self.symbols.len())]) + .collect() + } + + fn calculate_payout(&self, reels: &[char], bet: u64) -> u64 { + let reel_string: String = reels.iter().collect(); + + if let Some(&multiplier) = self.payouts.get(&reel_string) { + return bet * multiplier; + } + + for i in 0..2 { + if reels[i] == reels[i + 1] { + let two_of_kind = format!("{}{}", reels[i], reels[i]); + if let Some(&multiplier) = self.payouts.get(&two_of_kind) { + return bet * multiplier; + } + } + } + + 0 + } +} + +impl Game for SlotsGame { + fn game_type(&self) -> &'static str { + "slots" + } + + fn get_name(&self) -> &'static str { + "Slots" + } + + fn get_description(&self) -> &'static str { + "Spin the reels and match symbols to win!" + } + + fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState { + let mut data = HashMap::new(); + data.insert("username".to_string(), username.to_string()); + data.insert("status".to_string(), "ready".to_string()); + + GameState { + session_id: String::new(), + game_type: "slots".to_string(), + bet_amount, + phase: GamePhase::WaitingToStart, + data, + } + } + + fn process_action( + &self, + state: &GameState, + action: &str, + _params: &[&str], + ) -> Result { + if state.phase == GamePhase::Finished { + return Err(GameError::GameOver); + } + + match action { + "spin" => { + if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing { + return Err(GameError::InvalidAction( + "Cannot spin in current phase".to_string(), + )); + } + + let reels = self.spin_reels(); + let reel_string: String = reels.iter().collect(); + + let mut new_data = state.data.clone(); + new_data.insert("reels".to_string(), reel_string.clone()); + new_data.insert("status".to_string(), "spun".to_string()); + + let winnings = self.calculate_payout(&reels, state.bet_amount); + new_data.insert("winnings".to_string(), winnings.to_string()); + + let is_jackpot = reels[0] == reels[1] && reels[1] == reels[2]; + let is_two = reels[0] == reels[1] || reels[1] == reels[2]; + new_data.insert( + "win_type".to_string(), + if is_jackpot { + "jackpot".to_string() + } else if is_two { + "two".to_string() + } else { + "none".to_string() + }, + ); + + Ok(GameState { + session_id: state.session_id.clone(), + game_type: state.game_type.clone(), + bet_amount: state.bet_amount, + phase: GamePhase::Finished, + data: new_data, + }) + } + _ => Err(GameError::InvalidAction(format!( + "Unknown action: {}", + action + ))), + } + } + + fn calculate_result(&self, state: &GameState) -> Result { + if state.phase != GamePhase::Finished { + return Err(GameError::InternalError("Game not finished".to_string())); + } + + let reel_string = state + .data + .get("reels") + .ok_or(GameError::InternalError("No reels found".to_string()))?; + + let reels: Vec = reel_string.chars().collect(); + let winnings = self.calculate_payout(&reels, state.bet_amount); + + let mut result_data = HashMap::new(); + result_data.insert("reels".to_string(), reel_string.clone()); + result_data.insert( + "payout_multiplier".to_string(), + if winnings > 0 && state.bet_amount > 0 { + (winnings / state.bet_amount).to_string() + } else { + "0".to_string() + }, + ); + result_data.insert( + "win_type".to_string(), + state.data.get("win_type").cloned().unwrap_or_default(), + ); + + Ok(GameResult { + session_id: state.session_id.clone(), + game_type: state.game_type.clone(), + bet_amount: state.bet_amount, + winnings, + final_balance: 0, + result_data, + }) + } +} diff --git a/src/bin/server/games/traits.rs b/src/bin/server/games/traits.rs new file mode 100644 index 0000000..d56b334 --- /dev/null +++ b/src/bin/server/games/traits.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GameError { + InvalidAction(String), + InsufficientBalance, + InvalidBet, + GameOver, + SessionNotFound, + InternalError(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum GamePhase { + WaitingToStart, + Playing, + Finished, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameState { + pub session_id: String, + pub game_type: String, + pub bet_amount: u64, + pub phase: GamePhase, + pub data: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameResult { + pub session_id: String, + pub game_type: String, + pub bet_amount: u64, + pub winnings: u64, + pub final_balance: u64, + pub result_data: HashMap, +} + +pub trait Game: Send + Sync { + fn game_type(&self) -> &'static str; + + fn get_name(&self) -> &'static str; + + fn get_description(&self) -> &'static str; + + fn validate_bet(&self, bet_amount: u64, user_balance: u64) -> Result<(), GameError> { + if bet_amount == 0 { + return Err(GameError::InvalidBet); + } + if bet_amount > user_balance { + return Err(GameError::InsufficientBalance); + } + Ok(()) + } + + fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState; + + fn process_action( + &self, + state: &GameState, + action: &str, + params: &[&str], + ) -> Result; + + fn calculate_result(&self, state: &GameState) -> Result; +} diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs index 01fd9c9..867910e 100644 --- a/src/bin/server/handler.rs +++ b/src/bin/server/handler.rs @@ -1,12 +1,16 @@ +use crate::database; +use crate::games::registry::get_game_registry; +use crate::games::traits::GameError; +use crate::tls::TlsStream; +use once_cell::sync::Lazy; +use rusqlite::Connection; use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; use std::sync::Mutex; -use once_cell::sync::Lazy; -use rusqlite::Connection; -use crate::database; static DB_CONN: Lazy> = Lazy::new(|| Mutex::new(database::init_db())); +#[allow(dead_code)] pub fn handle_client(stream: TcpStream) { let client_addr = stream.peer_addr().unwrap().to_string(); let mut user = "guest".to_string(); @@ -25,8 +29,49 @@ pub fn handle_client(stream: TcpStream) { } fn process_message(msg: &str, user: &mut String) -> String { - let parts: Vec<&str> = msg.splitn(3, ' ').collect(); + let parts: Vec<&str> = msg.splitn(5, ' ').collect(); match parts[0] { + "GAME_LIST" => { + let registry = get_game_registry(); + let games: Vec = registry.list_games(); + if games.is_empty() { + "ERROR No games available".to_string() + } else { + let game_info: Vec = games + .iter() + .filter_map(|gt| { + registry + .get_game(gt) + .map(|g| format!("{}:{}", gt, g.get_name())) + }) + .collect(); + format!("GAME_LIST {}", game_info.join(",")) + } + } + "GAME_INFO" => { + if parts.len() != 2 { + "ERROR Invalid GAME_INFO format. Usage: GAME_INFO ".to_string() + } else { + let registry = get_game_registry(); + match registry.get_game(parts[1]) { + Some(game) => format!( + "GAME_INFO {} {} {}", + parts[1], + game.get_name(), + game.get_description() + ), + None => format!("ERROR Unknown game type: {}", parts[1]), + } + } + } + "PLAY_START" => { + if parts.len() != 3 { + "ERROR Invalid PLAY_START format. Usage: PLAY_START " + .to_string() + } else { + handle_play_start(user, parts[1], parts[2]) + } + } "REGISTER" => { if parts.len() != 3 { "ERROR Invalid REGISTER format".to_string() @@ -65,13 +110,133 @@ fn process_message(msg: &str, user: &mut String) -> String { "ERROR Not logged in".to_string() } else { let db = DB_CONN.lock().unwrap(); - if let (Some(id), Some(balance)) = (database::get_account_id(&db, user), database::get_balance(&db, user)) { + if let (Some(id), Some(balance)) = ( + database::get_account_id(&db, user), + database::get_balance(&db, user), + ) { format!("INFO {} {} {}", user, id, balance) } else { "ERROR Error retrieving user info".to_string() } } } - _ => msg.to_string(), // Echo back for compatibility + _ => msg.to_string(), + } +} + +fn handle_play_start(user: &str, game_type: &str, bet_amount_str: &str) -> String { + if user == "guest" { + return "ERROR Not logged in".to_string(); + } + + let bet_amount: u64 = match bet_amount_str.parse() { + Ok(amount) => amount, + Err(_) => return "ERROR Invalid bet amount".to_string(), + }; + + let db = DB_CONN.lock().unwrap(); + let current_balance = match database::get_balance(&db, user) { + Some(balance) => balance as u64, + None => return "ERROR Could not retrieve balance".to_string(), + }; + + let registry = get_game_registry(); + let game = match registry.get_game(game_type) { + Some(game) => game, + None => return format!("ERROR Unknown game type: {}", game_type), + }; + + if let Err(e) = game.validate_bet(bet_amount, current_balance) { + return match e { + GameError::InsufficientBalance => "ERROR Insufficient balance".to_string(), + GameError::InvalidBet => "ERROR Invalid bet amount".to_string(), + _ => "ERROR Bet validation failed".to_string(), + }; + } + + let game_state = game.prepare_state(user, bet_amount); + + drop(db); + let db = DB_CONN.lock().unwrap(); + if let Err(_) = db.execute( + "UPDATE users SET balance = balance - ?1 WHERE username = ?2", + (bet_amount as i64, user), + ) { + return "ERROR Failed to deduct bet".to_string(); + } + + let new_state = match game.process_action(&game_state, "spin", &[]) { + Ok(state) => state, + Err(e) => { + let _ = db.execute( + "UPDATE users SET balance = balance + ?1 WHERE username = ?2", + (bet_amount as i64, user), + ); + return format!("ERROR Failed to spin: {:?}", e); + } + }; + + let result = match game.calculate_result(&new_state) { + Ok(result) => result, + Err(e) => { + let _ = db.execute( + "UPDATE users SET balance = balance + ?1 WHERE username = ?2", + (bet_amount as i64, user), + ); + return format!("ERROR Failed to calculate result: {:?}", e); + } + }; + + let total_return = result.bet_amount + result.winnings; + if let Err(_) = db.execute( + "UPDATE users SET balance = balance + ?1 WHERE username = ?2", + (total_return as i64, user), + ) { + return "ERROR Failed to update balance".to_string(); + } + + let new_balance = match database::get_balance(&db, user) { + Some(balance) => balance, + None => return "ERROR Could not retrieve new balance".to_string(), + }; + + let reels = new_state.data.get("reels").cloned().unwrap_or_default(); + let win_type = new_state.data.get("win_type").cloned().unwrap_or_default(); + + format!( + "GAME_RESULT {} {} {} {} {} {}", + result.session_id, result.winnings, result.bet_amount, new_balance, reels, win_type + ) +} + +pub fn handle_client_tls(mut stream: TlsStream) { + let client_addr = stream.peer_addr().unwrap().to_string(); + let mut user = "guest".to_string(); + println!("[{} @ {}] TLS Client connected", user, client_addr); + + loop { + let mut line = String::new(); + match stream.read_line(&mut line) { + Ok(0) => break, + Ok(_) => {} + Err(e) => { + eprintln!("Read error: {}", e); + break; + } + } + + let line = line.trim(); + if line.is_empty() { + continue; + } + + println!("[{} @ {}] Received: {}", user, client_addr, line); + let response = process_message(line, &mut user); + if let Err(e) = stream.write_line(&response) { + eprintln!("Write error: {}", e); + break; + } + println!("[{} @ {}] Sent: {}", user, client_addr, response); } + println!("[{} @ {}] TLS Client disconnected", user, client_addr); } diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs index 7693069..4328559 100644 --- a/src/bin/server/main.rs +++ b/src/bin/server/main.rs @@ -1,8 +1,11 @@ mod args; -mod handler; mod database; +mod games; +mod handler; +mod tls; use std::net::TcpListener; +use std::sync::Arc; use std::thread; fn main() { @@ -14,11 +17,34 @@ fn main() { std::process::exit(1); } }; - println!("Server listening on {}", listener.local_addr().unwrap()); + + let tls = match tls::TlsServer::new("identity.p12", "key.pem", args.unsafe_mode) { + Ok(t) => Arc::new(t), + Err(e) => { + eprintln!("Failed to initialize TLS: {}", e); + std::process::exit(1); + } + }; + + println!( + "Server listening on {} (TLS enabled){}", + listener.local_addr().unwrap(), + if args.unsafe_mode { + " [UNSAFE MODE]" + } else { + "" + } + ); for stream in listener.incoming() { let stream = stream.unwrap(); - thread::spawn(move || { - handler::handle_client(stream); + let tls = tls.clone(); + thread::spawn(move || match tls.accept(stream) { + Ok(tls_stream) => { + handler::handle_client_tls(tls_stream); + } + Err(e) => { + eprintln!("TLS handshake failed: {}", e); + } }); } } diff --git a/src/bin/server/tls.rs b/src/bin/server/tls.rs new file mode 100644 index 0000000..acf3315 --- /dev/null +++ b/src/bin/server/tls.rs @@ -0,0 +1,96 @@ +use std::io::{Read, Write}; +use std::net::TcpStream; + +#[derive(Clone)] +pub struct TlsServer { + acceptor: native_tls::TlsAcceptor, +} + +impl TlsServer { + pub fn new( + cert_path: &str, + _key_path: &str, + unsafe_mode: bool, + ) -> Result> { + let cert_data = if unsafe_mode { + if std::path::Path::new(cert_path).exists() { + std::fs::read(cert_path)? + } else { + return Err(format!( + "Certificate file '{}' not found. In --unsafe mode, you must provide a valid PKCS12 certificate file.\n\ + You can generate one with:\n\ + openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes\n\ + openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem", + cert_path + ).into()); + } + } else { + std::fs::read(cert_path)? + }; + + let identity = native_tls::Identity::from_pkcs12(&cert_data, "")?; + let acceptor = native_tls::TlsAcceptor::new(identity)?; + + Ok(Self { acceptor }) + } + + pub fn accept(&self, stream: TcpStream) -> Result> { + let stream = self.acceptor.accept(stream)?; + Ok(TlsStream::new(stream)) + } +} + +pub struct TlsStream { + stream: native_tls::TlsStream, +} + +impl TlsStream { + pub fn new(stream: native_tls::TlsStream) -> Self { + Self { stream } + } + + pub fn read_line(&mut self, s: &mut String) -> std::io::Result { + let mut buf = [0u8; 1024]; + loop { + match self.stream.read(&mut buf) { + Ok(0) => return Ok(0), + Ok(n) => { + let chunk = std::str::from_utf8(&buf[..n]).unwrap_or(""); + s.push_str(chunk); + if s.contains('\n') { + return Ok(n); + } + } + Err(e) => return Err(e), + } + } + } + + pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { + use std::io::Write; + self.stream.write_all(msg.as_bytes())?; + self.stream.write_all(b"\n")?; + self.stream.flush()?; + Ok(()) + } + + pub fn peer_addr(&self) -> std::io::Result { + self.stream.get_ref().peer_addr() + } +} + +impl Read for TlsStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.stream.read(buf) + } +} + +impl Write for TlsStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.stream.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.stream.flush() + } +} diff --git a/src/bin/windows-test/main.rs b/src/bin/windows-test/main.rs deleted file mode 100644 index b8f84fe..0000000 --- a/src/bin/windows-test/main.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::io::{self, Write}; -use std::ptr; -use windows_sys::Win32::Foundation::HANDLE; -use windows_sys::Win32::System::Console::{ - GetConsoleMode, ReadConsoleW, SetConsoleMode, GetStdHandle, STD_INPUT_HANDLE, - ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, -}; - -fn main() -> io::Result<()> { - unsafe { - let h: HANDLE = GetStdHandle(STD_INPUT_HANDLE); - if h == std::ptr::null_mut() || h == -1_i32 as HANDLE { return Err(io::Error::last_os_error()); } - - // save current mode - let mut mode: u32 = 0; - if GetConsoleMode(h, &mut mode) == 0 { return Err(io::Error::last_os_error()); } - - // disable line input and echo so input is available per-key and not echoed - let new_mode = mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT); - if SetConsoleMode(h, new_mode) == 0 { return Err(io::Error::last_os_error()); } - - print!("Press keys (q to exit): "); - io::stdout().flush()?; - - // ReadConsoleW reads UTF-16; we'll read one wchar at a time - let mut buf: [u16; 1] = [0]; - loop { - let mut read = 0u32; - if ReadConsoleW( - h, - buf.as_mut_ptr() as *mut _, - 1, - &mut read, - ptr::null_mut(), - ) == 0 - { - // restore mode on error before returning - let _ = SetConsoleMode(h, mode); - return Err(io::Error::last_os_error()); - } - - let ch = std::char::from_u32(buf[0] as u32).unwrap_or('\u{FFFD}'); - if ch == 'q' { break; } // q to exit - - // handle Enter (CR) so output is readable - if ch == '\r' { - println!(); - } else { - // show code point (no automatic console echo) - print!("[{:?}]", ch); - io::stdout().flush()?; - } - } - - // restore original console mode - let _ = SetConsoleMode(h, mode); - println!("\nexited"); - } - - Ok(()) -} -- cgit v1.2.3