summaryrefslogtreecommitdiffstats
path: root/src/bin/server
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/server/args.rs11
-rw-r--r--src/bin/server/database.rs26
-rw-r--r--src/bin/server/games/mod.rs4
-rw-r--r--src/bin/server/games/registry.rs55
-rw-r--r--src/bin/server/games/session.rs77
-rw-r--r--src/bin/server/games/slots.rs179
-rw-r--r--src/bin/server/games/traits.rs67
-rw-r--r--src/bin/server/handler.rs177
-rw-r--r--src/bin/server/main.rs34
-rw-r--r--src/bin/server/tls.rs96
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 @@
1pub struct Args { 1pub 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
6pub fn parse_args() -> Args { 7pub 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
21pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> { 25pub 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
35pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { 43pub 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
45pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> { 55pub 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
55pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> { 67pub 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 @@
1pub mod registry;
2pub mod session;
3pub mod slots;
4pub 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
3use crate::games::traits::Game;
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6
7pub type GameRef = Arc<dyn Game + Send + Sync>;
8
9pub struct GameRegistry {
10 games: Arc<Mutex<HashMap<String, GameRef>>>,
11}
12
13impl 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
43use crate::games::slots::SlotsGame;
44use once_cell::sync::Lazy;
45
46static 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
53pub 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
3use crate::games::traits::{GameError, GameState};
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6use uuid::Uuid;
7
8#[derive(Debug, Clone)]
9pub 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
17pub struct SessionManager {
18 sessions: Arc<Mutex<HashMap<String, GameSession>>>,
19}
20
21impl 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 @@
1use crate::games::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use rand::Rng;
3use std::collections::HashMap;
4
5pub struct SlotsGame {
6 symbols: Vec<char>,
7 payouts: HashMap<String, u64>,
8}
9
10impl 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
59impl 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 @@
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub 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)]
15pub enum GamePhase {
16 WaitingToStart,
17 Playing,
18 Finished,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub 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)]
31pub 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
40pub 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 @@
1use crate::database;
2use crate::games::registry::get_game_registry;
3use crate::games::traits::GameError;
4use crate::tls::TlsStream;
5use once_cell::sync::Lazy;
6use rusqlite::Connection;
1use std::io::{BufRead, BufReader, Write}; 7use std::io::{BufRead, BufReader, Write};
2use std::net::TcpStream; 8use std::net::TcpStream;
3use std::sync::Mutex; 9use std::sync::Mutex;
4use once_cell::sync::Lazy;
5use rusqlite::Connection;
6use crate::database;
7 10
8static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db())); 11static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db()));
9 12
13#[allow(dead_code)]
10pub fn handle_client(stream: TcpStream) { 14pub 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
27fn process_message(msg: &str, user: &mut String) -> String { 31fn 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
127fn 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
212pub 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 @@
1mod args; 1mod args;
2mod handler;
3mod database; 2mod database;
3mod games;
4mod handler;
5mod tls;
4 6
5use std::net::TcpListener; 7use std::net::TcpListener;
8use std::sync::Arc;
6use std::thread; 9use std::thread;
7 10
8fn main() { 11fn 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 @@
1use std::io::{Read, Write};
2use std::net::TcpStream;
3
4#[derive(Clone)]
5pub struct TlsServer {
6 acceptor: native_tls::TlsAcceptor,
7}
8
9impl 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
43pub struct TlsStream {
44 stream: native_tls::TlsStream<TcpStream>,
45}
46
47impl 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
82impl Read for TlsStream {
83 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
84 self.stream.read(buf)
85 }
86}
87
88impl 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}