diff options
Diffstat (limited to '')
| -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 |
10 files changed, 709 insertions, 17 deletions
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 | } | ||
