diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 22:44:41 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 22:44:41 +0100 |
| commit | 63c2e2fc3ea299aee31b66d2bdd6ecd871df223a (patch) | |
| tree | 9c85ab7a4de17125055257dc055fa3e53dc1e8cf /src/bin | |
| parent | v0.2.6: preserve buffered lines and add request read timeouts (diff) | |
| download | zagan-bet-63c2e2fc3ea299aee31b66d2bdd6ecd871df223a.tar.gz zagan-bet-63c2e2fc3ea299aee31b66d2bdd6ecd871df223a.zip | |
v0.3.0: add server core, bcrypt migration and plays audit
Diffstat (limited to '')
| -rw-r--r-- | src/bin/server/handler.rs | 252 |
1 files changed, 27 insertions, 225 deletions
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs index 06ed5cb..86b09cb 100644 --- a/src/bin/server/handler.rs +++ b/src/bin/server/handler.rs | |||
| @@ -1,228 +1,34 @@ | |||
| 1 | use zagan_bet::server::database; | ||
| 2 | use zagan_bet::server::games::registry::get_game_registry; | ||
| 3 | use zagan_bet::server::games::session::SessionManager; | ||
| 4 | use zagan_bet::server::games::traits::GameError; | ||
| 5 | use crate::tls::TlsStream; | 1 | use crate::tls::TlsStream; |
| 6 | use once_cell::sync::Lazy; | 2 | use zagan_bet::server::core::{ConnectionState, ServerCore}; |
| 7 | use parking_lot::Mutex; | ||
| 8 | use rusqlite::Connection; | ||
| 9 | use std::io::{BufRead, BufReader, Write}; | ||
| 10 | use std::net::TcpStream; | ||
| 11 | 3 | ||
| 12 | static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| { | 4 | pub fn handle_client_tls(mut stream: TlsStream) { |
| 13 | let conn = Connection::open( | 5 | let client_addr = match stream.peer_addr() { |
| 14 | std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()), | 6 | Ok(addr) => addr.to_string(), |
| 15 | ) | 7 | Err(error) => { |
| 16 | .unwrap(); | 8 | eprintln!("Could not get peer address: {error}"); |
| 17 | database::init_db(&conn); | 9 | return; |
| 18 | Mutex::new(conn) | ||
| 19 | }); | ||
| 20 | |||
| 21 | static SESSIONS: Lazy<SessionManager> = Lazy::new(SessionManager::new); | ||
| 22 | |||
| 23 | pub fn handle_client(stream: TcpStream) { | ||
| 24 | let client_addr = stream.peer_addr().unwrap().to_string(); | ||
| 25 | let mut user = "guest".to_string(); | ||
| 26 | println!("[{} @ {}] Client connected", user, client_addr); | ||
| 27 | let reader_stream = stream.try_clone().unwrap(); | ||
| 28 | let reader = BufReader::new(reader_stream); | ||
| 29 | let mut writer = stream; | ||
| 30 | for line in reader.lines() { | ||
| 31 | let line = line.unwrap(); | ||
| 32 | println!("[{} @ {}] Received: {}", user, client_addr, line); | ||
| 33 | let response = process_message(&line, &mut user); | ||
| 34 | writeln!(writer, "{}", response).unwrap(); | ||
| 35 | println!("[{} @ {}] Sent: {}", user, client_addr, response); | ||
| 36 | } | ||
| 37 | println!("[{} @ {}] Client disconnected", user, client_addr); | ||
| 38 | } | ||
| 39 | |||
| 40 | fn process_message(msg: &str, user: &mut String) -> String { | ||
| 41 | let parts: Vec<&str> = msg.splitn(5, ' ').collect(); | ||
| 42 | match parts[0] { | ||
| 43 | "GAME_LIST" => { | ||
| 44 | let registry = get_game_registry(); | ||
| 45 | let games: Vec<String> = registry.list_games(); | ||
| 46 | if games.is_empty() { | ||
| 47 | "ERROR No games available".to_string() | ||
| 48 | } else { | ||
| 49 | let game_info: Vec<String> = games | ||
| 50 | .iter() | ||
| 51 | .filter_map(|gt| { | ||
| 52 | registry | ||
| 53 | .get_game(gt) | ||
| 54 | .map(|g| format!("{}:{}", gt, g.get_name())) | ||
| 55 | }) | ||
| 56 | .collect(); | ||
| 57 | format!("GAME_LIST {}", game_info.join(",")) | ||
| 58 | } | ||
| 59 | } | ||
| 60 | "GAME_INFO" => { | ||
| 61 | if parts.len() != 2 { | ||
| 62 | "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>".to_string() | ||
| 63 | } else { | ||
| 64 | let registry = get_game_registry(); | ||
| 65 | match registry.get_game(parts[1]) { | ||
| 66 | Some(game) => format!( | ||
| 67 | "GAME_INFO {} {} {}", | ||
| 68 | parts[1], | ||
| 69 | game.get_name(), | ||
| 70 | game.get_description() | ||
| 71 | ), | ||
| 72 | None => format!("ERROR Unknown game type: {}", parts[1]), | ||
| 73 | } | ||
| 74 | } | ||
| 75 | } | ||
| 76 | "PLAY_START" => { | ||
| 77 | if parts.len() != 3 { | ||
| 78 | "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount>" | ||
| 79 | .to_string() | ||
| 80 | } else { | ||
| 81 | handle_play_start(user, parts[1], parts[2]) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | "REGISTER" => { | ||
| 85 | if parts.len() != 3 { | ||
| 86 | "ERROR Invalid REGISTER format".to_string() | ||
| 87 | } else { | ||
| 88 | let username = parts[1]; | ||
| 89 | let hashed_password = parts[2]; | ||
| 90 | let db = DB_CONN.lock(); | ||
| 91 | match database::add_user(&db, username, hashed_password) { | ||
| 92 | Ok(_) => format!("REGISTERED {}", username), | ||
| 93 | Err(_) => "ERROR User already exists".to_string(), | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | "AUTH" => { | ||
| 98 | if parts.len() != 3 { | ||
| 99 | "ERROR Invalid AUTH format".to_string() | ||
| 100 | } else { | ||
| 101 | let username = parts[1]; | ||
| 102 | let sent_hash = parts[2]; | ||
| 103 | let db = DB_CONN.lock(); | ||
| 104 | if let Some(stored_hash) = database::get_hashed_password(&db, username) { | ||
| 105 | if sent_hash == stored_hash { | ||
| 106 | *user = username.to_string(); | ||
| 107 | format!("OK {}", username) | ||
| 108 | } else { | ||
| 109 | "ERROR Invalid credentials".to_string() | ||
| 110 | } | ||
| 111 | } else { | ||
| 112 | "ERROR User not found".to_string() | ||
| 113 | } | ||
| 114 | } | ||
| 115 | } | ||
| 116 | "PING" => "PONG".to_string(), | ||
| 117 | "INFO" => { | ||
| 118 | if *user == "guest" { | ||
| 119 | "ERROR Not logged in".to_string() | ||
| 120 | } else { | ||
| 121 | let db = DB_CONN.lock(); | ||
| 122 | if let (Some(id), Some(balance)) = ( | ||
| 123 | database::get_account_id(&db, user), | ||
| 124 | database::get_balance(&db, user), | ||
| 125 | ) { | ||
| 126 | format!("INFO {} {} {}", user, id, balance) | ||
| 127 | } else { | ||
| 128 | "ERROR Error retrieving user info".to_string() | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | 10 | } |
| 132 | _ => msg.to_string(), | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | fn handle_play_start(user: &str, game_type: &str, bet_amount_str: &str) -> String { | ||
| 137 | if user == "guest" { | ||
| 138 | return "ERROR Not logged in".to_string(); | ||
| 139 | } | ||
| 140 | |||
| 141 | let bet_amount: i64 = match bet_amount_str.parse() { | ||
| 142 | Ok(amount) => amount, | ||
| 143 | Err(_) => return "ERROR Invalid bet amount".to_string(), | ||
| 144 | }; | 11 | }; |
| 145 | if bet_amount <= 0 { | ||
| 146 | return "ERROR Invalid bet amount".to_string(); | ||
| 147 | } | ||
| 148 | 12 | ||
| 149 | let registry = get_game_registry(); | 13 | let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()); |
| 150 | let game = match registry.get_game(game_type) { | 14 | let mut core = match ServerCore::open(&db_path) { |
| 151 | Some(game) => game, | 15 | Ok(core) => core, |
| 152 | None => return format!("ERROR Unknown game type: {}", game_type), | 16 | Err(error) => { |
| 153 | }; | 17 | eprintln!("Failed to open database {db_path}: {error}"); |
| 154 | 18 | return; | |
| 155 | let db = DB_CONN.lock(); | 19 | } |
| 156 | |||
| 157 | // Validate balance inside a read. | ||
| 158 | let current_balance = match database::get_balance(&db, user) { | ||
| 159 | Some(balance) => balance, | ||
| 160 | None => return "ERROR Could not retrieve balance".to_string(), | ||
| 161 | }; | ||
| 162 | |||
| 163 | if let Err(e) = game.validate_bet(bet_amount as u64, current_balance as u64) { | ||
| 164 | return match e { | ||
| 165 | GameError::InsufficientBalance => "ERROR Insufficient balance".to_string(), | ||
| 166 | GameError::InvalidBet => "ERROR Invalid bet amount".to_string(), | ||
| 167 | _ => "ERROR Bet validation failed".to_string(), | ||
| 168 | }; | ||
| 169 | } | ||
| 170 | |||
| 171 | // Prepare and spin. | ||
| 172 | let game_state = game.prepare_state(user, bet_amount as u64); | ||
| 173 | |||
| 174 | let new_state = match game.process_action(&game_state, "spin", &[]) { | ||
| 175 | Ok(state) => state, | ||
| 176 | Err(e) => return format!("ERROR Failed to spin: {:?}", e), | ||
| 177 | }; | ||
| 178 | |||
| 179 | let result = match game.calculate_result(&new_state) { | ||
| 180 | Ok(result) => result, | ||
| 181 | Err(e) => return format!("ERROR Failed to calculate result: {:?}", e), | ||
| 182 | }; | ||
| 183 | |||
| 184 | // Create session before touching balance. | ||
| 185 | let session_id = SESSIONS.create_session(user, game_type.to_string(), new_state.clone()); | ||
| 186 | |||
| 187 | // All balance mutations in a single transaction. | ||
| 188 | let total_return = result.winnings as i64; | ||
| 189 | let tx_result = db.execute_batch(&format!( | ||
| 190 | "UPDATE users SET balance = balance - {bet_amount} WHERE username = '{user}'; | ||
| 191 | UPDATE users SET balance = balance + {total_return} WHERE username = '{user}';" | ||
| 192 | )); | ||
| 193 | |||
| 194 | if let Err(e) = tx_result { | ||
| 195 | let _ = SESSIONS.remove_session(&session_id); | ||
| 196 | eprintln!("Balance transaction failed for {user}: {e}"); | ||
| 197 | return "ERROR Failed to update balance".to_string(); | ||
| 198 | } | ||
| 199 | |||
| 200 | let new_balance = match database::get_balance(&db, user) { | ||
| 201 | Some(balance) => balance, | ||
| 202 | None => return "ERROR Could not retrieve new balance".to_string(), | ||
| 203 | }; | 20 | }; |
| 204 | 21 | ||
| 205 | let reels = new_state.data.get("reels").cloned().unwrap_or_default(); | 22 | let mut state = ConnectionState::default(); |
| 206 | let win_type = new_state.data.get("win_type").cloned().unwrap_or_default(); | 23 | println!("[TLS Client connected] {client_addr}"); |
| 207 | |||
| 208 | format!( | ||
| 209 | "GAME_RESULT {session_id} {} {} {new_balance} {reels} {win_type}", | ||
| 210 | result.winnings, result.bet_amount, | ||
| 211 | ) | ||
| 212 | } | ||
| 213 | |||
| 214 | pub fn handle_client_tls(mut stream: TlsStream) { | ||
| 215 | let client_addr = stream.peer_addr().unwrap().to_string(); | ||
| 216 | let mut user = "guest".to_string(); | ||
| 217 | println!("[{} @ {}] TLS Client connected", user, client_addr); | ||
| 218 | 24 | ||
| 219 | loop { | 25 | loop { |
| 220 | let mut line = String::new(); | 26 | let mut line = String::new(); |
| 221 | match stream.read_line(&mut line) { | 27 | match stream.read_line(&mut line) { |
| 222 | Ok(0) => break, | 28 | Ok(0) => break, |
| 223 | Ok(_) => {} | 29 | Ok(_) => {} |
| 224 | Err(e) => { | 30 | Err(error) => { |
| 225 | eprintln!("Read error: {}", e); | 31 | eprintln!("Read error: {error}"); |
| 226 | break; | 32 | break; |
| 227 | } | 33 | } |
| 228 | } | 34 | } |
| @@ -232,21 +38,17 @@ pub fn handle_client_tls(mut stream: TlsStream) { | |||
| 232 | continue; | 38 | continue; |
| 233 | } | 39 | } |
| 234 | 40 | ||
| 235 | println!("[{} @ {}] Received: {}", user, client_addr, line); | 41 | let user = state.user().unwrap_or("guest").to_string(); |
| 236 | let response = process_message(line, &mut user); | 42 | println!("[{user} @ {client_addr}] Received: {line}"); |
| 237 | if let Err(e) = stream.write_line(&response) { | 43 | let response = core.handle_message(&mut state, line); |
| 238 | eprintln!("Write error: {}", e); | 44 | if let Err(error) = stream.write_line(&response) { |
| 45 | eprintln!("Write error: {error}"); | ||
| 239 | break; | 46 | break; |
| 240 | } | 47 | } |
| 241 | println!("[{} @ {}] Sent: {}", user, client_addr, response); | 48 | let user = state.user().unwrap_or("guest"); |
| 242 | } | 49 | println!("[{user} @ {client_addr}] Sent: {response}"); |
| 243 | |||
| 244 | // Clean up any active game sessions for this user on disconnect. | ||
| 245 | if user != "guest" { | ||
| 246 | for session in SESSIONS.get_user_sessions(&user) { | ||
| 247 | let _ = SESSIONS.remove_session(&session.session_id); | ||
| 248 | } | ||
| 249 | } | 50 | } |
| 250 | 51 | ||
| 251 | println!("[{} @ {}] TLS Client disconnected", user, client_addr); | 52 | core.disconnect(&state); |
| 53 | println!("[TLS Client disconnected] {client_addr}"); | ||
| 252 | } | 54 | } |
