diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-24 14:48:12 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-24 14:48:12 +0200 |
| commit | 5d6b5049c2a34a3f8abe725d4a871ba84a0dd714 (patch) | |
| tree | 57cb03bf59b3358a96742f455d38c21f4f645396 | |
| parent | stable and working, both platforms (diff) | |
| download | zagan-bet-5d6b5049c2a34a3f8abe725d4a871ba84a0dd714.tar.gz zagan-bet-5d6b5049c2a34a3f8abe725d4a871ba84a0dd714.zip | |
TLS transport and games framework on client and server
| -rw-r--r-- | Cargo.toml | 7 | ||||
| -rw-r--r-- | src/bin/client/connection.rs | 136 | ||||
| -rw-r--r-- | src/bin/client/database.rs | 26 | ||||
| -rw-r--r-- | src/bin/client/games/manager.rs | 127 | ||||
| -rw-r--r-- | src/bin/client/games/mod.rs | 3 | ||||
| -rw-r--r-- | src/bin/client/games/slots.rs | 62 | ||||
| -rw-r--r-- | src/bin/client/games/traits.rs | 30 | ||||
| -rw-r--r-- | src/bin/client/macros.rs | 2 | ||||
| -rw-r--r-- | src/bin/client/main.rs | 14 | ||||
| -rw-r--r-- | src/bin/client/mod.rs | 3 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 990 | ||||
| -rw-r--r-- | src/bin/client/tls.rs | 150 | ||||
| -rw-r--r-- | src/bin/server/args.rs | 11 | ||||
| -rw-r--r-- | src/bin/server/database.rs | 26 | ||||
| -rw-r--r-- | src/bin/server/games/mod.rs | 4 | ||||
| -rw-r--r-- | src/bin/server/games/registry.rs | 55 | ||||
| -rw-r--r-- | src/bin/server/games/session.rs | 77 | ||||
| -rw-r--r-- | src/bin/server/games/slots.rs | 179 | ||||
| -rw-r--r-- | src/bin/server/games/traits.rs | 67 | ||||
| -rw-r--r-- | src/bin/server/handler.rs | 177 | ||||
| -rw-r--r-- | src/bin/server/main.rs | 34 | ||||
| -rw-r--r-- | src/bin/server/tls.rs | 96 | ||||
| -rw-r--r-- | src/bin/windows-test/main.rs | 61 |
23 files changed, 1906 insertions, 431 deletions
| @@ -15,4 +15,9 @@ once_cell = "1.18" | |||
| 15 | hmac = "0.12" | 15 | hmac = "0.12" |
| 16 | sha2 = "0.10" | 16 | sha2 = "0.10" |
| 17 | base64 = "0.21" | 17 | base64 = "0.21" |
| 18 | ctrlc = "3.4" \ No newline at end of file | 18 | ctrlc = "3.4" |
| 19 | serde = { version = "1.0", features = ["derive"] } | ||
| 20 | uuid = { version = "1.0", features = ["v4"] } | ||
| 21 | native-tls = "0.2" | ||
| 22 | rustls = "0.21" | ||
| 23 | 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 @@ | |||
| 1 | use std::io::{BufRead, BufReader, Write}; | 1 | use crate::database; |
| 2 | use native_tls::TlsConnector; | ||
| 3 | use rusqlite::Connection; | ||
| 4 | use std::io::{Read, Write}; | ||
| 2 | use std::net::TcpStream; | 5 | use std::net::TcpStream; |
| 3 | use std::sync::mpsc; | 6 | use std::sync::mpsc; |
| 7 | use std::sync::Arc; | ||
| 8 | use std::sync::Mutex; | ||
| 4 | use std::thread; | 9 | use std::thread; |
| 5 | use std::time::{Duration, Instant}; | 10 | use std::time::{Duration, Instant}; |
| 6 | use rusqlite::Connection; | ||
| 7 | use crate::database; | ||
| 8 | 11 | ||
| 9 | pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option<String>), Box<dyn std::error::Error>> { | 12 | static UNSAFE_MODE: Mutex<bool> = Mutex::new(false); |
| 13 | |||
| 14 | pub fn set_unsafe_mode(unsafe_mode: bool) { | ||
| 15 | let mut mode = UNSAFE_MODE.lock().unwrap(); | ||
| 16 | *mode = unsafe_mode; | ||
| 17 | } | ||
| 18 | |||
| 19 | pub fn connect( | ||
| 20 | addr: &str, | ||
| 21 | conn: &Connection, | ||
| 22 | ) -> Result<(TlsStream, Option<String>), Box<dyn std::error::Error>> { | ||
| 10 | let (tx, rx) = mpsc::channel(); | 23 | let (tx, rx) = mpsc::channel(); |
| 11 | let addr_str = addr.to_string(); | 24 | let addr_str = addr.to_string(); |
| 12 | thread::spawn(move || { | 25 | let server_name = addr_str |
| 13 | match TcpStream::connect(&addr_str) { | 26 | .split(':') |
| 14 | Ok(s) => tx.send(Ok(s)).unwrap(), | 27 | .next() |
| 15 | Err(e) => tx.send(Err(e)).unwrap(), | 28 | .unwrap_or("127.0.0.1") |
| 16 | } | 29 | .to_string(); |
| 30 | thread::spawn(move || match TcpStream::connect(&addr_str) { | ||
| 31 | Ok(s) => tx.send(Ok(s)).unwrap(), | ||
| 32 | Err(e) => tx.send(Err(e)).unwrap(), | ||
| 17 | }); | 33 | }); |
| 18 | match rx.recv_timeout(Duration::from_secs(3)) { | 34 | match rx.recv_timeout(Duration::from_secs(3)) { |
| 19 | Ok(Ok(s)) => { | 35 | Ok(Ok(s)) => { |
| 20 | let mut ping_writer = s.try_clone().unwrap(); | 36 | let unsafe_mode = *UNSAFE_MODE.lock().unwrap(); |
| 21 | let mut ping_reader = BufReader::new(s.try_clone().unwrap()); | 37 | |
| 38 | let connector = if unsafe_mode { | ||
| 39 | TlsConnector::builder() | ||
| 40 | .danger_accept_invalid_certs(true) | ||
| 41 | .danger_accept_invalid_hostnames(true) | ||
| 42 | .build()? | ||
| 43 | } else { | ||
| 44 | TlsConnector::builder().build()? | ||
| 45 | }; | ||
| 46 | |||
| 47 | let tls_stream = connector.connect(&server_name, s)?; | ||
| 48 | let mut tls = TlsStream::new(tls_stream); | ||
| 49 | |||
| 22 | let start = Instant::now(); | 50 | let start = Instant::now(); |
| 23 | writeln!(ping_writer, "PING").unwrap(); | 51 | tls.write_line("PING")?; |
| 24 | let mut response = String::new(); | 52 | let mut response = String::new(); |
| 25 | ping_reader.read_line(&mut response).unwrap(); | 53 | tls.read_line(&mut response)?; |
| 26 | let elapsed = start.elapsed(); | 54 | let elapsed = start.elapsed(); |
| 27 | println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed); | 55 | println!( |
| 28 | 56 | "\n\rConnected to {} with ping delay: {:?} (TLS)", | |
| 29 | // Check for credentials and auto-auth | 57 | addr, elapsed |
| 58 | ); | ||
| 59 | |||
| 30 | if let Some((username, password)) = database::get_credentials(conn, addr) { | 60 | if let Some((username, password)) = database::get_credentials(conn, addr) { |
| 31 | let auth_msg = format!("AUTH {} {}", username, password); | 61 | let auth_msg = format!("AUTH {} {}", username, password); |
| 32 | writeln!(ping_writer, "{}", auth_msg).unwrap(); | 62 | tls.write_line(&auth_msg)?; |
| 33 | let mut auth_response = String::new(); | 63 | let mut auth_response = String::new(); |
| 34 | ping_reader.read_line(&mut auth_response).unwrap(); | 64 | tls.read_line(&mut auth_response)?; |
| 35 | if auth_response.trim().starts_with("OK") { | 65 | if auth_response.trim().starts_with("OK") { |
| 36 | println!("Authenticated as {}", username); | 66 | println!("Authenticated as {}", username); |
| 37 | Ok((s, Some(username))) | 67 | Ok((tls, Some(username))) |
| 38 | } else { | 68 | } else { |
| 39 | println!("Authentication failed: {}", auth_response.trim()); | 69 | println!("Authentication failed: {}", auth_response.trim()); |
| 40 | Ok((s, None)) | 70 | Ok((tls, None)) |
| 41 | } | 71 | } |
| 42 | } else { | 72 | } else { |
| 43 | Ok((s, None)) | 73 | Ok((tls, None)) |
| 44 | } | 74 | } |
| 45 | } | 75 | } |
| 46 | Ok(Err(e)) => Err(Box::new(e)), | 76 | Ok(Err(e)) => Err(Box::new(e)), |
| @@ -48,3 +78,67 @@ pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option<Strin | |||
| 48 | Err(_) => Err("Connection attempt failed".into()), | 78 | Err(_) => Err("Connection attempt failed".into()), |
| 49 | } | 79 | } |
| 50 | } | 80 | } |
| 81 | |||
| 82 | pub struct TlsStream { | ||
| 83 | stream: Arc<Mutex<native_tls::TlsStream<TcpStream>>>, | ||
| 84 | } | ||
| 85 | |||
| 86 | unsafe impl Send for TlsStream {} | ||
| 87 | unsafe impl Sync for TlsStream {} | ||
| 88 | |||
| 89 | impl TlsStream { | ||
| 90 | pub fn new(stream: native_tls::TlsStream<TcpStream>) -> Self { | ||
| 91 | Self { | ||
| 92 | stream: Arc::new(Mutex::new(stream)), | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> { | ||
| 97 | let mut byte_buf = [0u8; 1024]; | ||
| 98 | let mut stream = self.stream.lock().unwrap(); | ||
| 99 | loop { | ||
| 100 | let n = stream.read(&mut byte_buf)?; | ||
| 101 | if n == 0 { | ||
| 102 | return Ok(0); | ||
| 103 | } | ||
| 104 | let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); | ||
| 105 | buf.push_str(chunk); | ||
| 106 | if buf.contains('\n') { | ||
| 107 | return Ok(n); | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { | ||
| 113 | let mut stream = self.stream.lock().unwrap(); | ||
| 114 | stream.write_all(msg.as_bytes())?; | ||
| 115 | stream.write_all(b"\n")?; | ||
| 116 | stream.flush()?; | ||
| 117 | Ok(()) | ||
| 118 | } | ||
| 119 | |||
| 120 | pub fn try_clone(&self) -> std::io::Result<TlsStream> { | ||
| 121 | Ok(TlsStream { | ||
| 122 | stream: Arc::clone(&self.stream), | ||
| 123 | }) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | impl Read for TlsStream { | ||
| 128 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | ||
| 129 | let mut stream = self.stream.lock().unwrap(); | ||
| 130 | stream.read(buf) | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | impl Write for TlsStream { | ||
| 135 | fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||
| 136 | let mut stream = self.stream.lock().unwrap(); | ||
| 137 | stream.write(buf) | ||
| 138 | } | ||
| 139 | |||
| 140 | fn flush(&mut self) -> std::io::Result<()> { | ||
| 141 | let mut stream = self.stream.lock().unwrap(); | ||
| 142 | stream.flush() | ||
| 143 | } | ||
| 144 | } | ||
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 { | |||
| 8 | last_used DATETIME DEFAULT CURRENT_TIMESTAMP | 8 | last_used DATETIME DEFAULT CURRENT_TIMESTAMP |
| 9 | )", | 9 | )", |
| 10 | [], | 10 | [], |
| 11 | ).unwrap(); | 11 | ) |
| 12 | .unwrap(); | ||
| 12 | conn.execute( | 13 | conn.execute( |
| 13 | "CREATE TABLE IF NOT EXISTS credentials ( | 14 | "CREATE TABLE IF NOT EXISTS credentials ( |
| 14 | host TEXT PRIMARY KEY, | 15 | host TEXT PRIMARY KEY, |
| @@ -16,12 +17,15 @@ pub fn init_db() -> Connection { | |||
| 16 | password TEXT NOT NULL | 17 | password TEXT NOT NULL |
| 17 | )", | 18 | )", |
| 18 | [], | 19 | [], |
| 19 | ).unwrap(); | 20 | ) |
| 21 | .unwrap(); | ||
| 20 | conn | 22 | conn |
| 21 | } | 23 | } |
| 22 | 24 | ||
| 23 | pub fn get_last_host(conn: &Connection) -> Option<String> { | 25 | pub fn get_last_host(conn: &Connection) -> Option<String> { |
| 24 | let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap(); | 26 | let mut stmt = conn |
| 27 | .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1") | ||
| 28 | .unwrap(); | ||
| 25 | let mut rows = stmt.query([]).unwrap(); | 29 | let mut rows = stmt.query([]).unwrap(); |
| 26 | if let Ok(Some(row)) = rows.next() { | 30 | if let Ok(Some(row)) = rows.next() { |
| 27 | row.get(0).ok() | 31 | row.get(0).ok() |
| @@ -34,10 +38,16 @@ pub fn save_host(conn: &Connection, host: &str) { | |||
| 34 | conn.execute( | 38 | conn.execute( |
| 35 | "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", | 39 | "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", |
| 36 | [host], | 40 | [host], |
| 37 | ).unwrap(); | 41 | ) |
| 42 | .unwrap(); | ||
| 38 | } | 43 | } |
| 39 | 44 | ||
| 40 | pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), Box<dyn std::error::Error>> { | 45 | pub fn save_credentials( |
| 46 | conn: &Connection, | ||
| 47 | host: &str, | ||
| 48 | username: &str, | ||
| 49 | password: &str, | ||
| 50 | ) -> Result<(), Box<dyn std::error::Error>> { | ||
| 41 | conn.execute( | 51 | conn.execute( |
| 42 | "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", | 52 | "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", |
| 43 | (host, username, password), | 53 | (host, username, password), |
| @@ -46,7 +56,9 @@ pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: | |||
| 46 | } | 56 | } |
| 47 | 57 | ||
| 48 | pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { | 58 | pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { |
| 49 | let mut stmt = conn.prepare("SELECT username, password FROM credentials WHERE host = ?1").unwrap(); | 59 | let mut stmt = conn |
| 60 | .prepare("SELECT username, password FROM credentials WHERE host = ?1") | ||
| 61 | .unwrap(); | ||
| 50 | let mut rows = stmt.query([host]).unwrap(); | 62 | let mut rows = stmt.query([host]).unwrap(); |
| 51 | if let Ok(Some(row)) = rows.next() { | 63 | if let Ok(Some(row)) = rows.next() { |
| 52 | let username: String = row.get(0).unwrap(); | 64 | let username: String = row.get(0).unwrap(); |
| @@ -55,4 +67,4 @@ pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String) | |||
| 55 | } else { | 67 | } else { |
| 56 | None | 68 | None |
| 57 | } | 69 | } |
| 58 | } \ No newline at end of file | 70 | } |
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 @@ | |||
| 1 | use crate::games::slots::SlotsGame; | ||
| 2 | use crate::games::traits::{Game, GameError, GamePhase, GameState}; | ||
| 3 | use std::collections::HashMap; | ||
| 4 | use std::io::Write; | ||
| 5 | |||
| 6 | pub struct GameManager { | ||
| 7 | current_session: Option<GameState>, | ||
| 8 | pending_game_type: Option<String>, | ||
| 9 | games: HashMap<String, Box<dyn Game>>, | ||
| 10 | } | ||
| 11 | |||
| 12 | impl GameManager { | ||
| 13 | pub fn new() -> Self { | ||
| 14 | let mut games: HashMap<String, Box<dyn Game>> = HashMap::new(); | ||
| 15 | games.insert("slots".to_string(), Box::new(SlotsGame::new())); | ||
| 16 | |||
| 17 | Self { | ||
| 18 | current_session: None, | ||
| 19 | pending_game_type: None, | ||
| 20 | games, | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | pub fn get_available_games(&self) -> Vec<(&str, &str)> { | ||
| 25 | self.games | ||
| 26 | .iter() | ||
| 27 | .map(|(k, v)| (k.as_str(), v.get_name())) | ||
| 28 | .collect() | ||
| 29 | } | ||
| 30 | |||
| 31 | pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> { | ||
| 32 | if self.current_session.is_some() { | ||
| 33 | return Err(GameError::InvalidAction); | ||
| 34 | } | ||
| 35 | |||
| 36 | if !self.games.contains_key(game_type) { | ||
| 37 | return Err(GameError::InvalidAction); | ||
| 38 | } | ||
| 39 | |||
| 40 | self.pending_game_type = Some(game_type.to_string()); | ||
| 41 | |||
| 42 | self.current_session = Some(GameState { | ||
| 43 | game_type: game_type.to_string(), | ||
| 44 | bet_amount: 10, | ||
| 45 | phase: GamePhase::WaitingToStart, | ||
| 46 | data: HashMap::new(), | ||
| 47 | }); | ||
| 48 | |||
| 49 | Ok(()) | ||
| 50 | } | ||
| 51 | |||
| 52 | pub fn start_game( | ||
| 53 | &mut self, | ||
| 54 | stream: &mut crate::connection::TlsStream, | ||
| 55 | ) -> Result<(), GameError> { | ||
| 56 | let game_type = self | ||
| 57 | .pending_game_type | ||
| 58 | .take() | ||
| 59 | .ok_or(GameError::InvalidAction)?; | ||
| 60 | |||
| 61 | let state = self | ||
| 62 | .current_session | ||
| 63 | .as_mut() | ||
| 64 | .ok_or(GameError::InvalidAction)?; | ||
| 65 | |||
| 66 | if state.phase != GamePhase::WaitingToStart { | ||
| 67 | return Err(GameError::InvalidAction); | ||
| 68 | } | ||
| 69 | |||
| 70 | let bet_amount = state.bet_amount; | ||
| 71 | |||
| 72 | let writer = stream; | ||
| 73 | writeln!(writer, "PLAY_START {} {}", game_type, bet_amount) | ||
| 74 | .map_err(|_| GameError::NetworkError)?; | ||
| 75 | |||
| 76 | Ok(()) | ||
| 77 | } | ||
| 78 | |||
| 79 | pub fn get_current_game(&self) -> Option<&dyn Game> { | ||
| 80 | self.current_session | ||
| 81 | .as_ref() | ||
| 82 | .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref())) | ||
| 83 | } | ||
| 84 | |||
| 85 | pub fn get_current_state(&self) -> Option<&GameState> { | ||
| 86 | self.current_session.as_ref() | ||
| 87 | } | ||
| 88 | |||
| 89 | pub fn get_current_render_lines(&self) -> Option<usize> { | ||
| 90 | let session = self.current_session.as_ref()?; | ||
| 91 | let game = self.games.get(&session.game_type)?; | ||
| 92 | Some(game.render_lines(session)) | ||
| 93 | } | ||
| 94 | |||
| 95 | pub fn is_in_game(&self) -> bool { | ||
| 96 | self.current_session.is_some() | ||
| 97 | } | ||
| 98 | |||
| 99 | pub fn is_game_started(&self) -> bool { | ||
| 100 | self.current_session | ||
| 101 | .as_ref() | ||
| 102 | .map(|s| s.phase != GamePhase::WaitingToStart) | ||
| 103 | .unwrap_or(false) | ||
| 104 | } | ||
| 105 | |||
| 106 | pub fn end_game(&mut self) { | ||
| 107 | self.current_session = None; | ||
| 108 | self.pending_game_type = None; | ||
| 109 | } | ||
| 110 | |||
| 111 | pub fn adjust_bet(&mut self, delta: i64) -> Result<u64, GameError> { | ||
| 112 | if let Some(ref session) = self.current_session { | ||
| 113 | if session.phase != GamePhase::WaitingToStart { | ||
| 114 | return Err(GameError::InvalidAction); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | if let Some(ref mut session) = self.current_session { | ||
| 119 | let current_bet = session.bet_amount as i64; | ||
| 120 | let new_bet = (current_bet + delta).max(10) as u64; | ||
| 121 | session.bet_amount = new_bet; | ||
| 122 | Ok(new_bet) | ||
| 123 | } else { | ||
| 124 | Err(GameError::InvalidAction) | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
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 @@ | |||
| 1 | pub mod manager; | ||
| 2 | pub mod slots; | ||
| 3 | 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 @@ | |||
| 1 | use crate::games::traits::{Game, GameState}; | ||
| 2 | |||
| 3 | pub struct SlotsGame; | ||
| 4 | |||
| 5 | impl SlotsGame { | ||
| 6 | pub fn new() -> Self { | ||
| 7 | Self | ||
| 8 | } | ||
| 9 | |||
| 10 | fn render_reels(&self, reels: &str) -> String { | ||
| 11 | if reels.is_empty() { | ||
| 12 | "[ ? ] [ ? ] [ ? ]".to_string() | ||
| 13 | } else { | ||
| 14 | let chars: Vec<char> = reels.chars().collect(); | ||
| 15 | format!( | ||
| 16 | "[ {} ] [ {} ] [ {} ]\r", | ||
| 17 | chars.get(0).unwrap_or(&'?'), | ||
| 18 | chars.get(1).unwrap_or(&'?'), | ||
| 19 | chars.get(2).unwrap_or(&'?') | ||
| 20 | ) | ||
| 21 | } | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | impl Game for SlotsGame { | ||
| 26 | fn get_name(&self) -> &'static str { | ||
| 27 | "Slots" | ||
| 28 | } | ||
| 29 | |||
| 30 | fn render(&self, state: &GameState) -> String { | ||
| 31 | let reels = state.data.get("reels").cloned().unwrap_or_default(); | ||
| 32 | |||
| 33 | let mut output = format!("\n=== SLOTS ===\r\n"); | ||
| 34 | output += &format!("Bet: {} PLN\r\n", state.bet_amount); | ||
| 35 | output += &format!("{}\r\n", self.render_reels(&reels)); | ||
| 36 | output += "\nPress [SPACE] to spin!\r\n"; | ||
| 37 | output += "\nControls: [+/-] Adjust Bet [Q] Quit\r\n"; | ||
| 38 | output | ||
| 39 | } | ||
| 40 | |||
| 41 | fn render_lines(&self, state: &GameState) -> usize { | ||
| 42 | self.render(state).lines().count() | ||
| 43 | } | ||
| 44 | |||
| 45 | fn get_help(&self) -> String { | ||
| 46 | "SLOTS GAME\n\ | ||
| 47 | =========\n\ | ||
| 48 | Match symbols to win!\n\ | ||
| 49 | \n\ | ||
| 50 | Payouts:\n\ | ||
| 51 | Three of a kind: 5x-100x bet\n\ | ||
| 52 | Two of a kind: 2x-20x bet\n\ | ||
| 53 | \n\ | ||
| 54 | Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ | ||
| 55 | \n\ | ||
| 56 | Controls:\n\ | ||
| 57 | SPACE - Spin the reels\n\ | ||
| 58 | +/- - Adjust bet amount\n\ | ||
| 59 | Q - Exit the game" | ||
| 60 | .to_string() | ||
| 61 | } | ||
| 62 | } | ||
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 @@ | |||
| 1 | use std::collections::HashMap; | ||
| 2 | |||
| 3 | #[derive(Debug, Clone)] | ||
| 4 | pub enum GameError { | ||
| 5 | InvalidAction, | ||
| 6 | NetworkError, | ||
| 7 | } | ||
| 8 | |||
| 9 | #[derive(Debug, Clone, PartialEq)] | ||
| 10 | pub enum GamePhase { | ||
| 11 | WaitingToStart, | ||
| 12 | } | ||
| 13 | |||
| 14 | #[derive(Debug, Clone)] | ||
| 15 | pub struct GameState { | ||
| 16 | pub game_type: String, | ||
| 17 | pub bet_amount: u64, | ||
| 18 | pub phase: GamePhase, | ||
| 19 | pub data: HashMap<String, String>, | ||
| 20 | } | ||
| 21 | |||
| 22 | pub trait Game: Send + Sync { | ||
| 23 | fn get_name(&self) -> &'static str; | ||
| 24 | |||
| 25 | fn render(&self, state: &GameState) -> String; | ||
| 26 | |||
| 27 | fn render_lines(&self, state: &GameState) -> usize; | ||
| 28 | |||
| 29 | fn get_help(&self) -> String; | ||
| 30 | } | ||
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 { | |||
| 14 | ($($arg:tt)*) => { | 14 | ($($arg:tt)*) => { |
| 15 | print!("\r{}", format_args!($($arg)*)) | 15 | print!("\r{}", format_args!($($arg)*)) |
| 16 | }; | 16 | }; |
| 17 | } \ No newline at end of file | 17 | } |
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 @@ | |||
| 1 | #[macro_use] | 1 | #[macro_use] |
| 2 | mod macros; | 2 | mod macros; |
| 3 | mod repl; | ||
| 4 | mod database; | ||
| 5 | mod connection; | 3 | mod connection; |
| 4 | mod database; | ||
| 5 | mod games; | ||
| 6 | mod repl; | ||
| 6 | 7 | ||
| 7 | use repl::ClientState; | 8 | use repl::ClientState; |
| 8 | 9 | ||
| 9 | fn main() { | 10 | fn main() { |
| 11 | let args: Vec<String> = std::env::args().collect(); | ||
| 12 | let unsafe_mode = args.iter().any(|arg| arg == "--unsafe"); | ||
| 13 | |||
| 14 | if unsafe_mode { | ||
| 15 | println!("WARNING: Running in UNSAFE mode - certificate verification disabled!"); | ||
| 16 | } | ||
| 17 | |||
| 18 | connection::set_unsafe_mode(unsafe_mode); | ||
| 19 | |||
| 10 | let mut state = ClientState::new(); | 20 | let mut state = ClientState::new(); |
| 11 | if let Some(host) = state.addr.clone() { | 21 | if let Some(host) = state.addr.clone() { |
| 12 | state.connect_to(&host); | 22 | 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 @@ | |||
| 1 | pub mod database; | 1 | pub mod database; |
| 2 | pub mod connection; | 2 | mod connection; |
| 3 | mod games; | ||
| 3 | pub mod platform; | 4 | pub mod platform; |
| 4 | pub mod repl; | 5 | 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 @@ | |||
| 1 | use std::io::{BufRead, BufReader, Write, stdout}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | use rusqlite::Connection; | 1 | use rusqlite::Connection; |
| 2 | use std::io::{stdout, BufRead, BufReader, Write}; | ||
| 3 | use std::sync::{Arc, Mutex}; | ||
| 4 | use std::time::Instant; | 4 | use std::time::Instant; |
| 5 | 5 | ||
| 6 | use crate::connection; | 6 | use crate::connection::TlsStream; |
| 7 | use crate::database; | 7 | use crate::database; |
| 8 | use crate::games::manager::GameManager; | ||
| 8 | 9 | ||
| 9 | use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show, Hide}, execute}; | 10 | use crossterm::{ |
| 11 | cursor::{Hide, MoveToColumn, Show}, | ||
| 12 | event::{read, Event, KeyCode, KeyModifiers}, | ||
| 13 | execute, | ||
| 14 | terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, | ||
| 15 | }; | ||
| 10 | 16 | ||
| 11 | use sha2::{Sha256, Digest}; | 17 | use sha2::{Digest, Sha256}; |
| 12 | 18 | ||
| 13 | pub struct ClientState { | 19 | pub struct ClientState { |
| 14 | pub stream: Option<TcpStream>, | 20 | pub stream: Option<TlsStream>, |
| 15 | pub conn: Connection, | 21 | pub conn: Connection, |
| 16 | pub addr: Option<String>, | 22 | pub addr: Option<String>, |
| 17 | pub default_port: u16, | 23 | pub default_port: u16, |
| 18 | pub history: Vec<String>, | 24 | pub history: Vec<String>, |
| 19 | pub history_index: usize, | 25 | pub history_index: usize, |
| 20 | pub logged_in_user: Option<String>, | 26 | pub logged_in_user: Option<String>, |
| 27 | pub game_manager: GameManager, | ||
| 28 | cleanup_done: Arc<Mutex<bool>>, | ||
| 29 | in_game_mode: bool, | ||
| 21 | } | 30 | } |
| 22 | 31 | ||
| 23 | impl ClientState { | 32 | impl ClientState { |
| @@ -37,12 +46,86 @@ impl ClientState { | |||
| 37 | history: vec![], | 46 | history: vec![], |
| 38 | history_index: 0, | 47 | history_index: 0, |
| 39 | logged_in_user: None, | 48 | logged_in_user: None, |
| 49 | game_manager: GameManager::new(), | ||
| 50 | cleanup_done: Arc::new(Mutex::new(false)), | ||
| 51 | in_game_mode: false, | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | pub fn enter_game_mode(&mut self) { | ||
| 56 | self.in_game_mode = true; | ||
| 57 | } | ||
| 58 | |||
| 59 | pub fn exit_game_mode(&mut self) { | ||
| 60 | self.in_game_mode = false; | ||
| 61 | self.game_manager.end_game(); | ||
| 62 | } | ||
| 63 | |||
| 64 | pub fn handle_game_key(&mut self, key_code: KeyCode) -> bool { | ||
| 65 | if !self.game_manager.is_in_game() { | ||
| 66 | return false; | ||
| 67 | } | ||
| 68 | |||
| 69 | match key_code { | ||
| 70 | KeyCode::Char('q') => { | ||
| 71 | self.exit_game_mode(); | ||
| 72 | println!("\nGame exited."); | ||
| 73 | return true; | ||
| 74 | } | ||
| 75 | KeyCode::Char(' ') => { | ||
| 76 | let should_start = !self.game_manager.is_game_started(); | ||
| 77 | if should_start { | ||
| 78 | if let Some(mut stream) = self.stream.take() { | ||
| 79 | match self.game_manager.start_game(&mut stream) { | ||
| 80 | Ok(()) => { | ||
| 81 | self.play_spin_animation(&mut stream); | ||
| 82 | } | ||
| 83 | Err(e) => println!("\nError starting game: {:?}", e), | ||
| 84 | } | ||
| 85 | self.stream = Some(stream); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | return true; | ||
| 89 | } | ||
| 90 | KeyCode::Char('+') => { | ||
| 91 | // Increase bet | ||
| 92 | match self.game_manager.adjust_bet(10) { | ||
| 93 | Ok(_new_bet) => { | ||
| 94 | if let Some(state) = self.game_manager.get_current_state() { | ||
| 95 | if let Some(game) = self.game_manager.get_current_game() { | ||
| 96 | let lines = | ||
| 97 | self.game_manager.get_current_render_lines().unwrap_or(8); | ||
| 98 | redraw_game_lines(&game.render(state), lines); | ||
| 99 | } | ||
| 100 | } | ||
| 101 | } | ||
| 102 | Err(e) => println!("\nError: {:?}", e), | ||
| 103 | } | ||
| 104 | return true; | ||
| 105 | } | ||
| 106 | KeyCode::Char('-') => { | ||
| 107 | // Decrease bet | ||
| 108 | match self.game_manager.adjust_bet(-10) { | ||
| 109 | Ok(_new_bet) => { | ||
| 110 | if let Some(state) = self.game_manager.get_current_state() { | ||
| 111 | if let Some(game) = self.game_manager.get_current_game() { | ||
| 112 | let lines = | ||
| 113 | self.game_manager.get_current_render_lines().unwrap_or(8); | ||
| 114 | redraw_game_lines(&game.render(state), lines); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | Err(e) => println!("\nError: {:?}", e), | ||
| 119 | } | ||
| 120 | return true; | ||
| 121 | } | ||
| 122 | _ => return false, | ||
| 40 | } | 123 | } |
| 41 | } | 124 | } |
| 42 | 125 | ||
| 43 | pub fn connect_to(&mut self, addr: &str) { | 126 | pub fn connect_to(&mut self, addr: &str) { |
| 44 | self.addr = Some(addr.to_string()); | 127 | self.addr = Some(addr.to_string()); |
| 45 | match connection::connect(addr, &self.conn) { | 128 | match crate::connection::connect(addr, &self.conn) { |
| 46 | Ok((s, authed_user)) => { | 129 | Ok((s, authed_user)) => { |
| 47 | self.stream = Some(s); | 130 | self.stream = Some(s); |
| 48 | self.logged_in_user = authed_user; | 131 | self.logged_in_user = authed_user; |
| @@ -53,6 +136,24 @@ impl ClientState { | |||
| 53 | } | 136 | } |
| 54 | 137 | ||
| 55 | pub fn run(&mut self) { | 138 | pub fn run(&mut self) { |
| 139 | // Set up cleanup handlers | ||
| 140 | let cleanup_done = self.cleanup_done.clone(); | ||
| 141 | |||
| 142 | // Set up panic handler | ||
| 143 | let panic_cleanup = cleanup_done.clone(); | ||
| 144 | std::panic::set_hook(Box::new(move |_| { | ||
| 145 | restore_terminal(&panic_cleanup); | ||
| 146 | })); | ||
| 147 | |||
| 148 | // Set up Ctrl+C handler | ||
| 149 | let ctrlc_cleanup = cleanup_done.clone(); | ||
| 150 | if let Err(e) = ctrlc::set_handler(move || { | ||
| 151 | restore_terminal(&ctrlc_cleanup); | ||
| 152 | std::process::exit(0); | ||
| 153 | }) { | ||
| 154 | eprintln!("Warning: Could not set Ctrl+C handler: {}", e); | ||
| 155 | } | ||
| 156 | |||
| 56 | enable_raw_mode().unwrap(); | 157 | enable_raw_mode().unwrap(); |
| 57 | 'outer: loop { | 158 | 'outer: loop { |
| 58 | let mut current_input = String::new(); | 159 | let mut current_input = String::new(); |
| @@ -64,308 +165,446 @@ impl ClientState { | |||
| 64 | Event::Key(key) => { | 165 | Event::Key(key) => { |
| 65 | // Only handle key press events, not release events | 166 | // Only handle key press events, not release events |
| 66 | if key.kind == crossterm::event::KeyEventKind::Press { | 167 | if key.kind == crossterm::event::KeyEventKind::Press { |
| 67 | match key.code { | 168 | // Handle game mode keys first |
| 68 | KeyCode::Up => { | 169 | if self.game_manager.is_in_game() && self.handle_game_key(key.code) { |
| 69 | if self.history_index > 0 { | 170 | // Game key was handled, continue loop |
| 70 | self.history_index -= 1; | 171 | if !self.game_manager.is_in_game() { |
| 71 | current_input = self.history[self.history_index].clone(); | 172 | // Game ended, redraw command prompt |
| 72 | redraw(¤t_input); | 173 | redraw(""); |
| 73 | } | ||
| 74 | } | ||
| 75 | KeyCode::Down => { | ||
| 76 | if self.history_index < self.history.len() { | ||
| 77 | self.history_index += 1; | ||
| 78 | current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() }; | ||
| 79 | redraw(¤t_input); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | KeyCode::Enter => { | ||
| 83 | let line = current_input.trim().to_string(); | ||
| 84 | if !line.is_empty() { | ||
| 85 | self.history.push(line.clone()); | ||
| 86 | } | ||
| 87 | // Process command | ||
| 88 | let parts: Vec<&str> = line.splitn(2, ' ').collect(); | ||
| 89 | match parts[0] { | ||
| 90 | "connect" => { | ||
| 91 | let addr_str = if parts.len() < 2 { | ||
| 92 | if let Some(h) = &self.addr { | ||
| 93 | h.clone() | ||
| 94 | } else { | ||
| 95 | println!("\nNo host specified and no previous connection. Usage: connect <ip>:<port>"); | ||
| 96 | continue 'outer; | ||
| 97 | } | ||
| 98 | } else { | ||
| 99 | let mut a = parts[1].to_string(); | ||
| 100 | if !a.contains(':') { | ||
| 101 | a = format!("{}:{}", a, self.default_port); | ||
| 102 | } | ||
| 103 | a | ||
| 104 | }; | ||
| 105 | self.connect_to(&addr_str); | ||
| 106 | } | 174 | } |
| 107 | "send" => { | 175 | continue; |
| 108 | if self.stream.is_none() { | 176 | } |
| 109 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | 177 | |
| 110 | continue 'outer; | 178 | // Handle normal command mode keys |
| 111 | } | 179 | match key.code { |
| 112 | if parts.len() < 2 { | 180 | KeyCode::Up => { |
| 113 | println!("\nUsage: send <message>"); | 181 | if self.history_index > 0 { |
| 114 | continue 'outer; | 182 | self.history_index -= 1; |
| 115 | } | 183 | current_input = self.history[self.history_index].clone(); |
| 116 | let msg = parts[1]; | 184 | redraw(¤t_input); |
| 117 | let s = self.stream.as_mut().unwrap(); | ||
| 118 | let writer_stream = s.try_clone().unwrap(); | ||
| 119 | let mut writer = writer_stream; | ||
| 120 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 121 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 122 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 123 | self.stream = None; | ||
| 124 | println!("\nConnection lost"); | ||
| 125 | continue 'outer; | ||
| 126 | } | ||
| 127 | let mut response = String::new(); | ||
| 128 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 129 | self.stream = None; | ||
| 130 | println!("\nConnection lost"); | ||
| 131 | } else { | ||
| 132 | println!("\nEcho: {}", response.trim()); | ||
| 133 | } | 185 | } |
| 134 | } | 186 | } |
| 135 | "disconnect" => { | 187 | KeyCode::Down => { |
| 136 | self.stream = None; | 188 | if self.history_index < self.history.len() { |
| 137 | self.logged_in_user = None; | 189 | self.history_index += 1; |
| 138 | println!("\nDisconnected"); | 190 | current_input = if self.history_index == self.history.len() |
| 139 | } | 191 | { |
| 140 | "ping" => { | 192 | "".to_string() |
| 141 | if self.stream.is_none() { | ||
| 142 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 143 | continue 'outer; | ||
| 144 | } | ||
| 145 | let s = self.stream.as_mut().unwrap(); | ||
| 146 | let writer_stream = s.try_clone().unwrap(); | ||
| 147 | let mut writer = writer_stream; | ||
| 148 | let start = Instant::now(); | ||
| 149 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 150 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 151 | if let Err(_) = writeln!(writer, "PING") { | ||
| 152 | self.stream = None; | ||
| 153 | println!("\nConnection lost"); | ||
| 154 | continue 'outer; | ||
| 155 | } | ||
| 156 | let mut response = String::new(); | ||
| 157 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 158 | self.stream = None; | ||
| 159 | println!("\nConnection lost"); | ||
| 160 | } else { | ||
| 161 | if response.trim() == "PONG" { | ||
| 162 | let elapsed = start.elapsed(); | ||
| 163 | println!("\nPong ({:?})", elapsed); | ||
| 164 | } else { | 193 | } else { |
| 165 | self.stream = None; | 194 | self.history[self.history_index].clone() |
| 166 | println!("\nConnection lost"); | 195 | }; |
| 167 | } | 196 | redraw(¤t_input); |
| 168 | } | 197 | } |
| 169 | } | 198 | } |
| 170 | "register" => { | 199 | KeyCode::Enter => { |
| 171 | if self.stream.is_none() { | 200 | let line = current_input.trim().to_string(); |
| 172 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | 201 | if !line.is_empty() { |
| 173 | continue 'outer; | 202 | self.history.push(line.clone()); |
| 174 | } | ||
| 175 | let username = read_line("\nUsername: "); | ||
| 176 | if username.is_empty() { | ||
| 177 | continue 'outer; | ||
| 178 | } | 203 | } |
| 179 | let password = read_password("Password: "); | 204 | let line_trimmed = current_input.trim().to_string(); |
| 180 | if password.is_empty() { | 205 | if !line_trimmed.is_empty() { |
| 181 | continue 'outer; | 206 | self.history.push(line_trimmed.clone()); |
| 182 | } | 207 | } |
| 183 | let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); | 208 | |
| 184 | let msg = format!("REGISTER {} {}", username, hashed); | 209 | // Process normal commands |
| 185 | let s = self.stream.as_mut().unwrap(); | 210 | let parts: Vec<&str> = line_trimmed.splitn(3, ' ').collect(); |
| 186 | let writer_stream = s.try_clone().unwrap(); | 211 | match parts[0] { |
| 187 | let mut writer = writer_stream; | 212 | "play" => { |
| 188 | let response_reader_stream = s.try_clone().unwrap(); | 213 | if self.stream.is_none() { |
| 189 | let mut response_reader = BufReader::new(response_reader_stream); | 214 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); |
| 190 | if let Err(_) = writeln!(writer, "{}", msg) { | 215 | continue 'outer; |
| 191 | self.stream = None; | 216 | } |
| 192 | println!("\nConnection lost"); | 217 | if self.logged_in_user.is_none() { |
| 193 | continue 'outer; | 218 | println!("\nNot logged in. Use 'login' first."); |
| 194 | } | 219 | continue 'outer; |
| 195 | let mut response = String::new(); | 220 | } |
| 196 | if let Err(_) = response_reader.read_line(&mut response) { | 221 | if parts.len() < 2 { |
| 197 | self.stream = None; | 222 | let games = self.game_manager.get_available_games(); |
| 198 | println!("\nConnection lost"); | 223 | println!("\nAvailable games:"); |
| 199 | } else { | 224 | for (gt, name) in games { |
| 200 | let resp = response.trim(); | 225 | println!(" - {} ({})", gt, name); |
| 201 | if resp == format!("REGISTERED {}", username) { | 226 | } |
| 202 | println!("\nSuccessfully registered as {}", username); | 227 | println!("\nUsage: play <game>"); |
| 203 | } else if resp.starts_with("ERROR ") { | 228 | continue 'outer; |
| 204 | println!("\nRegistration failed: {}", &resp[6..]); | 229 | } |
| 205 | } else { | 230 | let game_type = parts[1]; |
| 206 | println!("\n{}", resp); | 231 | |
| 232 | if self.game_manager.is_in_game() { | ||
| 233 | println!("\nAlready in a game. Use 'quit' to exit current game."); | ||
| 234 | continue 'outer; | ||
| 235 | } | ||
| 236 | |||
| 237 | match self.game_manager.enter_game(game_type) { | ||
| 238 | Ok(()) => { | ||
| 239 | self.enter_game_mode(); | ||
| 240 | if let Some(game) = | ||
| 241 | self.game_manager.get_current_game() | ||
| 242 | { | ||
| 243 | if let Some(state) = | ||
| 244 | self.game_manager.get_current_state() | ||
| 245 | { | ||
| 246 | redraw_game(&game.render(state)); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | } | ||
| 250 | Err(e) => { | ||
| 251 | println!("\nFailed to enter game: {:?}", e); | ||
| 252 | } | ||
| 253 | } | ||
| 207 | } | 254 | } |
| 208 | } | 255 | "games" => { |
| 209 | } | 256 | let games = self.game_manager.get_available_games(); |
| 210 | "login" => { | 257 | println!("\nAvailable games:"); |
| 211 | if self.stream.is_none() { | 258 | for (gt, name) in games { |
| 212 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | 259 | println!(" - {} ({})", gt, name); |
| 213 | continue 'outer; | 260 | } |
| 214 | } | ||
| 215 | if self.logged_in_user.is_some() { | ||
| 216 | println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap()); | ||
| 217 | continue 'outer; | ||
| 218 | } | ||
| 219 | loop { | ||
| 220 | let username = read_line("\nUsername: "); | ||
| 221 | if username.is_empty() { | ||
| 222 | break; | ||
| 223 | } | 261 | } |
| 224 | let password = read_password("Password: "); | 262 | "connect" => { |
| 225 | if password.is_empty() { | 263 | let addr_str = if parts.len() < 2 { |
| 226 | break; | 264 | if let Some(h) = &self.addr { |
| 265 | h.clone() | ||
| 266 | } else { | ||
| 267 | println!("\nNo host specified and no previous connection. Usage: connect <ip>:<port>"); | ||
| 268 | continue 'outer; | ||
| 269 | } | ||
| 270 | } else { | ||
| 271 | let mut a = parts[1].to_string(); | ||
| 272 | if !a.contains(':') { | ||
| 273 | a = format!("{}:{}", a, self.default_port); | ||
| 274 | } | ||
| 275 | a | ||
| 276 | }; | ||
| 277 | self.connect_to(&addr_str); | ||
| 227 | } | 278 | } |
| 228 | let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); | 279 | "send" => { |
| 229 | let msg = format!("AUTH {} {}", username, hashed); | 280 | if self.stream.is_none() { |
| 230 | let s = self.stream.as_mut().unwrap(); | 281 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); |
| 231 | let writer_stream = s.try_clone().unwrap(); | 282 | continue 'outer; |
| 232 | let mut writer = writer_stream; | 283 | } |
| 233 | let response_reader_stream = s.try_clone().unwrap(); | 284 | if parts.len() < 2 { |
| 234 | let mut response_reader = BufReader::new(response_reader_stream); | 285 | println!("\nUsage: send <message>"); |
| 235 | if let Err(_) = writeln!(writer, "{}", msg) { | 286 | continue 'outer; |
| 236 | self.stream = None; | 287 | } |
| 237 | println!("\nConnection lost"); | 288 | let msg = parts[1]; |
| 238 | break; | 289 | let s = self.stream.as_mut().unwrap(); |
| 290 | let writer_stream = s.try_clone().unwrap(); | ||
| 291 | let mut writer = writer_stream; | ||
| 292 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 293 | let mut response_reader = | ||
| 294 | BufReader::new(response_reader_stream); | ||
| 295 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 296 | self.stream = None; | ||
| 297 | println!("\nConnection lost"); | ||
| 298 | continue 'outer; | ||
| 299 | } | ||
| 300 | let mut response = String::new(); | ||
| 301 | if let Err(_) = response_reader.read_line(&mut response) | ||
| 302 | { | ||
| 303 | self.stream = None; | ||
| 304 | println!("\nConnection lost"); | ||
| 305 | } else { | ||
| 306 | println!("\nEcho: {}", response.trim()); | ||
| 307 | } | ||
| 239 | } | 308 | } |
| 240 | let mut response = String::new(); | 309 | "disconnect" => { |
| 241 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 242 | self.stream = None; | 310 | self.stream = None; |
| 243 | println!("\nConnection lost"); | 311 | self.logged_in_user = None; |
| 244 | break; | 312 | println!("\nDisconnected"); |
| 245 | } else { | 313 | } |
| 246 | let resp = response.trim(); | 314 | "ping" => { |
| 247 | if resp.starts_with("OK") { | 315 | if self.stream.is_none() { |
| 248 | // Save credentials | 316 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); |
| 249 | if let Some(addr) = &self.addr { | 317 | continue 'outer; |
| 250 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { | 318 | } |
| 251 | println!("Failed to save credentials: {}", e); | 319 | let s = self.stream.as_mut().unwrap(); |
| 320 | let writer_stream = s.try_clone().unwrap(); | ||
| 321 | let mut writer = writer_stream; | ||
| 322 | let start = Instant::now(); | ||
| 323 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 324 | let mut response_reader = | ||
| 325 | BufReader::new(response_reader_stream); | ||
| 326 | if let Err(_) = writeln!(writer, "PING") { | ||
| 327 | self.stream = None; | ||
| 328 | println!("\nConnection lost"); | ||
| 329 | continue 'outer; | ||
| 330 | } | ||
| 331 | let mut response = String::new(); | ||
| 332 | if let Err(_) = response_reader.read_line(&mut response) | ||
| 333 | { | ||
| 334 | self.stream = None; | ||
| 335 | println!("\nConnection lost"); | ||
| 336 | } else { | ||
| 337 | if response.trim() == "PONG" { | ||
| 338 | let elapsed = start.elapsed(); | ||
| 339 | println!("\nPong ({:?})", elapsed); | ||
| 340 | } else { | ||
| 341 | self.stream = None; | ||
| 342 | println!("\nConnection lost"); | ||
| 343 | } | ||
| 344 | } | ||
| 345 | } | ||
| 346 | "register" => { | ||
| 347 | if self.stream.is_none() { | ||
| 348 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 349 | continue 'outer; | ||
| 350 | } | ||
| 351 | let username = read_line("\nUsername: "); | ||
| 352 | if username.is_empty() { | ||
| 353 | continue 'outer; | ||
| 354 | } | ||
| 355 | let password = read_password("Password: "); | ||
| 356 | if password.is_empty() { | ||
| 357 | continue 'outer; | ||
| 358 | } | ||
| 359 | let hashed = format!( | ||
| 360 | "{:x}", | ||
| 361 | Sha256::digest(password.as_bytes()) | ||
| 362 | ); | ||
| 363 | let msg = format!("REGISTER {} {}", username, hashed); | ||
| 364 | let s = self.stream.as_mut().unwrap(); | ||
| 365 | let writer_stream = s.try_clone().unwrap(); | ||
| 366 | let mut writer = writer_stream; | ||
| 367 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 368 | let mut response_reader = | ||
| 369 | BufReader::new(response_reader_stream); | ||
| 370 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 371 | self.stream = None; | ||
| 372 | println!("\nConnection lost"); | ||
| 373 | continue 'outer; | ||
| 374 | } | ||
| 375 | let mut response = String::new(); | ||
| 376 | if let Err(_) = response_reader.read_line(&mut response) | ||
| 377 | { | ||
| 378 | self.stream = None; | ||
| 379 | println!("\nConnection lost"); | ||
| 380 | } else { | ||
| 381 | let resp = response.trim(); | ||
| 382 | if resp == format!("REGISTERED {}", username) { | ||
| 383 | println!( | ||
| 384 | "\nSuccessfully registered as {}", | ||
| 385 | username | ||
| 386 | ); | ||
| 387 | } else if resp.starts_with("ERROR ") { | ||
| 388 | println!( | ||
| 389 | "\nRegistration failed: {}", | ||
| 390 | &resp[6..] | ||
| 391 | ); | ||
| 392 | } else { | ||
| 393 | println!("\n{}", resp); | ||
| 394 | } | ||
| 395 | } | ||
| 396 | } | ||
| 397 | "login" => { | ||
| 398 | if self.stream.is_none() { | ||
| 399 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 400 | continue 'outer; | ||
| 401 | } | ||
| 402 | if self.logged_in_user.is_some() { | ||
| 403 | println!( | ||
| 404 | "\nAlready logged in as {}", | ||
| 405 | self.logged_in_user.as_ref().unwrap() | ||
| 406 | ); | ||
| 407 | continue 'outer; | ||
| 408 | } | ||
| 409 | loop { | ||
| 410 | let username = read_line("\nUsername: "); | ||
| 411 | if username.is_empty() { | ||
| 412 | break; | ||
| 413 | } | ||
| 414 | let password = read_password("Password: "); | ||
| 415 | if password.is_empty() { | ||
| 416 | break; | ||
| 417 | } | ||
| 418 | let hashed = format!( | ||
| 419 | "{:x}", | ||
| 420 | Sha256::digest(password.as_bytes()) | ||
| 421 | ); | ||
| 422 | let msg = format!("AUTH {} {}", username, hashed); | ||
| 423 | let s = self.stream.as_mut().unwrap(); | ||
| 424 | let writer_stream = s.try_clone().unwrap(); | ||
| 425 | let mut writer = writer_stream; | ||
| 426 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 427 | let mut response_reader = | ||
| 428 | BufReader::new(response_reader_stream); | ||
| 429 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 430 | self.stream = None; | ||
| 431 | println!("\nConnection lost"); | ||
| 432 | break; | ||
| 433 | } | ||
| 434 | let mut response = String::new(); | ||
| 435 | if let Err(_) = | ||
| 436 | response_reader.read_line(&mut response) | ||
| 437 | { | ||
| 438 | self.stream = None; | ||
| 439 | println!("\nConnection lost"); | ||
| 440 | break; | ||
| 441 | } else { | ||
| 442 | let resp = response.trim(); | ||
| 443 | if resp.starts_with("OK") { | ||
| 444 | // Save credentials | ||
| 445 | if let Some(addr) = &self.addr { | ||
| 446 | if let Err(e) = | ||
| 447 | database::save_credentials( | ||
| 448 | &self.conn, addr, &username, | ||
| 449 | &hashed, | ||
| 450 | ) | ||
| 451 | { | ||
| 452 | println!("Failed to save credentials: {}", e); | ||
| 453 | } | ||
| 454 | } | ||
| 455 | self.logged_in_user = | ||
| 456 | Some(username.clone()); | ||
| 457 | println!("\nLogged in as {}", username); | ||
| 458 | break; | ||
| 459 | } else if resp.starts_with("ERROR ") { | ||
| 460 | println!( | ||
| 461 | "\nAuthentication failed: {}", | ||
| 462 | &resp[6..] | ||
| 463 | ); | ||
| 464 | println!( | ||
| 465 | "Try again or press Enter to cancel." | ||
| 466 | ); | ||
| 467 | } else { | ||
| 468 | println!("\n{}", resp); | ||
| 252 | } | 469 | } |
| 253 | } | 470 | } |
| 254 | self.logged_in_user = Some(username.clone()); | 471 | } |
| 255 | println!("\nLogged in as {}", username); | 472 | } |
| 256 | break; | 473 | "setcred" => { |
| 257 | } else if resp.starts_with("ERROR ") { | 474 | if self.addr.is_none() { |
| 258 | println!("\nAuthentication failed: {}", &resp[6..]); | 475 | println!("\nNo host connected. Use 'connect <ip>:<port>' first."); |
| 259 | println!("Try again or press Enter to cancel."); | 476 | continue 'outer; |
| 477 | } | ||
| 478 | let username = read_line("\nUsername: "); | ||
| 479 | if username.is_empty() { | ||
| 480 | continue 'outer; | ||
| 481 | } | ||
| 482 | let password = read_password("Password: "); | ||
| 483 | if password.is_empty() { | ||
| 484 | continue 'outer; | ||
| 485 | } | ||
| 486 | let hashed = format!( | ||
| 487 | "{:x}", | ||
| 488 | Sha256::digest(password.as_bytes()) | ||
| 489 | ); | ||
| 490 | if let Some(addr) = &self.addr { | ||
| 491 | if let Err(e) = database::save_credentials( | ||
| 492 | &self.conn, addr, &username, &hashed, | ||
| 493 | ) { | ||
| 494 | println!("Failed to save credentials: {}", e); | ||
| 495 | } else { | ||
| 496 | println!("Credentials saved for {}", addr); | ||
| 497 | } | ||
| 498 | } | ||
| 499 | } | ||
| 500 | "logout" => { | ||
| 501 | self.logged_in_user = None; | ||
| 502 | println!("\nLogged out"); | ||
| 503 | } | ||
| 504 | "info" => { | ||
| 505 | if self.stream.is_none() { | ||
| 506 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 507 | continue 'outer; | ||
| 508 | } | ||
| 509 | let s = self.stream.as_mut().unwrap(); | ||
| 510 | let writer_stream = s.try_clone().unwrap(); | ||
| 511 | let mut writer = writer_stream; | ||
| 512 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 513 | let mut response_reader = | ||
| 514 | BufReader::new(response_reader_stream); | ||
| 515 | if let Err(_) = writeln!(writer, "INFO") { | ||
| 516 | self.stream = None; | ||
| 517 | println!("\nConnection lost"); | ||
| 518 | continue 'outer; | ||
| 519 | } | ||
| 520 | let mut response = String::new(); | ||
| 521 | if let Err(_) = response_reader.read_line(&mut response) | ||
| 522 | { | ||
| 523 | self.stream = None; | ||
| 524 | println!("\nConnection lost"); | ||
| 260 | } else { | 525 | } else { |
| 261 | println!("\n{}", resp); | 526 | let resp = response.trim(); |
| 527 | if resp.starts_with("INFO ") { | ||
| 528 | let parts: Vec<&str> = | ||
| 529 | resp.split_whitespace().collect(); | ||
| 530 | if parts.len() == 4 { | ||
| 531 | println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); | ||
| 532 | } else { | ||
| 533 | println!("\n{}", resp); | ||
| 534 | } | ||
| 535 | } else if resp.starts_with("ERROR ") { | ||
| 536 | println!("\nError: {}", &resp[6..]); | ||
| 537 | } else { | ||
| 538 | println!("\n{}", resp); | ||
| 539 | } | ||
| 262 | } | 540 | } |
| 263 | } | 541 | } |
| 264 | } | 542 | "help" => { |
| 265 | } | 543 | if self.game_manager.is_in_game() { |
| 266 | "setcred" => { | 544 | if let Some(game) = |
| 267 | if self.addr.is_none() { | 545 | self.game_manager.get_current_game() |
| 268 | println!("\nNo host connected. Use 'connect <ip>:<port>' first."); | 546 | { |
| 269 | continue 'outer; | 547 | println!("\n{}", game.get_help()); |
| 270 | } | 548 | } |
| 271 | let username = read_line("\nUsername: "); | 549 | } else { |
| 272 | if username.is_empty() { | 550 | help(); |
| 273 | continue 'outer; | 551 | } |
| 274 | } | ||
| 275 | let password = read_password("Password: "); | ||
| 276 | if password.is_empty() { | ||
| 277 | continue 'outer; | ||
| 278 | } | ||
| 279 | let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); | ||
| 280 | if let Some(addr) = &self.addr { | ||
| 281 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { | ||
| 282 | println!("Failed to save credentials: {}", e); | ||
| 283 | } else { | ||
| 284 | println!("Credentials saved for {}", addr); | ||
| 285 | } | 552 | } |
| 553 | "quit" => { | ||
| 554 | if self.game_manager.is_in_game() { | ||
| 555 | self.game_manager.end_game(); | ||
| 556 | println!("\nGame exited."); | ||
| 557 | } else { | ||
| 558 | restore_terminal(&self.cleanup_done); | ||
| 559 | break 'outer; | ||
| 560 | } | ||
| 561 | } | ||
| 562 | _ => help(), | ||
| 286 | } | 563 | } |
| 564 | break; | ||
| 287 | } | 565 | } |
| 288 | "logout" => { | 566 | KeyCode::Backspace => { |
| 289 | self.logged_in_user = None; | 567 | if !current_input.is_empty() { |
| 290 | println!("\nLogged out"); | 568 | current_input.pop(); |
| 291 | } | 569 | execute!( |
| 292 | "info" => { | 570 | stdout(), |
| 293 | if self.stream.is_none() { | 571 | MoveToColumn(2), |
| 294 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | 572 | Clear(ClearType::UntilNewLine) |
| 295 | continue 'outer; | 573 | ) |
| 296 | } | 574 | .unwrap(); |
| 297 | let s = self.stream.as_mut().unwrap(); | 575 | print!("{}", current_input); |
| 298 | let writer_stream = s.try_clone().unwrap(); | 576 | stdout().flush().unwrap(); |
| 299 | let mut writer = writer_stream; | ||
| 300 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 301 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 302 | if let Err(_) = writeln!(writer, "INFO") { | ||
| 303 | self.stream = None; | ||
| 304 | println!("\nConnection lost"); | ||
| 305 | continue 'outer; | ||
| 306 | } | 577 | } |
| 307 | let mut response = String::new(); | 578 | } |
| 308 | if let Err(_) = response_reader.read_line(&mut response) { | 579 | KeyCode::Char(c) => { |
| 309 | self.stream = None; | 580 | if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { |
| 310 | println!("\nConnection lost"); | 581 | restore_terminal(&self.cleanup_done); |
| 582 | std::process::exit(0); | ||
| 583 | } else if key.modifiers.contains(KeyModifiers::CONTROL) | ||
| 584 | && c == 'd' | ||
| 585 | { | ||
| 586 | restore_terminal(&self.cleanup_done); | ||
| 587 | std::process::exit(0); | ||
| 588 | } else if c == '\t' { | ||
| 589 | self.handle_tab_completion(&mut current_input); | ||
| 311 | } else { | 590 | } else { |
| 312 | let resp = response.trim(); | 591 | current_input.push(c); |
| 313 | if resp.starts_with("INFO ") { | 592 | print!("{}", c); |
| 314 | let parts: Vec<&str> = resp.split_whitespace().collect(); | 593 | stdout().flush().unwrap(); |
| 315 | if parts.len() == 4 { | ||
| 316 | println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); | ||
| 317 | } else { | ||
| 318 | println!("\n{}", resp); | ||
| 319 | } | ||
| 320 | } else if resp.starts_with("ERROR ") { | ||
| 321 | println!("\nError: {}", &resp[6..]); | ||
| 322 | } else { | ||
| 323 | println!("\n{}", resp); | ||
| 324 | } | ||
| 325 | } | 594 | } |
| 326 | } | 595 | } |
| 327 | "help" => { | 596 | KeyCode::Tab => { |
| 328 | help(); | 597 | self.handle_tab_completion(&mut current_input); |
| 329 | } | 598 | } |
| 330 | "quit" => break 'outer, | 599 | _ => {} |
| 331 | _ => help(), | ||
| 332 | } | ||
| 333 | break; | ||
| 334 | } | ||
| 335 | KeyCode::Backspace => { | ||
| 336 | if !current_input.is_empty() { | ||
| 337 | current_input.pop(); | ||
| 338 | execute!(stdout(), MoveToColumn(2), | ||
| 339 | Clear(ClearType::UntilNewLine)).unwrap(); | ||
| 340 | print!("{}", current_input); | ||
| 341 | stdout().flush().unwrap(); | ||
| 342 | } | 600 | } |
| 343 | } | 601 | } |
| 344 | KeyCode::Char(c) => { | ||
| 345 | if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { | ||
| 346 | execute!(stdout(), Show).unwrap(); | ||
| 347 | return; | ||
| 348 | } else if c == '\t' { | ||
| 349 | self.handle_tab_completion(&mut current_input); | ||
| 350 | } else { | ||
| 351 | current_input.push(c); | ||
| 352 | print!("{}", c); | ||
| 353 | stdout().flush().unwrap(); | ||
| 354 | } | ||
| 355 | } | ||
| 356 | KeyCode::Tab => { | ||
| 357 | self.handle_tab_completion(&mut current_input); | ||
| 358 | } | ||
| 359 | _ => {} | ||
| 360 | } | ||
| 361 | } | 602 | } |
| 362 | } | ||
| 363 | _ => {} | 603 | _ => {} |
| 364 | } | 604 | } |
| 365 | } | 605 | } |
| 366 | } | 606 | } |
| 367 | disable_raw_mode().unwrap(); | 607 | restore_terminal(&self.cleanup_done); |
| 368 | execute!(stdout(), Show).unwrap(); | ||
| 369 | } | 608 | } |
| 370 | 609 | ||
| 371 | fn handle_tab_completion(&mut self, current_input: &mut String) { | 610 | fn handle_tab_completion(&mut self, current_input: &mut String) { |
| @@ -387,6 +626,77 @@ impl ClientState { | |||
| 387 | } | 626 | } |
| 388 | // else do nothing | 627 | // else do nothing |
| 389 | } | 628 | } |
| 629 | |||
| 630 | fn play_spin_animation(&mut self, stream: &mut TlsStream) { | ||
| 631 | use std::io::Write; | ||
| 632 | use std::thread; | ||
| 633 | use std::time::Duration; | ||
| 634 | |||
| 635 | let symbols = ['C', 'L', 'O', 'G', 'D', 'S']; | ||
| 636 | let frames = 20; | ||
| 637 | let delay = Duration::from_millis(50); | ||
| 638 | |||
| 639 | for _ in 0..frames { | ||
| 640 | let reels: String = (0..3) | ||
| 641 | .map(|_| symbols[rand::random::<usize>() % symbols.len()]) | ||
| 642 | .collect(); | ||
| 643 | |||
| 644 | execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); | ||
| 645 | print!( | ||
| 646 | "[ {} ] [ {} ] [ {} ]", | ||
| 647 | reels.chars().nth(0).unwrap(), | ||
| 648 | reels.chars().nth(1).unwrap(), | ||
| 649 | reels.chars().nth(2).unwrap() | ||
| 650 | ); | ||
| 651 | stdout().flush().unwrap(); | ||
| 652 | |||
| 653 | thread::sleep(delay); | ||
| 654 | } | ||
| 655 | |||
| 656 | let mut response_str = String::new(); | ||
| 657 | let mut reader = std::io::BufReader::new(stream.try_clone().unwrap()); | ||
| 658 | if reader.read_line(&mut response_str).is_ok() { | ||
| 659 | let response = response_str.trim(); | ||
| 660 | |||
| 661 | if response.starts_with("GAME_RESULT") { | ||
| 662 | let parts: Vec<&str> = response.split_whitespace().collect(); | ||
| 663 | if parts.len() >= 7 { | ||
| 664 | let reels = parts[5]; | ||
| 665 | let win_type = parts[6]; | ||
| 666 | |||
| 667 | execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); | ||
| 668 | print!( | ||
| 669 | "[ {} ] [ {} ] [ {} ]", | ||
| 670 | reels.chars().nth(0).unwrap_or('?'), | ||
| 671 | reels.chars().nth(1).unwrap_or('?'), | ||
| 672 | reels.chars().nth(2).unwrap_or('?') | ||
| 673 | ); | ||
| 674 | println!(); | ||
| 675 | |||
| 676 | if win_type == "jackpot" { | ||
| 677 | println!("\n🎉 JACKPOT! 🎉"); | ||
| 678 | } else if win_type == "two" { | ||
| 679 | println!("\n✨ Two of a kind! ✨"); | ||
| 680 | } else { | ||
| 681 | println!("\nNo win."); | ||
| 682 | } | ||
| 683 | |||
| 684 | println!("Winnings: {} PLN", parts[2]); | ||
| 685 | println!("New Balance: {} PLN", parts[4]); | ||
| 686 | stdout().flush().unwrap(); | ||
| 687 | } | ||
| 688 | } | ||
| 689 | } | ||
| 690 | } | ||
| 691 | } | ||
| 692 | |||
| 693 | fn restore_terminal(cleanup_done: &Arc<Mutex<bool>>) { | ||
| 694 | let mut done = cleanup_done.lock().unwrap(); | ||
| 695 | if !*done { | ||
| 696 | let _ = disable_raw_mode(); | ||
| 697 | let _ = execute!(stdout(), Show); | ||
| 698 | *done = true; | ||
| 699 | } | ||
| 390 | } | 700 | } |
| 391 | 701 | ||
| 392 | fn get_commands() -> Vec<String> { | 702 | fn get_commands() -> Vec<String> { |
| @@ -400,13 +710,15 @@ fn get_commands() -> Vec<String> { | |||
| 400 | "setcred".to_string(), | 710 | "setcred".to_string(), |
| 401 | "logout".to_string(), | 711 | "logout".to_string(), |
| 402 | "info".to_string(), | 712 | "info".to_string(), |
| 713 | "play".to_string(), | ||
| 403 | "help".to_string(), | 714 | "help".to_string(), |
| 404 | "quit".to_string(), | 715 | "quit".to_string(), |
| 405 | ] | 716 | ] |
| 406 | } | 717 | } |
| 407 | 718 | ||
| 408 | fn help() { | 719 | fn help() { |
| 409 | println!("\nAvailable commands:\r | 720 | println!( |
| 721 | "\nAvailable commands:\r | ||
| 410 | connect <ip>:<port> - connect to the server\r | 722 | connect <ip>:<port> - connect to the server\r |
| 411 | send <message> - send a message to the server\r | 723 | send <message> - send a message to the server\r |
| 412 | disconnect - disconnect from the server\r | 724 | disconnect - disconnect from the server\r |
| @@ -416,8 +728,38 @@ fn help() { | |||
| 416 | setcred - set credentials for a host\r | 728 | setcred - set credentials for a host\r |
| 417 | logout - logout from the server\r | 729 | logout - logout from the server\r |
| 418 | info - show login info\r | 730 | info - show login info\r |
| 731 | play <game> [bet] - start a game (e.g., play slots 50)\r | ||
| 419 | help - show this message\r | 732 | help - show this message\r |
| 420 | quit - quit the program"); | 733 | quit - quit the program" |
| 734 | ); | ||
| 735 | } | ||
| 736 | |||
| 737 | fn redraw_game(game_output: &str) { | ||
| 738 | execute!( | ||
| 739 | stdout(), | ||
| 740 | Clear(ClearType::CurrentLine), | ||
| 741 | Clear(ClearType::FromCursorDown), | ||
| 742 | MoveToColumn(0) | ||
| 743 | ) | ||
| 744 | .unwrap(); | ||
| 745 | println!("{}", game_output); | ||
| 746 | stdout().flush().unwrap(); | ||
| 747 | } | ||
| 748 | |||
| 749 | fn redraw_game_lines(game_output: &str, lines: usize) { | ||
| 750 | use crossterm::cursor::MoveUp; | ||
| 751 | if lines > 0 { | ||
| 752 | execute!(stdout(), MoveUp(lines as u16)).unwrap(); | ||
| 753 | } | ||
| 754 | execute!( | ||
| 755 | stdout(), | ||
| 756 | Clear(ClearType::CurrentLine), | ||
| 757 | Clear(ClearType::FromCursorDown), | ||
| 758 | MoveToColumn(0) | ||
| 759 | ) | ||
| 760 | .unwrap(); | ||
| 761 | println!("{}", game_output); | ||
| 762 | stdout().flush().unwrap(); | ||
| 421 | } | 763 | } |
| 422 | 764 | ||
| 423 | fn redraw(current_input: &str) { | 765 | fn redraw(current_input: &str) { |
| @@ -431,36 +773,40 @@ fn read_line(prompt: &str) -> String { | |||
| 431 | execute!(stdout(), Show).unwrap(); | 773 | execute!(stdout(), Show).unwrap(); |
| 432 | printsame!("{}", prompt); | 774 | printsame!("{}", prompt); |
| 433 | stdout().flush().unwrap(); | 775 | stdout().flush().unwrap(); |
| 434 | 776 | ||
| 435 | let mut input = String::new(); | 777 | let mut input = String::new(); |
| 436 | let prompt_len = prompt.len(); | 778 | let prompt_len = prompt.len(); |
| 437 | 779 | ||
| 438 | loop { | 780 | loop { |
| 439 | match read().unwrap() { | 781 | match read().unwrap() { |
| 440 | Event::Key(key) => { | 782 | Event::Key(key) => { |
| 441 | if key.kind == crossterm::event::KeyEventKind::Press { | 783 | if key.kind == crossterm::event::KeyEventKind::Press { |
| 442 | match key.code { | 784 | match key.code { |
| 443 | KeyCode::Enter => { | 785 | KeyCode::Enter => { |
| 444 | println!(); | 786 | println!(); |
| 445 | break; | 787 | break; |
| 446 | } | 788 | } |
| 447 | KeyCode::Backspace => { | 789 | KeyCode::Backspace => { |
| 448 | if !input.is_empty() { | 790 | if !input.is_empty() { |
| 449 | input.pop(); | 791 | input.pop(); |
| 450 | // Move cursor back and clear from current position to end of line | 792 | // Move cursor back and clear from current position to end of line |
| 451 | execute!(stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), | 793 | execute!( |
| 452 | Clear(ClearType::UntilNewLine)).unwrap(); | 794 | stdout(), |
| 453 | stdout().flush().unwrap(); | 795 | MoveToColumn((prompt_len + input.len() + 1) as u16), |
| 796 | Clear(ClearType::UntilNewLine) | ||
| 797 | ) | ||
| 798 | .unwrap(); | ||
| 799 | stdout().flush().unwrap(); | ||
| 800 | } | ||
| 801 | } | ||
| 802 | KeyCode::Char(c) => { | ||
| 803 | input.push(c); | ||
| 804 | print!("{}", c); | ||
| 805 | stdout().flush().unwrap(); | ||
| 806 | } | ||
| 807 | _ => {} | ||
| 454 | } | 808 | } |
| 455 | } | 809 | } |
| 456 | KeyCode::Char(c) => { | ||
| 457 | input.push(c); | ||
| 458 | print!("{}", c); | ||
| 459 | stdout().flush().unwrap(); | ||
| 460 | } | ||
| 461 | _ => {} | ||
| 462 | } | ||
| 463 | } | ||
| 464 | } | 810 | } |
| 465 | _ => {} | 811 | _ => {} |
| 466 | } | 812 | } |
| @@ -472,36 +818,40 @@ fn read_password(prompt: &str) -> String { | |||
| 472 | execute!(stdout(), Show).unwrap(); | 818 | execute!(stdout(), Show).unwrap(); |
| 473 | print!("{}", prompt); | 819 | print!("{}", prompt); |
| 474 | stdout().flush().unwrap(); | 820 | stdout().flush().unwrap(); |
| 475 | 821 | ||
| 476 | let mut input = String::new(); | 822 | let mut input = String::new(); |
| 477 | let prompt_len = prompt.len(); | 823 | let prompt_len = prompt.len(); |
| 478 | 824 | ||
| 479 | loop { | 825 | loop { |
| 480 | match read().unwrap() { | 826 | match read().unwrap() { |
| 481 | Event::Key(key) => { | 827 | Event::Key(key) => { |
| 482 | if key.kind == crossterm::event::KeyEventKind::Press { | 828 | if key.kind == crossterm::event::KeyEventKind::Press { |
| 483 | match key.code { | 829 | match key.code { |
| 484 | KeyCode::Enter => { | 830 | KeyCode::Enter => { |
| 485 | println!(); | 831 | println!(); |
| 486 | break; | 832 | break; |
| 487 | } | 833 | } |
| 488 | KeyCode::Backspace => { | 834 | KeyCode::Backspace => { |
| 489 | if !input.is_empty() { | 835 | if !input.is_empty() { |
| 490 | input.pop(); | 836 | input.pop(); |
| 491 | // Move cursor back and clear from current position to end of line | 837 | // Move cursor back and clear from current position to end of line |
| 492 | execute!(stdout(), MoveToColumn((prompt_len + input.len() + 1) as u16), | 838 | execute!( |
| 493 | Clear(ClearType::UntilNewLine)).unwrap(); | 839 | stdout(), |
| 494 | stdout().flush().unwrap(); | 840 | MoveToColumn((prompt_len + input.len() + 1) as u16), |
| 841 | Clear(ClearType::UntilNewLine) | ||
| 842 | ) | ||
| 843 | .unwrap(); | ||
| 844 | stdout().flush().unwrap(); | ||
| 845 | } | ||
| 846 | } | ||
| 847 | KeyCode::Char(c) => { | ||
| 848 | input.push(c); | ||
| 849 | print!("*"); | ||
| 850 | stdout().flush().unwrap(); | ||
| 851 | } | ||
| 852 | _ => {} | ||
| 495 | } | 853 | } |
| 496 | } | 854 | } |
| 497 | KeyCode::Char(c) => { | ||
| 498 | input.push(c); | ||
| 499 | print!("*"); | ||
| 500 | stdout().flush().unwrap(); | ||
| 501 | } | ||
| 502 | _ => {} | ||
| 503 | } | ||
| 504 | } | ||
| 505 | } | 855 | } |
| 506 | _ => {} | 856 | _ => {} |
| 507 | } | 857 | } |
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 @@ | |||
| 1 | use std::io::{Read, Write}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | use std::sync::mpsc; | ||
| 4 | use std::thread; | ||
| 5 | use std::time::{Duration, Instant}; | ||
| 6 | |||
| 7 | pub struct TlsConnector { | ||
| 8 | connector: rustls::ClientConnection, | ||
| 9 | server_name: String, | ||
| 10 | } | ||
| 11 | |||
| 12 | impl TlsConnector { | ||
| 13 | pub fn new(server_name: &str) -> Result<Self, Box<dyn std::error::Error>> { | ||
| 14 | let config = rustls::ClientConfig::builder() | ||
| 15 | .dangerous() | ||
| 16 | .with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier)) | ||
| 17 | .with_no_client_auth(); | ||
| 18 | |||
| 19 | let connector = | ||
| 20 | rustls::ClientConnection::new(std::sync::Arc::new(config), server_name.try_into()?); | ||
| 21 | |||
| 22 | Ok(Self { | ||
| 23 | connector, | ||
| 24 | server_name: server_name.to_string(), | ||
| 25 | }) | ||
| 26 | } | ||
| 27 | |||
| 28 | pub fn connect( | ||
| 29 | &self, | ||
| 30 | stream: TcpStream, | ||
| 31 | ) -> Result<TlsClientStream, Box<dyn std::error::Error>> { | ||
| 32 | let mut conn = rustls::ClientConnection::new( | ||
| 33 | std::sync::Arc::new( | ||
| 34 | rustls::ClientConfig::builder() | ||
| 35 | .dangerous() | ||
| 36 | .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT) | ||
| 37 | .with_no_client_auth(), | ||
| 38 | ), | ||
| 39 | self.server_name.clone().try_into()?, | ||
| 40 | )?; | ||
| 41 | |||
| 42 | conn.complete_io(&mut std::io::BufReader::new(&stream))?; | ||
| 43 | |||
| 44 | Ok(TlsClientStream { | ||
| 45 | stream, | ||
| 46 | conn: std::sync::Arc::new(conn), | ||
| 47 | }) | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | struct InsecureVerifier; | ||
| 52 | |||
| 53 | impl rustls::client::danger::ServerCertVerifier for InsecureVerifier { | ||
| 54 | fn verify_server_cert( | ||
| 55 | &self, | ||
| 56 | _end_entity: &rustls::pki_types::CertificateDer<'_>, | ||
| 57 | _intermediates: &[rustls::pki_types::CertificateDer<'_>], | ||
| 58 | _server_name: &rustls::pki_types::ServerName<'_>, | ||
| 59 | _ocsp_response: &[u8], | ||
| 60 | _now: std::time::SystemTime, | ||
| 61 | ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> { | ||
| 62 | Ok(rustls::client::danger::ServerCertVerified::assertion()) | ||
| 63 | } | ||
| 64 | |||
| 65 | fn verify_tls12_signature( | ||
| 66 | &self, | ||
| 67 | _message: &[u8], | ||
| 68 | _cert: &rustls::pki_types::CertificateDer<'_>, | ||
| 69 | _dss: &rustls::DigitallySignedStruct, | ||
| 70 | ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { | ||
| 71 | Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) | ||
| 72 | } | ||
| 73 | |||
| 74 | fn verify_tls13_signature( | ||
| 75 | &self, | ||
| 76 | _message: &[u8], | ||
| 77 | _cert: &rustls::pki_types::CertificateDer<'_>, | ||
| 78 | _dss: &rustls::DigitallySignedStruct, | ||
| 79 | ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { | ||
| 80 | Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) | ||
| 81 | } | ||
| 82 | |||
| 83 | fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> { | ||
| 84 | vec![ | ||
| 85 | rustls::SignatureScheme::RSA_PKCS1_SHA256, | ||
| 86 | rustls::SignatureScheme::RSA_PKCS1_SHA384, | ||
| 87 | rustls::SignatureScheme::RSA_PKCS1_SHA512, | ||
| 88 | rustls::SignatureScheme::RSA_PSS_SHA256, | ||
| 89 | rustls::SignatureScheme::RSA_PSS_SHA384, | ||
| 90 | rustls::SignatureScheme::RSA_PSS_SHA512, | ||
| 91 | rustls::SignatureScheme::ECDSA_NISTP256_SHA256, | ||
| 92 | rustls::SignatureScheme::ECDSA_NISTP384_SHA384, | ||
| 93 | rustls::SignatureScheme::ECDSA_NISTP521_SHA512, | ||
| 94 | rustls::SignatureScheme::RSA_PSS_SHA256_LEGACY, | ||
| 95 | rustls::SignatureScheme::RSA_PSS_SHA384_LEGACY, | ||
| 96 | rustls::SignatureScheme::RSA_PSS_SHA512_LEGACY, | ||
| 97 | ] | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | pub struct TlsClientStream { | ||
| 102 | stream: TcpStream, | ||
| 103 | conn: std::sync::Arc<rustls::ClientConnection>, | ||
| 104 | } | ||
| 105 | |||
| 106 | impl TlsClientStream { | ||
| 107 | pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> { | ||
| 108 | let mut byte_buf = [0u8; 1024]; | ||
| 109 | loop { | ||
| 110 | let mut tls_reader = std::io::BufReader::new(self.conn.clone()); | ||
| 111 | let n = tls_reader.read(&mut byte_buf)?; | ||
| 112 | if n == 0 { | ||
| 113 | return Ok(0); | ||
| 114 | } | ||
| 115 | let chunk = std::str::from_utf8(&byte_buf[..n]).unwrap_or(""); | ||
| 116 | buf.push_str(chunk); | ||
| 117 | if buf.contains('\n') { | ||
| 118 | return Ok(n); | ||
| 119 | } | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { | ||
| 124 | use std::io::Write; | ||
| 125 | self.conn.writer().write_all(msg.as_bytes())?; | ||
| 126 | self.conn.writer().write_all(b"\n")?; | ||
| 127 | self.conn.writer().flush()?; | ||
| 128 | Ok(()) | ||
| 129 | } | ||
| 130 | |||
| 131 | pub fn try_clone(&self) -> std::io::Result<TlsClientStream> { | ||
| 132 | let new_stream = self.stream.try_clone()?; | ||
| 133 | let mut conn = rustls::ClientConnection::new( | ||
| 134 | std::sync::Arc::new( | ||
| 135 | rustls::ClientConfig::builder() | ||
| 136 | .dangerous() | ||
| 137 | .with_custom_verifier(rustls::client::danger::ServerCertVerifier::DEFAULT) | ||
| 138 | .with_no_client_auth(), | ||
| 139 | ), | ||
| 140 | self.server_name.clone().try_into().unwrap(), | ||
| 141 | )?; | ||
| 142 | |||
| 143 | conn.complete_io(&mut std::io::BufReader::new(&new_stream))?; | ||
| 144 | |||
| 145 | Ok(TlsClientStream { | ||
| 146 | stream: new_stream, | ||
| 147 | conn: std::sync::Arc::new(conn), | ||
| 148 | }) | ||
| 149 | } | ||
| 150 | } | ||
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 @@ | |||
| 1 | pub struct Args { | 1 | pub struct Args { |
| 2 | pub ip: String, | 2 | pub ip: String, |
| 3 | pub port: u16, | 3 | pub port: u16, |
| 4 | pub unsafe_mode: bool, | ||
| 4 | } | 5 | } |
| 5 | 6 | ||
| 6 | pub fn parse_args() -> Args { | 7 | pub fn parse_args() -> Args { |
| 7 | let args: Vec<String> = std::env::args().collect(); | 8 | let args: Vec<String> = std::env::args().collect(); |
| 8 | let mut ip = "127.0.0.1".to_string(); | 9 | let mut ip = "127.0.0.1".to_string(); |
| 9 | let mut port = 11525; | 10 | let mut port = 11525; |
| 11 | let mut unsafe_mode = false; | ||
| 10 | let mut i = 1; | 12 | let mut i = 1; |
| 11 | while i < args.len() { | 13 | while i < args.len() { |
| 12 | if args[i] == "-p" { | 14 | if args[i] == "-p" { |
| @@ -32,10 +34,17 @@ pub fn parse_args() -> Args { | |||
| 32 | eprintln!("-i requires an IP address"); | 34 | eprintln!("-i requires an IP address"); |
| 33 | std::process::exit(1); | 35 | std::process::exit(1); |
| 34 | } | 36 | } |
| 37 | } else if args[i] == "--unsafe" { | ||
| 38 | unsafe_mode = true; | ||
| 39 | i += 1; | ||
| 35 | } else { | 40 | } else { |
| 36 | eprintln!("Unknown argument: {}", args[i]); | 41 | eprintln!("Unknown argument: {}", args[i]); |
| 37 | std::process::exit(1); | 42 | std::process::exit(1); |
| 38 | } | 43 | } |
| 39 | } | 44 | } |
| 40 | Args { ip, port } | 45 | Args { |
| 46 | ip, | ||
| 47 | port, | ||
| 48 | unsafe_mode, | ||
| 49 | } | ||
| 41 | } | 50 | } |
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 { | |||
| 11 | balance INTEGER DEFAULT 1000 | 11 | balance INTEGER DEFAULT 1000 |
| 12 | )", | 12 | )", |
| 13 | [], | 13 | [], |
| 14 | ).unwrap(); | 14 | ) |
| 15 | .unwrap(); | ||
| 15 | // Add column if not exists (for migration) | 16 | // Add column if not exists (for migration) |
| 16 | let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []); | 17 | let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []); |
| 17 | let _ = conn.execute("ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", []); | 18 | let _ = conn.execute( |
| 19 | "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", | ||
| 20 | [], | ||
| 21 | ); | ||
| 18 | conn | 22 | conn |
| 19 | } | 23 | } |
| 20 | 24 | ||
| 21 | pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> { | 25 | pub fn add_user( |
| 26 | conn: &Connection, | ||
| 27 | username: &str, | ||
| 28 | hashed_password: &str, | ||
| 29 | ) -> Result<(), Box<dyn std::error::Error>> { | ||
| 22 | loop { | 30 | loop { |
| 23 | let account_id: u32 = rand::random::<u32>(); | 31 | let account_id: u32 = rand::random::<u32>(); |
| 24 | match conn.execute( | 32 | match conn.execute( |
| @@ -33,7 +41,9 @@ pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Res | |||
| 33 | } | 41 | } |
| 34 | 42 | ||
| 35 | pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { | 43 | pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { |
| 36 | let mut stmt = conn.prepare("SELECT hashed_password FROM users WHERE username = ?1").unwrap(); | 44 | let mut stmt = conn |
| 45 | .prepare("SELECT hashed_password FROM users WHERE username = ?1") | ||
| 46 | .unwrap(); | ||
| 37 | let mut rows = stmt.query([username]).unwrap(); | 47 | let mut rows = stmt.query([username]).unwrap(); |
| 38 | if let Ok(Some(row)) = rows.next() { | 48 | if let Ok(Some(row)) = rows.next() { |
| 39 | row.get(0).ok() | 49 | row.get(0).ok() |
| @@ -43,7 +53,9 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> | |||
| 43 | } | 53 | } |
| 44 | 54 | ||
| 45 | pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { | 55 | pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { |
| 46 | let mut stmt = conn.prepare("SELECT account_id FROM users WHERE username = ?1").unwrap(); | 56 | let mut stmt = conn |
| 57 | .prepare("SELECT account_id FROM users WHERE username = ?1") | ||
| 58 | .unwrap(); | ||
| 47 | let mut rows = stmt.query([username]).unwrap(); | 59 | let mut rows = stmt.query([username]).unwrap(); |
| 48 | if let Ok(Some(row)) = rows.next() { | 60 | if let Ok(Some(row)) = rows.next() { |
| 49 | Some(row.get::<_, u32>(0).ok()?) | 61 | Some(row.get::<_, u32>(0).ok()?) |
| @@ -53,7 +65,9 @@ pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { | |||
| 53 | } | 65 | } |
| 54 | 66 | ||
| 55 | pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> { | 67 | pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> { |
| 56 | let mut stmt = conn.prepare("SELECT balance FROM users WHERE username = ?1").unwrap(); | 68 | let mut stmt = conn |
| 69 | .prepare("SELECT balance FROM users WHERE username = ?1") | ||
| 70 | .unwrap(); | ||
| 57 | let mut rows = stmt.query([username]).unwrap(); | 71 | let mut rows = stmt.query([username]).unwrap(); |
| 58 | if let Ok(Some(row)) = rows.next() { | 72 | if let Ok(Some(row)) = rows.next() { |
| 59 | Some(row.get::<_, i64>(0).ok()?) | 73 | 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 @@ | |||
| 1 | pub mod registry; | ||
| 2 | pub mod session; | ||
| 3 | pub mod slots; | ||
| 4 | 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 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | |||
| 3 | use crate::games::traits::Game; | ||
| 4 | use std::collections::HashMap; | ||
| 5 | use std::sync::{Arc, Mutex}; | ||
| 6 | |||
| 7 | pub type GameRef = Arc<dyn Game + Send + Sync>; | ||
| 8 | |||
| 9 | pub struct GameRegistry { | ||
| 10 | games: Arc<Mutex<HashMap<String, GameRef>>>, | ||
| 11 | } | ||
| 12 | |||
| 13 | impl GameRegistry { | ||
| 14 | pub fn new() -> Self { | ||
| 15 | Self { | ||
| 16 | games: Arc::new(Mutex::new(HashMap::new())), | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | pub fn register_game<G: Game + Send + Sync + 'static>(&self, game: G) { | ||
| 21 | let game_type = game.game_type().to_string(); | ||
| 22 | let mut games = self.games.lock().unwrap(); | ||
| 23 | games.insert(game_type, Arc::new(game)); | ||
| 24 | } | ||
| 25 | |||
| 26 | pub fn get_game(&self, game_type: &str) -> Option<GameRef> { | ||
| 27 | let games = self.games.lock().unwrap(); | ||
| 28 | games.get(game_type).cloned() | ||
| 29 | } | ||
| 30 | |||
| 31 | pub fn list_games(&self) -> Vec<String> { | ||
| 32 | let games = self.games.lock().unwrap(); | ||
| 33 | games.keys().cloned().collect() | ||
| 34 | } | ||
| 35 | |||
| 36 | pub fn game_exists(&self, game_type: &str) -> bool { | ||
| 37 | let games = self.games.lock().unwrap(); | ||
| 38 | games.contains_key(game_type) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | // Global registry instance | ||
| 43 | use crate::games::slots::SlotsGame; | ||
| 44 | use once_cell::sync::Lazy; | ||
| 45 | |||
| 46 | static GAME_REGISTRY: Lazy<GameRegistry> = Lazy::new(|| { | ||
| 47 | let registry = GameRegistry::new(); | ||
| 48 | // Register games here | ||
| 49 | registry.register_game(SlotsGame::new()); | ||
| 50 | registry | ||
| 51 | }); | ||
| 52 | |||
| 53 | pub fn get_game_registry() -> &'static GameRegistry { | ||
| 54 | &GAME_REGISTRY | ||
| 55 | } | ||
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 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | |||
| 3 | use crate::games::traits::{GameError, GameState}; | ||
| 4 | use std::collections::HashMap; | ||
| 5 | use std::sync::{Arc, Mutex}; | ||
| 6 | use uuid::Uuid; | ||
| 7 | |||
| 8 | #[derive(Debug, Clone)] | ||
| 9 | pub struct GameSession { | ||
| 10 | pub session_id: String, | ||
| 11 | pub username: String, | ||
| 12 | pub game_type: String, | ||
| 13 | pub bet_amount: u64, | ||
| 14 | pub state: GameState, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub struct SessionManager { | ||
| 18 | sessions: Arc<Mutex<HashMap<String, GameSession>>>, | ||
| 19 | } | ||
| 20 | |||
| 21 | impl SessionManager { | ||
| 22 | pub fn new() -> Self { | ||
| 23 | Self { | ||
| 24 | sessions: Arc::new(Mutex::new(HashMap::new())), | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | pub fn create_session(&self, username: &str, game_type: String, state: GameState) -> String { | ||
| 29 | let session_id = Uuid::new_v4().to_string(); | ||
| 30 | let session = GameSession { | ||
| 31 | session_id: session_id.clone(), | ||
| 32 | username: username.to_string(), | ||
| 33 | game_type, | ||
| 34 | bet_amount: state.bet_amount, | ||
| 35 | state, | ||
| 36 | }; | ||
| 37 | |||
| 38 | let mut sessions = self.sessions.lock().unwrap(); | ||
| 39 | sessions.insert(session_id.clone(), session); | ||
| 40 | session_id | ||
| 41 | } | ||
| 42 | |||
| 43 | pub fn get_session(&self, session_id: &str) -> Result<GameSession, GameError> { | ||
| 44 | let sessions = self.sessions.lock().unwrap(); | ||
| 45 | sessions | ||
| 46 | .get(session_id) | ||
| 47 | .cloned() | ||
| 48 | .ok_or(GameError::SessionNotFound) | ||
| 49 | } | ||
| 50 | |||
| 51 | pub fn update_session(&self, session_id: &str, new_state: GameState) -> Result<(), GameError> { | ||
| 52 | let mut sessions = self.sessions.lock().unwrap(); | ||
| 53 | if let Some(session) = sessions.get_mut(session_id) { | ||
| 54 | session.state = new_state; | ||
| 55 | Ok(()) | ||
| 56 | } else { | ||
| 57 | Err(GameError::SessionNotFound) | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | pub fn remove_session(&self, session_id: &str) -> Result<(), GameError> { | ||
| 62 | let mut sessions = self.sessions.lock().unwrap(); | ||
| 63 | sessions | ||
| 64 | .remove(session_id) | ||
| 65 | .ok_or(GameError::SessionNotFound) | ||
| 66 | .map(|_| ()) | ||
| 67 | } | ||
| 68 | |||
| 69 | pub fn get_user_sessions(&self, username: &str) -> Vec<GameSession> { | ||
| 70 | let sessions = self.sessions.lock().unwrap(); | ||
| 71 | sessions | ||
| 72 | .values() | ||
| 73 | .filter(|s| s.username == username) | ||
| 74 | .cloned() | ||
| 75 | .collect() | ||
| 76 | } | ||
| 77 | } | ||
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 @@ | |||
| 1 | use crate::games::traits::{Game, GameError, GamePhase, GameResult, GameState}; | ||
| 2 | use rand::Rng; | ||
| 3 | use std::collections::HashMap; | ||
| 4 | |||
| 5 | pub struct SlotsGame { | ||
| 6 | symbols: Vec<char>, | ||
| 7 | payouts: HashMap<String, u64>, | ||
| 8 | } | ||
| 9 | |||
| 10 | impl SlotsGame { | ||
| 11 | pub fn new() -> Self { | ||
| 12 | let symbols = vec!['C', 'L', 'O', 'G', 'D', 'S']; | ||
| 13 | let mut payouts = HashMap::new(); | ||
| 14 | |||
| 15 | payouts.insert("CCC".to_string(), 5); | ||
| 16 | payouts.insert("LLL".to_string(), 10); | ||
| 17 | payouts.insert("OOO".to_string(), 15); | ||
| 18 | payouts.insert("GGG".to_string(), 20); | ||
| 19 | payouts.insert("DDD".to_string(), 50); | ||
| 20 | payouts.insert("SSS".to_string(), 100); | ||
| 21 | |||
| 22 | payouts.insert("CC".to_string(), 2); | ||
| 23 | payouts.insert("LL".to_string(), 3); | ||
| 24 | payouts.insert("OO".to_string(), 4); | ||
| 25 | payouts.insert("GG".to_string(), 5); | ||
| 26 | payouts.insert("DD".to_string(), 10); | ||
| 27 | payouts.insert("SS".to_string(), 20); | ||
| 28 | |||
| 29 | Self { symbols, payouts } | ||
| 30 | } | ||
| 31 | |||
| 32 | fn spin_reels(&self) -> Vec<char> { | ||
| 33 | let mut rng = rand::thread_rng(); | ||
| 34 | (0..3) | ||
| 35 | .map(|_| self.symbols[rng.gen_range(0..self.symbols.len())]) | ||
| 36 | .collect() | ||
| 37 | } | ||
| 38 | |||
| 39 | fn calculate_payout(&self, reels: &[char], bet: u64) -> u64 { | ||
| 40 | let reel_string: String = reels.iter().collect(); | ||
| 41 | |||
| 42 | if let Some(&multiplier) = self.payouts.get(&reel_string) { | ||
| 43 | return bet * multiplier; | ||
| 44 | } | ||
| 45 | |||
| 46 | for i in 0..2 { | ||
| 47 | if reels[i] == reels[i + 1] { | ||
| 48 | let two_of_kind = format!("{}{}", reels[i], reels[i]); | ||
| 49 | if let Some(&multiplier) = self.payouts.get(&two_of_kind) { | ||
| 50 | return bet * multiplier; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | 0 | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | impl Game for SlotsGame { | ||
| 60 | fn game_type(&self) -> &'static str { | ||
| 61 | "slots" | ||
| 62 | } | ||
| 63 | |||
| 64 | fn get_name(&self) -> &'static str { | ||
| 65 | "Slots" | ||
| 66 | } | ||
| 67 | |||
| 68 | fn get_description(&self) -> &'static str { | ||
| 69 | "Spin the reels and match symbols to win!" | ||
| 70 | } | ||
| 71 | |||
| 72 | fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState { | ||
| 73 | let mut data = HashMap::new(); | ||
| 74 | data.insert("username".to_string(), username.to_string()); | ||
| 75 | data.insert("status".to_string(), "ready".to_string()); | ||
| 76 | |||
| 77 | GameState { | ||
| 78 | session_id: String::new(), | ||
| 79 | game_type: "slots".to_string(), | ||
| 80 | bet_amount, | ||
| 81 | phase: GamePhase::WaitingToStart, | ||
| 82 | data, | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | fn process_action( | ||
| 87 | &self, | ||
| 88 | state: &GameState, | ||
| 89 | action: &str, | ||
| 90 | _params: &[&str], | ||
| 91 | ) -> Result<GameState, GameError> { | ||
| 92 | if state.phase == GamePhase::Finished { | ||
| 93 | return Err(GameError::GameOver); | ||
| 94 | } | ||
| 95 | |||
| 96 | match action { | ||
| 97 | "spin" => { | ||
| 98 | if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing { | ||
| 99 | return Err(GameError::InvalidAction( | ||
| 100 | "Cannot spin in current phase".to_string(), | ||
| 101 | )); | ||
| 102 | } | ||
| 103 | |||
| 104 | let reels = self.spin_reels(); | ||
| 105 | let reel_string: String = reels.iter().collect(); | ||
| 106 | |||
| 107 | let mut new_data = state.data.clone(); | ||
| 108 | new_data.insert("reels".to_string(), reel_string.clone()); | ||
| 109 | new_data.insert("status".to_string(), "spun".to_string()); | ||
| 110 | |||
| 111 | let winnings = self.calculate_payout(&reels, state.bet_amount); | ||
| 112 | new_data.insert("winnings".to_string(), winnings.to_string()); | ||
| 113 | |||
| 114 | let is_jackpot = reels[0] == reels[1] && reels[1] == reels[2]; | ||
| 115 | let is_two = reels[0] == reels[1] || reels[1] == reels[2]; | ||
| 116 | new_data.insert( | ||
| 117 | "win_type".to_string(), | ||
| 118 | if is_jackpot { | ||
| 119 | "jackpot".to_string() | ||
| 120 | } else if is_two { | ||
| 121 | "two".to_string() | ||
| 122 | } else { | ||
| 123 | "none".to_string() | ||
| 124 | }, | ||
| 125 | ); | ||
| 126 | |||
| 127 | Ok(GameState { | ||
| 128 | session_id: state.session_id.clone(), | ||
| 129 | game_type: state.game_type.clone(), | ||
| 130 | bet_amount: state.bet_amount, | ||
| 131 | phase: GamePhase::Finished, | ||
| 132 | data: new_data, | ||
| 133 | }) | ||
| 134 | } | ||
| 135 | _ => Err(GameError::InvalidAction(format!( | ||
| 136 | "Unknown action: {}", | ||
| 137 | action | ||
| 138 | ))), | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError> { | ||
| 143 | if state.phase != GamePhase::Finished { | ||
| 144 | return Err(GameError::InternalError("Game not finished".to_string())); | ||
| 145 | } | ||
| 146 | |||
| 147 | let reel_string = state | ||
| 148 | .data | ||
| 149 | .get("reels") | ||
| 150 | .ok_or(GameError::InternalError("No reels found".to_string()))?; | ||
| 151 | |||
| 152 | let reels: Vec<char> = reel_string.chars().collect(); | ||
| 153 | let winnings = self.calculate_payout(&reels, state.bet_amount); | ||
| 154 | |||
| 155 | let mut result_data = HashMap::new(); | ||
| 156 | result_data.insert("reels".to_string(), reel_string.clone()); | ||
| 157 | result_data.insert( | ||
| 158 | "payout_multiplier".to_string(), | ||
| 159 | if winnings > 0 && state.bet_amount > 0 { | ||
| 160 | (winnings / state.bet_amount).to_string() | ||
| 161 | } else { | ||
| 162 | "0".to_string() | ||
| 163 | }, | ||
| 164 | ); | ||
| 165 | result_data.insert( | ||
| 166 | "win_type".to_string(), | ||
| 167 | state.data.get("win_type").cloned().unwrap_or_default(), | ||
| 168 | ); | ||
| 169 | |||
| 170 | Ok(GameResult { | ||
| 171 | session_id: state.session_id.clone(), | ||
| 172 | game_type: state.game_type.clone(), | ||
| 173 | bet_amount: state.bet_amount, | ||
| 174 | winnings, | ||
| 175 | final_balance: 0, | ||
| 176 | result_data, | ||
| 177 | }) | ||
| 178 | } | ||
| 179 | } | ||
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 @@ | |||
| 1 | use serde::{Deserialize, Serialize}; | ||
| 2 | use std::collections::HashMap; | ||
| 3 | |||
| 4 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 5 | pub enum GameError { | ||
| 6 | InvalidAction(String), | ||
| 7 | InsufficientBalance, | ||
| 8 | InvalidBet, | ||
| 9 | GameOver, | ||
| 10 | SessionNotFound, | ||
| 11 | InternalError(String), | ||
| 12 | } | ||
| 13 | |||
| 14 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||
| 15 | pub enum GamePhase { | ||
| 16 | WaitingToStart, | ||
| 17 | Playing, | ||
| 18 | Finished, | ||
| 19 | } | ||
| 20 | |||
| 21 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 22 | pub struct GameState { | ||
| 23 | pub session_id: String, | ||
| 24 | pub game_type: String, | ||
| 25 | pub bet_amount: u64, | ||
| 26 | pub phase: GamePhase, | ||
| 27 | pub data: HashMap<String, String>, | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 31 | pub struct GameResult { | ||
| 32 | pub session_id: String, | ||
| 33 | pub game_type: String, | ||
| 34 | pub bet_amount: u64, | ||
| 35 | pub winnings: u64, | ||
| 36 | pub final_balance: u64, | ||
| 37 | pub result_data: HashMap<String, String>, | ||
| 38 | } | ||
| 39 | |||
| 40 | pub trait Game: Send + Sync { | ||
| 41 | fn game_type(&self) -> &'static str; | ||
| 42 | |||
| 43 | fn get_name(&self) -> &'static str; | ||
| 44 | |||
| 45 | fn get_description(&self) -> &'static str; | ||
| 46 | |||
| 47 | fn validate_bet(&self, bet_amount: u64, user_balance: u64) -> Result<(), GameError> { | ||
| 48 | if bet_amount == 0 { | ||
| 49 | return Err(GameError::InvalidBet); | ||
| 50 | } | ||
| 51 | if bet_amount > user_balance { | ||
| 52 | return Err(GameError::InsufficientBalance); | ||
| 53 | } | ||
| 54 | Ok(()) | ||
| 55 | } | ||
| 56 | |||
| 57 | fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState; | ||
| 58 | |||
| 59 | fn process_action( | ||
| 60 | &self, | ||
| 61 | state: &GameState, | ||
| 62 | action: &str, | ||
| 63 | params: &[&str], | ||
| 64 | ) -> Result<GameState, GameError>; | ||
| 65 | |||
| 66 | fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError>; | ||
| 67 | } | ||
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 @@ | |||
| 1 | use crate::database; | ||
| 2 | use crate::games::registry::get_game_registry; | ||
| 3 | use crate::games::traits::GameError; | ||
| 4 | use crate::tls::TlsStream; | ||
| 5 | use once_cell::sync::Lazy; | ||
| 6 | use rusqlite::Connection; | ||
| 1 | use std::io::{BufRead, BufReader, Write}; | 7 | use std::io::{BufRead, BufReader, Write}; |
| 2 | use std::net::TcpStream; | 8 | use std::net::TcpStream; |
| 3 | use std::sync::Mutex; | 9 | use std::sync::Mutex; |
| 4 | use once_cell::sync::Lazy; | ||
| 5 | use rusqlite::Connection; | ||
| 6 | use crate::database; | ||
| 7 | 10 | ||
| 8 | static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db())); | 11 | static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db())); |
| 9 | 12 | ||
| 13 | #[allow(dead_code)] | ||
| 10 | pub fn handle_client(stream: TcpStream) { | 14 | pub fn handle_client(stream: TcpStream) { |
| 11 | let client_addr = stream.peer_addr().unwrap().to_string(); | 15 | let client_addr = stream.peer_addr().unwrap().to_string(); |
| 12 | let mut user = "guest".to_string(); | 16 | let mut user = "guest".to_string(); |
| @@ -25,8 +29,49 @@ pub fn handle_client(stream: TcpStream) { | |||
| 25 | } | 29 | } |
| 26 | 30 | ||
| 27 | fn process_message(msg: &str, user: &mut String) -> String { | 31 | fn process_message(msg: &str, user: &mut String) -> String { |
| 28 | let parts: Vec<&str> = msg.splitn(3, ' ').collect(); | 32 | let parts: Vec<&str> = msg.splitn(5, ' ').collect(); |
| 29 | match parts[0] { | 33 | match parts[0] { |
| 34 | "GAME_LIST" => { | ||
| 35 | let registry = get_game_registry(); | ||
| 36 | let games: Vec<String> = registry.list_games(); | ||
| 37 | if games.is_empty() { | ||
| 38 | "ERROR No games available".to_string() | ||
| 39 | } else { | ||
| 40 | let game_info: Vec<String> = games | ||
| 41 | .iter() | ||
| 42 | .filter_map(|gt| { | ||
| 43 | registry | ||
| 44 | .get_game(gt) | ||
| 45 | .map(|g| format!("{}:{}", gt, g.get_name())) | ||
| 46 | }) | ||
| 47 | .collect(); | ||
| 48 | format!("GAME_LIST {}", game_info.join(",")) | ||
| 49 | } | ||
| 50 | } | ||
| 51 | "GAME_INFO" => { | ||
| 52 | if parts.len() != 2 { | ||
| 53 | "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>".to_string() | ||
| 54 | } else { | ||
| 55 | let registry = get_game_registry(); | ||
| 56 | match registry.get_game(parts[1]) { | ||
| 57 | Some(game) => format!( | ||
| 58 | "GAME_INFO {} {} {}", | ||
| 59 | parts[1], | ||
| 60 | game.get_name(), | ||
| 61 | game.get_description() | ||
| 62 | ), | ||
| 63 | None => format!("ERROR Unknown game type: {}", parts[1]), | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | "PLAY_START" => { | ||
| 68 | if parts.len() != 3 { | ||
| 69 | "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount>" | ||
| 70 | .to_string() | ||
| 71 | } else { | ||
| 72 | handle_play_start(user, parts[1], parts[2]) | ||
| 73 | } | ||
| 74 | } | ||
| 30 | "REGISTER" => { | 75 | "REGISTER" => { |
| 31 | if parts.len() != 3 { | 76 | if parts.len() != 3 { |
| 32 | "ERROR Invalid REGISTER format".to_string() | 77 | "ERROR Invalid REGISTER format".to_string() |
| @@ -65,13 +110,133 @@ fn process_message(msg: &str, user: &mut String) -> String { | |||
| 65 | "ERROR Not logged in".to_string() | 110 | "ERROR Not logged in".to_string() |
| 66 | } else { | 111 | } else { |
| 67 | let db = DB_CONN.lock().unwrap(); | 112 | let db = DB_CONN.lock().unwrap(); |
| 68 | if let (Some(id), Some(balance)) = (database::get_account_id(&db, user), database::get_balance(&db, user)) { | 113 | if let (Some(id), Some(balance)) = ( |
| 114 | database::get_account_id(&db, user), | ||
| 115 | database::get_balance(&db, user), | ||
| 116 | ) { | ||
| 69 | format!("INFO {} {} {}", user, id, balance) | 117 | format!("INFO {} {} {}", user, id, balance) |
| 70 | } else { | 118 | } else { |
| 71 | "ERROR Error retrieving user info".to_string() | 119 | "ERROR Error retrieving user info".to_string() |
| 72 | } | 120 | } |
| 73 | } | 121 | } |
| 74 | } | 122 | } |
| 75 | _ => msg.to_string(), // Echo back for compatibility | 123 | _ => msg.to_string(), |
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | fn handle_play_start(user: &str, game_type: &str, bet_amount_str: &str) -> String { | ||
| 128 | if user == "guest" { | ||
| 129 | return "ERROR Not logged in".to_string(); | ||
| 130 | } | ||
| 131 | |||
| 132 | let bet_amount: u64 = match bet_amount_str.parse() { | ||
| 133 | Ok(amount) => amount, | ||
| 134 | Err(_) => return "ERROR Invalid bet amount".to_string(), | ||
| 135 | }; | ||
| 136 | |||
| 137 | let db = DB_CONN.lock().unwrap(); | ||
| 138 | let current_balance = match database::get_balance(&db, user) { | ||
| 139 | Some(balance) => balance as u64, | ||
| 140 | None => return "ERROR Could not retrieve balance".to_string(), | ||
| 141 | }; | ||
| 142 | |||
| 143 | let registry = get_game_registry(); | ||
| 144 | let game = match registry.get_game(game_type) { | ||
| 145 | Some(game) => game, | ||
| 146 | None => return format!("ERROR Unknown game type: {}", game_type), | ||
| 147 | }; | ||
| 148 | |||
| 149 | if let Err(e) = game.validate_bet(bet_amount, current_balance) { | ||
| 150 | return match e { | ||
| 151 | GameError::InsufficientBalance => "ERROR Insufficient balance".to_string(), | ||
| 152 | GameError::InvalidBet => "ERROR Invalid bet amount".to_string(), | ||
| 153 | _ => "ERROR Bet validation failed".to_string(), | ||
| 154 | }; | ||
| 155 | } | ||
| 156 | |||
| 157 | let game_state = game.prepare_state(user, bet_amount); | ||
| 158 | |||
| 159 | drop(db); | ||
| 160 | let db = DB_CONN.lock().unwrap(); | ||
| 161 | if let Err(_) = db.execute( | ||
| 162 | "UPDATE users SET balance = balance - ?1 WHERE username = ?2", | ||
| 163 | (bet_amount as i64, user), | ||
| 164 | ) { | ||
| 165 | return "ERROR Failed to deduct bet".to_string(); | ||
| 166 | } | ||
| 167 | |||
| 168 | let new_state = match game.process_action(&game_state, "spin", &[]) { | ||
| 169 | Ok(state) => state, | ||
| 170 | Err(e) => { | ||
| 171 | let _ = db.execute( | ||
| 172 | "UPDATE users SET balance = balance + ?1 WHERE username = ?2", | ||
| 173 | (bet_amount as i64, user), | ||
| 174 | ); | ||
| 175 | return format!("ERROR Failed to spin: {:?}", e); | ||
| 176 | } | ||
| 177 | }; | ||
| 178 | |||
| 179 | let result = match game.calculate_result(&new_state) { | ||
| 180 | Ok(result) => result, | ||
| 181 | Err(e) => { | ||
| 182 | let _ = db.execute( | ||
| 183 | "UPDATE users SET balance = balance + ?1 WHERE username = ?2", | ||
| 184 | (bet_amount as i64, user), | ||
| 185 | ); | ||
| 186 | return format!("ERROR Failed to calculate result: {:?}", e); | ||
| 187 | } | ||
| 188 | }; | ||
| 189 | |||
| 190 | let total_return = result.bet_amount + result.winnings; | ||
| 191 | if let Err(_) = db.execute( | ||
| 192 | "UPDATE users SET balance = balance + ?1 WHERE username = ?2", | ||
| 193 | (total_return as i64, user), | ||
| 194 | ) { | ||
| 195 | return "ERROR Failed to update balance".to_string(); | ||
| 196 | } | ||
| 197 | |||
| 198 | let new_balance = match database::get_balance(&db, user) { | ||
| 199 | Some(balance) => balance, | ||
| 200 | None => return "ERROR Could not retrieve new balance".to_string(), | ||
| 201 | }; | ||
| 202 | |||
| 203 | let reels = new_state.data.get("reels").cloned().unwrap_or_default(); | ||
| 204 | let win_type = new_state.data.get("win_type").cloned().unwrap_or_default(); | ||
| 205 | |||
| 206 | format!( | ||
| 207 | "GAME_RESULT {} {} {} {} {} {}", | ||
| 208 | result.session_id, result.winnings, result.bet_amount, new_balance, reels, win_type | ||
| 209 | ) | ||
| 210 | } | ||
| 211 | |||
| 212 | pub fn handle_client_tls(mut stream: TlsStream) { | ||
| 213 | let client_addr = stream.peer_addr().unwrap().to_string(); | ||
| 214 | let mut user = "guest".to_string(); | ||
| 215 | println!("[{} @ {}] TLS Client connected", user, client_addr); | ||
| 216 | |||
| 217 | loop { | ||
| 218 | let mut line = String::new(); | ||
| 219 | match stream.read_line(&mut line) { | ||
| 220 | Ok(0) => break, | ||
| 221 | Ok(_) => {} | ||
| 222 | Err(e) => { | ||
| 223 | eprintln!("Read error: {}", e); | ||
| 224 | break; | ||
| 225 | } | ||
| 226 | } | ||
| 227 | |||
| 228 | let line = line.trim(); | ||
| 229 | if line.is_empty() { | ||
| 230 | continue; | ||
| 231 | } | ||
| 232 | |||
| 233 | println!("[{} @ {}] Received: {}", user, client_addr, line); | ||
| 234 | let response = process_message(line, &mut user); | ||
| 235 | if let Err(e) = stream.write_line(&response) { | ||
| 236 | eprintln!("Write error: {}", e); | ||
| 237 | break; | ||
| 238 | } | ||
| 239 | println!("[{} @ {}] Sent: {}", user, client_addr, response); | ||
| 76 | } | 240 | } |
| 241 | println!("[{} @ {}] TLS Client disconnected", user, client_addr); | ||
| 77 | } | 242 | } |
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 @@ | |||
| 1 | mod args; | 1 | mod args; |
| 2 | mod handler; | ||
| 3 | mod database; | 2 | mod database; |
| 3 | mod games; | ||
| 4 | mod handler; | ||
| 5 | mod tls; | ||
| 4 | 6 | ||
| 5 | use std::net::TcpListener; | 7 | use std::net::TcpListener; |
| 8 | use std::sync::Arc; | ||
| 6 | use std::thread; | 9 | use std::thread; |
| 7 | 10 | ||
| 8 | fn main() { | 11 | fn main() { |
| @@ -14,11 +17,34 @@ fn main() { | |||
| 14 | std::process::exit(1); | 17 | std::process::exit(1); |
| 15 | } | 18 | } |
| 16 | }; | 19 | }; |
| 17 | println!("Server listening on {}", listener.local_addr().unwrap()); | 20 | |
| 21 | let tls = match tls::TlsServer::new("identity.p12", "key.pem", args.unsafe_mode) { | ||
| 22 | Ok(t) => Arc::new(t), | ||
| 23 | Err(e) => { | ||
| 24 | eprintln!("Failed to initialize TLS: {}", e); | ||
| 25 | std::process::exit(1); | ||
| 26 | } | ||
| 27 | }; | ||
| 28 | |||
| 29 | println!( | ||
| 30 | "Server listening on {} (TLS enabled){}", | ||
| 31 | listener.local_addr().unwrap(), | ||
| 32 | if args.unsafe_mode { | ||
| 33 | " [UNSAFE MODE]" | ||
| 34 | } else { | ||
| 35 | "" | ||
| 36 | } | ||
| 37 | ); | ||
| 18 | for stream in listener.incoming() { | 38 | for stream in listener.incoming() { |
| 19 | let stream = stream.unwrap(); | 39 | let stream = stream.unwrap(); |
| 20 | thread::spawn(move || { | 40 | let tls = tls.clone(); |
| 21 | handler::handle_client(stream); | 41 | thread::spawn(move || match tls.accept(stream) { |
| 42 | Ok(tls_stream) => { | ||
| 43 | handler::handle_client_tls(tls_stream); | ||
| 44 | } | ||
| 45 | Err(e) => { | ||
| 46 | eprintln!("TLS handshake failed: {}", e); | ||
| 47 | } | ||
| 22 | }); | 48 | }); |
| 23 | } | 49 | } |
| 24 | } | 50 | } |
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 @@ | |||
| 1 | use std::io::{Read, Write}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | |||
| 4 | #[derive(Clone)] | ||
| 5 | pub struct TlsServer { | ||
| 6 | acceptor: native_tls::TlsAcceptor, | ||
| 7 | } | ||
| 8 | |||
| 9 | impl TlsServer { | ||
| 10 | pub fn new( | ||
| 11 | cert_path: &str, | ||
| 12 | _key_path: &str, | ||
| 13 | unsafe_mode: bool, | ||
| 14 | ) -> Result<Self, Box<dyn std::error::Error>> { | ||
| 15 | let cert_data = if unsafe_mode { | ||
| 16 | if std::path::Path::new(cert_path).exists() { | ||
| 17 | std::fs::read(cert_path)? | ||
| 18 | } else { | ||
| 19 | return Err(format!( | ||
| 20 | "Certificate file '{}' not found. In --unsafe mode, you must provide a valid PKCS12 certificate file.\n\ | ||
| 21 | You can generate one with:\n\ | ||
| 22 | openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes\n\ | ||
| 23 | openssl pkcs12 -export -out identity.p12 -inkey key.pem -in cert.pem", | ||
| 24 | cert_path | ||
| 25 | ).into()); | ||
| 26 | } | ||
| 27 | } else { | ||
| 28 | std::fs::read(cert_path)? | ||
| 29 | }; | ||
| 30 | |||
| 31 | let identity = native_tls::Identity::from_pkcs12(&cert_data, "")?; | ||
| 32 | let acceptor = native_tls::TlsAcceptor::new(identity)?; | ||
| 33 | |||
| 34 | Ok(Self { acceptor }) | ||
| 35 | } | ||
| 36 | |||
| 37 | pub fn accept(&self, stream: TcpStream) -> Result<TlsStream, Box<dyn std::error::Error>> { | ||
| 38 | let stream = self.acceptor.accept(stream)?; | ||
| 39 | Ok(TlsStream::new(stream)) | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | pub struct TlsStream { | ||
| 44 | stream: native_tls::TlsStream<TcpStream>, | ||
| 45 | } | ||
| 46 | |||
| 47 | impl TlsStream { | ||
| 48 | pub fn new(stream: native_tls::TlsStream<TcpStream>) -> Self { | ||
| 49 | Self { stream } | ||
| 50 | } | ||
| 51 | |||
| 52 | pub fn read_line(&mut self, s: &mut String) -> std::io::Result<usize> { | ||
| 53 | let mut buf = [0u8; 1024]; | ||
| 54 | loop { | ||
| 55 | match self.stream.read(&mut buf) { | ||
| 56 | Ok(0) => return Ok(0), | ||
| 57 | Ok(n) => { | ||
| 58 | let chunk = std::str::from_utf8(&buf[..n]).unwrap_or(""); | ||
| 59 | s.push_str(chunk); | ||
| 60 | if s.contains('\n') { | ||
| 61 | return Ok(n); | ||
| 62 | } | ||
| 63 | } | ||
| 64 | Err(e) => return Err(e), | ||
| 65 | } | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> { | ||
| 70 | use std::io::Write; | ||
| 71 | self.stream.write_all(msg.as_bytes())?; | ||
| 72 | self.stream.write_all(b"\n")?; | ||
| 73 | self.stream.flush()?; | ||
| 74 | Ok(()) | ||
| 75 | } | ||
| 76 | |||
| 77 | pub fn peer_addr(&self) -> std::io::Result<std::net::SocketAddr> { | ||
| 78 | self.stream.get_ref().peer_addr() | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | impl Read for TlsStream { | ||
| 83 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | ||
| 84 | self.stream.read(buf) | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | impl Write for TlsStream { | ||
| 89 | fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||
| 90 | self.stream.write(buf) | ||
| 91 | } | ||
| 92 | |||
| 93 | fn flush(&mut self) -> std::io::Result<()> { | ||
| 94 | self.stream.flush() | ||
| 95 | } | ||
| 96 | } | ||
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 @@ | |||
| 1 | use std::io::{self, Write}; | ||
| 2 | use std::ptr; | ||
| 3 | use windows_sys::Win32::Foundation::HANDLE; | ||
| 4 | use windows_sys::Win32::System::Console::{ | ||
| 5 | GetConsoleMode, ReadConsoleW, SetConsoleMode, GetStdHandle, STD_INPUT_HANDLE, | ||
| 6 | ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, | ||
| 7 | }; | ||
| 8 | |||
| 9 | fn main() -> io::Result<()> { | ||
| 10 | unsafe { | ||
| 11 | let h: HANDLE = GetStdHandle(STD_INPUT_HANDLE); | ||
| 12 | if h == std::ptr::null_mut() || h == -1_i32 as HANDLE { return Err(io::Error::last_os_error()); } | ||
| 13 | |||
| 14 | // save current mode | ||
| 15 | let mut mode: u32 = 0; | ||
| 16 | if GetConsoleMode(h, &mut mode) == 0 { return Err(io::Error::last_os_error()); } | ||
| 17 | |||
| 18 | // disable line input and echo so input is available per-key and not echoed | ||
| 19 | let new_mode = mode & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT); | ||
| 20 | if SetConsoleMode(h, new_mode) == 0 { return Err(io::Error::last_os_error()); } | ||
| 21 | |||
| 22 | print!("Press keys (q to exit): "); | ||
| 23 | io::stdout().flush()?; | ||
| 24 | |||
| 25 | // ReadConsoleW reads UTF-16; we'll read one wchar at a time | ||
| 26 | let mut buf: [u16; 1] = [0]; | ||
| 27 | loop { | ||
| 28 | let mut read = 0u32; | ||
| 29 | if ReadConsoleW( | ||
| 30 | h, | ||
| 31 | buf.as_mut_ptr() as *mut _, | ||
| 32 | 1, | ||
| 33 | &mut read, | ||
| 34 | ptr::null_mut(), | ||
| 35 | ) == 0 | ||
| 36 | { | ||
| 37 | // restore mode on error before returning | ||
| 38 | let _ = SetConsoleMode(h, mode); | ||
| 39 | return Err(io::Error::last_os_error()); | ||
| 40 | } | ||
| 41 | |||
| 42 | let ch = std::char::from_u32(buf[0] as u32).unwrap_or('\u{FFFD}'); | ||
| 43 | if ch == 'q' { break; } // q to exit | ||
| 44 | |||
| 45 | // handle Enter (CR) so output is readable | ||
| 46 | if ch == '\r' { | ||
| 47 | println!(); | ||
| 48 | } else { | ||
| 49 | // show code point (no automatic console echo) | ||
| 50 | print!("[{:?}]", ch); | ||
| 51 | io::stdout().flush()?; | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | // restore original console mode | ||
| 56 | let _ = SetConsoleMode(h, mode); | ||
| 57 | println!("\nexited"); | ||
| 58 | } | ||
| 59 | |||
| 60 | Ok(()) | ||
| 61 | } | ||
