diff options
| author | salmonik <raisersoft@proton.me> | 2026-03-01 14:39:56 +0100 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-03-01 14:39:56 +0100 |
| commit | 7ca1b5aeacd9633e5a3dca9f7596975dbc686508 (patch) | |
| tree | 62de1c6d904e4c2dc1cf60539a16ed1dbb07458b /src/bin | |
| parent | Revert "folder restructuring" (diff) | |
| download | zagan-bet-7ca1b5aeacd9633e5a3dca9f7596975dbc686508.tar.gz zagan-bet-7ca1b5aeacd9633e5a3dca9f7596975dbc686508.zip | |
slowly preparing for actual casino
Diffstat (limited to '')
| -rw-r--r-- | src/bin/client/connection.rs | 19 | ||||
| -rw-r--r-- | src/bin/client/database.rs | 54 | ||||
| -rw-r--r-- | src/bin/client/macros.rs | 8 | ||||
| -rw-r--r-- | src/bin/client/main.rs | 2 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 156 | ||||
| -rw-r--r-- | src/bin/server/database.rs | 44 | ||||
| -rw-r--r-- | src/bin/server/handler.rs | 35 |
7 files changed, 213 insertions, 105 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs index 497ed36..b07bf01 100644 --- a/src/bin/client/connection.rs +++ b/src/bin/client/connection.rs | |||
| @@ -4,17 +4,9 @@ use std::sync::mpsc; | |||
| 4 | use std::thread; | 4 | use std::thread; |
| 5 | use std::time::{Duration, Instant}; | 5 | use std::time::{Duration, Instant}; |
| 6 | use rusqlite::Connection; | 6 | use rusqlite::Connection; |
| 7 | use bcrypt::hash_with_salt; | ||
| 8 | use crate::database; | 7 | use crate::database; |
| 9 | 8 | ||
| 10 | const SALT: [u8; 16] = *b"fixedsalt1234567"; | 9 | pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option<String>), Box<dyn std::error::Error>> { |
| 11 | |||
| 12 | fn hash_password(password: &str) -> String { | ||
| 13 | let result = hash_with_salt(password, 10, SALT).unwrap(); | ||
| 14 | result.to_string() | ||
| 15 | } | ||
| 16 | |||
| 17 | pub fn connect(addr: &str, conn: &Connection) -> Result<TcpStream, Box<dyn std::error::Error>> { | ||
| 18 | let (tx, rx) = mpsc::channel(); | 10 | let (tx, rx) = mpsc::channel(); |
| 19 | let addr_str = addr.to_string(); | 11 | let addr_str = addr.to_string(); |
| 20 | thread::spawn(move || { | 12 | thread::spawn(move || { |
| @@ -36,19 +28,20 @@ pub fn connect(addr: &str, conn: &Connection) -> Result<TcpStream, Box<dyn std:: | |||
| 36 | 28 | ||
| 37 | // Check for credentials and auto-auth | 29 | // Check for credentials and auto-auth |
| 38 | if let Some((username, password)) = database::get_credentials(conn, addr) { | 30 | if let Some((username, password)) = database::get_credentials(conn, addr) { |
| 39 | let hashed = hash_password(&password); | 31 | let auth_msg = format!("AUTH {} {}", username, password); |
| 40 | let auth_msg = format!("AUTH {} {}", username, hashed); | ||
| 41 | writeln!(ping_writer, "{}", auth_msg).unwrap(); | 32 | writeln!(ping_writer, "{}", auth_msg).unwrap(); |
| 42 | let mut auth_response = String::new(); | 33 | let mut auth_response = String::new(); |
| 43 | ping_reader.read_line(&mut auth_response).unwrap(); | 34 | ping_reader.read_line(&mut auth_response).unwrap(); |
| 44 | if auth_response.trim().starts_with("OK") { | 35 | if auth_response.trim().starts_with("OK") { |
| 45 | println!("Authenticated as {}", username); | 36 | println!("Authenticated as {}", username); |
| 37 | Ok((s, Some(username))) | ||
| 46 | } else { | 38 | } else { |
| 47 | println!("Authentication failed: {}", auth_response.trim()); | 39 | println!("Authentication failed: {}", auth_response.trim()); |
| 48 | // Do not fail, allow manual login | 40 | Ok((s, None)) |
| 49 | } | 41 | } |
| 42 | } else { | ||
| 43 | Ok((s, None)) | ||
| 50 | } | 44 | } |
| 51 | Ok(s) | ||
| 52 | } | 45 | } |
| 53 | Ok(Err(e)) => Err(Box::new(e)), | 46 | Ok(Err(e)) => Err(Box::new(e)), |
| 54 | Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), | 47 | Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), |
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs index 03a6c45..da069a7 100644 --- a/src/bin/client/database.rs +++ b/src/bin/client/database.rs | |||
| @@ -1,10 +1,4 @@ | |||
| 1 | use rusqlite::Connection; | 1 | use rusqlite::Connection; |
| 2 | use aes_gcm::{Aes256Gcm, Key, Nonce}; | ||
| 3 | use aes_gcm::aead::{Aead, KeyInit}; | ||
| 4 | use pbkdf2::pbkdf2; | ||
| 5 | use hmac::Hmac; | ||
| 6 | use sha2::Sha256; | ||
| 7 | use rand::Rng; | ||
| 8 | 2 | ||
| 9 | pub fn init_db() -> Connection { | 3 | pub fn init_db() -> Connection { |
| 10 | let conn = Connection::open("client.db").unwrap(); | 4 | let conn = Connection::open("client.db").unwrap(); |
| @@ -19,7 +13,7 @@ pub fn init_db() -> Connection { | |||
| 19 | "CREATE TABLE IF NOT EXISTS credentials ( | 13 | "CREATE TABLE IF NOT EXISTS credentials ( |
| 20 | host TEXT PRIMARY KEY, | 14 | host TEXT PRIMARY KEY, |
| 21 | username TEXT NOT NULL, | 15 | username TEXT NOT NULL, |
| 22 | encrypted_password BLOB NOT NULL | 16 | password TEXT NOT NULL |
| 23 | )", | 17 | )", |
| 24 | [], | 18 | [], |
| 25 | ).unwrap(); | 19 | ).unwrap(); |
| @@ -43,52 +37,22 @@ pub fn save_host(conn: &Connection, host: &str) { | |||
| 43 | ).unwrap(); | 37 | ).unwrap(); |
| 44 | } | 38 | } |
| 45 | 39 | ||
| 46 | pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), aes_gcm::Error> { | 40 | pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), Box<dyn std::error::Error>> { |
| 47 | let encrypted = encrypt_password(password)?; | ||
| 48 | conn.execute( | 41 | conn.execute( |
| 49 | "INSERT OR REPLACE INTO credentials (host, username, encrypted_password) VALUES (?1, ?2, ?3)", | 42 | "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", |
| 50 | (host, username, &encrypted), | 43 | (host, username, password), |
| 51 | ).unwrap(); | 44 | )?; |
| 52 | Ok(()) | 45 | Ok(()) |
| 53 | } | 46 | } |
| 54 | 47 | ||
| 55 | pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { | 48 | pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { |
| 56 | let mut stmt = conn.prepare("SELECT username, encrypted_password FROM credentials WHERE host = ?1").unwrap(); | 49 | let mut stmt = conn.prepare("SELECT username, password FROM credentials WHERE host = ?1").unwrap(); |
| 57 | let mut rows = stmt.query([host]).unwrap(); | 50 | let mut rows = stmt.query([host]).unwrap(); |
| 58 | if let Ok(Some(row)) = rows.next() { | 51 | if let Ok(Some(row)) = rows.next() { |
| 59 | let username: String = row.get(0).unwrap(); | 52 | let username: String = row.get(0).unwrap(); |
| 60 | let encrypted: Vec<u8> = row.get(1).unwrap(); | 53 | let password: String = row.get(1).unwrap(); |
| 61 | decrypt_password(&encrypted).map(|password| (username, password)) | 54 | Some((username, password)) |
| 62 | } else { | 55 | } else { |
| 63 | None | 56 | None |
| 64 | } | 57 | } |
| 65 | } | 58 | } \ No newline at end of file |
| 66 | |||
| 67 | fn derive_key() -> Key<Aes256Gcm> { | ||
| 68 | let mut key = [0u8; 32]; | ||
| 69 | let salt = b"client_salt"; // Fixed salt for simplicity | ||
| 70 | pbkdf2::<Hmac<Sha256>>(b"client_key", salt, 10000, &mut key).expect("error deriving key"); | ||
| 71 | key.into() | ||
| 72 | } | ||
| 73 | |||
| 74 | fn encrypt_password(password: &str) -> Result<Vec<u8>, aes_gcm::Error> { | ||
| 75 | let key = derive_key(); | ||
| 76 | let cipher = Aes256Gcm::new(&key); | ||
| 77 | let mut nonce_bytes = [0u8; 12]; | ||
| 78 | rand::thread_rng().fill(&mut nonce_bytes); | ||
| 79 | let nonce = Nonce::from_slice(&nonce_bytes); | ||
| 80 | let ciphertext = cipher.encrypt(nonce, password.as_bytes())?; | ||
| 81 | let mut result = nonce_bytes.to_vec(); | ||
| 82 | result.extend(ciphertext); | ||
| 83 | Ok(result) | ||
| 84 | } | ||
| 85 | |||
| 86 | fn decrypt_password(encrypted: &[u8]) -> Option<String> { | ||
| 87 | if encrypted.len() < 12 { | ||
| 88 | return None; | ||
| 89 | } | ||
| 90 | let key = derive_key(); | ||
| 91 | let cipher = Aes256Gcm::new(&key); | ||
| 92 | let nonce = Nonce::from_slice(&encrypted[..12]); | ||
| 93 | cipher.decrypt(nonce, &encrypted[12..]).ok().and_then(|plaintext| String::from_utf8(plaintext).ok()) | ||
| 94 | } | ||
diff --git a/src/bin/client/macros.rs b/src/bin/client/macros.rs new file mode 100644 index 0000000..f97a4ff --- /dev/null +++ b/src/bin/client/macros.rs | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | macro_rules! println { | ||
| 2 | () => { | ||
| 3 | print!("\r\n") | ||
| 4 | }; | ||
| 5 | ($($arg:tt)*) => { | ||
| 6 | print!("\r{}\n", format_args!($($arg)*)) | ||
| 7 | }; | ||
| 8 | } | ||
diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs index ce9c92f..9c15e9e 100644 --- a/src/bin/client/main.rs +++ b/src/bin/client/main.rs | |||
| @@ -1,3 +1,5 @@ | |||
| 1 | #[macro_use] | ||
| 2 | mod macros; | ||
| 1 | mod repl; | 3 | mod repl; |
| 2 | mod database; | 4 | mod database; |
| 3 | mod connection; | 5 | mod connection; |
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 5559cfd..b2f6ecd 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -2,28 +2,13 @@ use std::io::{BufRead, BufReader, Write, stdout}; | |||
| 2 | use std::net::TcpStream; | 2 | use std::net::TcpStream; |
| 3 | use rusqlite::Connection; | 3 | use rusqlite::Connection; |
| 4 | use std::time::Instant; | 4 | use std::time::Instant; |
| 5 | use bcrypt::hash_with_salt; | ||
| 6 | 5 | ||
| 7 | use crate::connection; | 6 | use crate::connection; |
| 8 | use crate::database; | 7 | use crate::database; |
| 9 | 8 | ||
| 10 | const SALT: [u8; 16] = *b"fixedsalt1234567"; | ||
| 11 | |||
| 12 | fn hash_password(password: &str) -> String { | ||
| 13 | let result = hash_with_salt(password, 10, SALT).unwrap(); | ||
| 14 | result.to_string() | ||
| 15 | } | ||
| 16 | |||
| 17 | use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; | 9 | use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; |
| 18 | 10 | ||
| 19 | macro_rules! println { | 11 | use sha2::{Sha256, Digest}; |
| 20 | () => { | ||
| 21 | print!("\r\n") | ||
| 22 | }; | ||
| 23 | ($($arg:tt)*) => { | ||
| 24 | print!("\r{}\n", format_args!($($arg)*)) | ||
| 25 | }; | ||
| 26 | } | ||
| 27 | 12 | ||
| 28 | pub struct ClientState { | 13 | pub struct ClientState { |
| 29 | pub stream: Option<TcpStream>, | 14 | pub stream: Option<TcpStream>, |
| @@ -32,6 +17,7 @@ pub struct ClientState { | |||
| 32 | pub default_port: u16, | 17 | pub default_port: u16, |
| 33 | pub history: Vec<String>, | 18 | pub history: Vec<String>, |
| 34 | pub history_index: usize, | 19 | pub history_index: usize, |
| 20 | pub logged_in_user: Option<String>, | ||
| 35 | } | 21 | } |
| 36 | 22 | ||
| 37 | impl ClientState { | 23 | impl ClientState { |
| @@ -50,14 +36,16 @@ impl ClientState { | |||
| 50 | default_port, | 36 | default_port, |
| 51 | history: vec![], | 37 | history: vec![], |
| 52 | history_index: 0, | 38 | history_index: 0, |
| 39 | logged_in_user: None, | ||
| 53 | } | 40 | } |
| 54 | } | 41 | } |
| 55 | 42 | ||
| 56 | pub fn connect_to(&mut self, addr: &str) { | 43 | pub fn connect_to(&mut self, addr: &str) { |
| 57 | self.addr = Some(addr.to_string()); | 44 | self.addr = Some(addr.to_string()); |
| 58 | match connection::connect(addr, &self.conn) { | 45 | match connection::connect(addr, &self.conn) { |
| 59 | Ok(s) => { | 46 | Ok((s, authed_user)) => { |
| 60 | self.stream = Some(s); | 47 | self.stream = Some(s); |
| 48 | self.logged_in_user = authed_user; | ||
| 61 | database::save_host(&self.conn, addr); | 49 | database::save_host(&self.conn, addr); |
| 62 | } | 50 | } |
| 63 | Err(e) => println!("Failed to connect: {}", e), | 51 | Err(e) => println!("Failed to connect: {}", e), |
| @@ -142,6 +130,7 @@ impl ClientState { | |||
| 142 | } | 130 | } |
| 143 | "disconnect" => { | 131 | "disconnect" => { |
| 144 | self.stream = None; | 132 | self.stream = None; |
| 133 | self.logged_in_user = None; | ||
| 145 | println!("\nDisconnected"); | 134 | println!("\nDisconnected"); |
| 146 | } | 135 | } |
| 147 | "ping" => { | 136 | "ping" => { |
| @@ -165,7 +154,7 @@ impl ClientState { | |||
| 165 | self.stream = None; | 154 | self.stream = None; |
| 166 | println!("\nConnection lost"); | 155 | println!("\nConnection lost"); |
| 167 | } else { | 156 | } else { |
| 168 | if response.trim() == "PING" { | 157 | if response.trim() == "PONG" { |
| 169 | let elapsed = start.elapsed(); | 158 | let elapsed = start.elapsed(); |
| 170 | println!("\nPong ({:?})", elapsed); | 159 | println!("\nPong ({:?})", elapsed); |
| 171 | } else { | 160 | } else { |
| @@ -187,7 +176,7 @@ impl ClientState { | |||
| 187 | if password.is_empty() { | 176 | if password.is_empty() { |
| 188 | continue 'outer; | 177 | continue 'outer; |
| 189 | } | 178 | } |
| 190 | let hashed = hash_password(&password); | 179 | let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); |
| 191 | let msg = format!("REGISTER {} {}", username, hashed); | 180 | let msg = format!("REGISTER {} {}", username, hashed); |
| 192 | let s = self.stream.as_mut().unwrap(); | 181 | let s = self.stream.as_mut().unwrap(); |
| 193 | let writer_stream = s.try_clone().unwrap(); | 182 | let writer_stream = s.try_clone().unwrap(); |
| @@ -204,7 +193,14 @@ impl ClientState { | |||
| 204 | self.stream = None; | 193 | self.stream = None; |
| 205 | println!("\nConnection lost"); | 194 | println!("\nConnection lost"); |
| 206 | } else { | 195 | } else { |
| 207 | println!("\n{}", response.trim()); | 196 | let resp = response.trim(); |
| 197 | if resp == format!("REGISTERED {}", username) { | ||
| 198 | println!("\nSuccessfully registered as {}", username); | ||
| 199 | } else if resp.starts_with("ERROR ") { | ||
| 200 | println!("\nRegistration failed: {}", &resp[6..]); | ||
| 201 | } else { | ||
| 202 | println!("\n{}", resp); | ||
| 203 | } | ||
| 208 | } | 204 | } |
| 209 | } | 205 | } |
| 210 | "login" => { | 206 | "login" => { |
| @@ -212,6 +208,10 @@ impl ClientState { | |||
| 212 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | 208 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); |
| 213 | continue 'outer; | 209 | continue 'outer; |
| 214 | } | 210 | } |
| 211 | if self.logged_in_user.is_some() { | ||
| 212 | println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap()); | ||
| 213 | continue 'outer; | ||
| 214 | } | ||
| 215 | loop { | 215 | loop { |
| 216 | let username = read_line("\nUsername: "); | 216 | let username = read_line("\nUsername: "); |
| 217 | if username.is_empty() { | 217 | if username.is_empty() { |
| @@ -221,7 +221,7 @@ impl ClientState { | |||
| 221 | if password.is_empty() { | 221 | if password.is_empty() { |
| 222 | break; | 222 | break; |
| 223 | } | 223 | } |
| 224 | let hashed = hash_password(&password); | 224 | let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); |
| 225 | let msg = format!("AUTH {} {}", username, hashed); | 225 | let msg = format!("AUTH {} {}", username, hashed); |
| 226 | let s = self.stream.as_mut().unwrap(); | 226 | let s = self.stream.as_mut().unwrap(); |
| 227 | let writer_stream = s.try_clone().unwrap(); | 227 | let writer_stream = s.try_clone().unwrap(); |
| @@ -240,17 +240,21 @@ impl ClientState { | |||
| 240 | break; | 240 | break; |
| 241 | } else { | 241 | } else { |
| 242 | let resp = response.trim(); | 242 | let resp = response.trim(); |
| 243 | println!("\n{}", resp); | ||
| 244 | if resp.starts_with("OK") { | 243 | if resp.starts_with("OK") { |
| 245 | // Save credentials | 244 | // Save credentials |
| 246 | if let Some(addr) = &self.addr { | 245 | if let Some(addr) = &self.addr { |
| 247 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { | 246 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { |
| 248 | println!("Failed to save credentials: {}", e); | 247 | println!("Failed to save credentials: {}", e); |
| 249 | } | 248 | } |
| 250 | } | 249 | } |
| 250 | self.logged_in_user = Some(username.clone()); | ||
| 251 | println!("\nLogged in as {}", username); | ||
| 251 | break; | 252 | break; |
| 253 | } else if resp.starts_with("ERROR ") { | ||
| 254 | println!("\nAuthentication failed: {}", &resp[6..]); | ||
| 255 | println!("Try again or press Enter to cancel."); | ||
| 252 | } else { | 256 | } else { |
| 253 | println!("Authentication failed, try again or press Enter to cancel."); | 257 | println!("\n{}", resp); |
| 254 | } | 258 | } |
| 255 | } | 259 | } |
| 256 | } | 260 | } |
| @@ -268,14 +272,54 @@ impl ClientState { | |||
| 268 | if password.is_empty() { | 272 | if password.is_empty() { |
| 269 | continue 'outer; | 273 | continue 'outer; |
| 270 | } | 274 | } |
| 275 | let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); | ||
| 271 | if let Some(addr) = &self.addr { | 276 | if let Some(addr) = &self.addr { |
| 272 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { | 277 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { |
| 273 | println!("Failed to save credentials: {}", e); | 278 | println!("Failed to save credentials: {}", e); |
| 274 | } else { | 279 | } else { |
| 275 | println!("Credentials saved for {}", addr); | 280 | println!("Credentials saved for {}", addr); |
| 276 | } | 281 | } |
| 277 | } | 282 | } |
| 278 | } | 283 | } |
| 284 | "logout" => { | ||
| 285 | self.logged_in_user = None; | ||
| 286 | println!("\nLogged out"); | ||
| 287 | } | ||
| 288 | "info" => { | ||
| 289 | if self.stream.is_none() { | ||
| 290 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 291 | continue 'outer; | ||
| 292 | } | ||
| 293 | let s = self.stream.as_mut().unwrap(); | ||
| 294 | let writer_stream = s.try_clone().unwrap(); | ||
| 295 | let mut writer = writer_stream; | ||
| 296 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 297 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 298 | if let Err(_) = writeln!(writer, "INFO") { | ||
| 299 | self.stream = None; | ||
| 300 | println!("\nConnection lost"); | ||
| 301 | continue 'outer; | ||
| 302 | } | ||
| 303 | let mut response = String::new(); | ||
| 304 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 305 | self.stream = None; | ||
| 306 | println!("\nConnection lost"); | ||
| 307 | } else { | ||
| 308 | let resp = response.trim(); | ||
| 309 | if resp.starts_with("INFO ") { | ||
| 310 | let parts: Vec<&str> = resp.split_whitespace().collect(); | ||
| 311 | if parts.len() == 4 { | ||
| 312 | println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); | ||
| 313 | } else { | ||
| 314 | println!("\n{}", resp); | ||
| 315 | } | ||
| 316 | } else if resp.starts_with("ERROR ") { | ||
| 317 | println!("\nError: {}", &resp[6..]); | ||
| 318 | } else { | ||
| 319 | println!("\n{}", resp); | ||
| 320 | } | ||
| 321 | } | ||
| 322 | } | ||
| 279 | "help" => { | 323 | "help" => { |
| 280 | help(); | 324 | help(); |
| 281 | } | 325 | } |
| @@ -295,11 +339,16 @@ impl ClientState { | |||
| 295 | disable_raw_mode().unwrap(); | 339 | disable_raw_mode().unwrap(); |
| 296 | execute!(stdout(), Show).unwrap(); | 340 | execute!(stdout(), Show).unwrap(); |
| 297 | return; | 341 | return; |
| 342 | } else if c == '\t' { | ||
| 343 | self.handle_tab_completion(&mut current_input); | ||
| 298 | } else { | 344 | } else { |
| 299 | current_input.push(c); | 345 | current_input.push(c); |
| 300 | redraw(¤t_input); | 346 | redraw(¤t_input); |
| 301 | } | 347 | } |
| 302 | } | 348 | } |
| 349 | KeyCode::Tab => { | ||
| 350 | self.handle_tab_completion(&mut current_input); | ||
| 351 | } | ||
| 303 | _ => {} | 352 | _ => {} |
| 304 | } | 353 | } |
| 305 | _ => {} | 354 | _ => {} |
| @@ -309,6 +358,58 @@ impl ClientState { | |||
| 309 | disable_raw_mode().unwrap(); | 358 | disable_raw_mode().unwrap(); |
| 310 | execute!(stdout(), Show).unwrap(); | 359 | execute!(stdout(), Show).unwrap(); |
| 311 | } | 360 | } |
| 361 | |||
| 362 | fn handle_tab_completion(&mut self, current_input: &mut String) { | ||
| 363 | let prefix = current_input.trim(); | ||
| 364 | let commands = get_commands(); | ||
| 365 | let matches: Vec<_> = commands.iter().filter(|c| c.starts_with(prefix)).collect(); | ||
| 366 | if matches.len() == 1 { | ||
| 367 | *current_input = matches[0].clone(); | ||
| 368 | redraw(current_input); | ||
| 369 | } else if matches.len() > 1 { | ||
| 370 | disable_raw_mode().unwrap(); | ||
| 371 | execute!(stdout(), Show).unwrap(); | ||
| 372 | println!("\nPossible completions:"); | ||
| 373 | for m in &matches { | ||
| 374 | print!("{} ", m); | ||
| 375 | } | ||
| 376 | println!(); | ||
| 377 | enable_raw_mode().unwrap(); | ||
| 378 | redraw(current_input); | ||
| 379 | } | ||
| 380 | // else do nothing | ||
| 381 | } | ||
| 382 | } | ||
| 383 | |||
| 384 | fn get_commands() -> Vec<String> { | ||
| 385 | vec![ | ||
| 386 | "connect".to_string(), | ||
| 387 | "send".to_string(), | ||
| 388 | "disconnect".to_string(), | ||
| 389 | "ping".to_string(), | ||
| 390 | "login".to_string(), | ||
| 391 | "register".to_string(), | ||
| 392 | "setcred".to_string(), | ||
| 393 | "logout".to_string(), | ||
| 394 | "info".to_string(), | ||
| 395 | "help".to_string(), | ||
| 396 | "quit".to_string(), | ||
| 397 | ] | ||
| 398 | } | ||
| 399 | |||
| 400 | fn help() { | ||
| 401 | println!("\nAvailable commands:\r | ||
| 402 | connect <ip>:<port> - connect to the server\r | ||
| 403 | send <message> - send a message to the server\r | ||
| 404 | disconnect - disconnect from the server\r | ||
| 405 | ping - ping the server\r | ||
| 406 | login - login to the server\r | ||
| 407 | register - register a new user\r | ||
| 408 | setcred - set credentials for a host\r | ||
| 409 | logout - logout from the server\r | ||
| 410 | info - show login info\r | ||
| 411 | help - show this message\r | ||
| 412 | quit - quit the program\n"); | ||
| 312 | } | 413 | } |
| 313 | 414 | ||
| 314 | fn redraw(current_input: &str) { | 415 | fn redraw(current_input: &str) { |
| @@ -326,10 +427,5 @@ fn read_line(prompt: &str) -> String { | |||
| 326 | let mut input = String::new(); | 427 | let mut input = String::new(); |
| 327 | std::io::stdin().read_line(&mut input).unwrap(); | 428 | std::io::stdin().read_line(&mut input).unwrap(); |
| 328 | enable_raw_mode().unwrap(); | 429 | enable_raw_mode().unwrap(); |
| 329 | execute!(stdout(), crossterm::cursor::Hide).unwrap(); | ||
| 330 | input.trim().to_string() | 430 | input.trim().to_string() |
| 331 | } | 431 | } |
| 332 | |||
| 333 | fn help() { | ||
| 334 | println!("Available commands: connect <ip>:<port>, send <message>, disconnect, ping, login, register, setcred, help, quit"); | ||
| 335 | } | ||
diff --git a/src/bin/server/database.rs b/src/bin/server/database.rs index 9c51997..0fac387 100644 --- a/src/bin/server/database.rs +++ b/src/bin/server/database.rs | |||
| @@ -1,22 +1,34 @@ | |||
| 1 | use rusqlite::Connection; | 1 | use rusqlite::Connection; |
| 2 | 2 | ||
| 3 | pub fn init_db() -> Connection { | 3 | pub fn init_db() -> Connection { |
| 4 | let conn = Connection::open("server.db").unwrap(); | 4 | let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()); |
| 5 | let conn = Connection::open(&db_path).unwrap(); | ||
| 5 | conn.execute( | 6 | conn.execute( |
| 6 | "CREATE TABLE IF NOT EXISTS users ( | 7 | "CREATE TABLE IF NOT EXISTS users ( |
| 7 | username TEXT PRIMARY KEY, | 8 | username TEXT PRIMARY KEY, |
| 8 | hashed_password TEXT NOT NULL | 9 | hashed_password TEXT NOT NULL, |
| 10 | account_id INTEGER UNIQUE, | ||
| 11 | balance INTEGER DEFAULT 1000 | ||
| 9 | )", | 12 | )", |
| 10 | [], | 13 | [], |
| 11 | ).unwrap(); | 14 | ).unwrap(); |
| 15 | // 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 balance INTEGER DEFAULT 1000", []); | ||
| 12 | conn | 18 | conn |
| 13 | } | 19 | } |
| 14 | 20 | ||
| 15 | pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> { | 21 | pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> { |
| 16 | conn.execute( | 22 | loop { |
| 17 | "INSERT INTO users (username, hashed_password) VALUES (?1, ?2)", | 23 | let account_id: u32 = rand::random::<u32>(); |
| 18 | [username, hashed_password], | 24 | match conn.execute( |
| 19 | )?; | 25 | "INSERT INTO users (username, hashed_password, account_id, balance) VALUES (?1, ?2, ?3, ?4)", |
| 26 | [username, hashed_password, &account_id.to_string(), "1000"], | ||
| 27 | ) { | ||
| 28 | Ok(_) => break, | ||
| 29 | Err(_) => continue, // Retry if account_id conflict | ||
| 30 | } | ||
| 31 | } | ||
| 20 | Ok(()) | 32 | Ok(()) |
| 21 | } | 33 | } |
| 22 | 34 | ||
| @@ -29,3 +41,23 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> | |||
| 29 | None | 41 | None |
| 30 | } | 42 | } |
| 31 | } | 43 | } |
| 44 | |||
| 45 | 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(); | ||
| 47 | let mut rows = stmt.query([username]).unwrap(); | ||
| 48 | if let Ok(Some(row)) = rows.next() { | ||
| 49 | Some(row.get::<_, u32>(0).ok()?) | ||
| 50 | } else { | ||
| 51 | None | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> { | ||
| 56 | let mut stmt = conn.prepare("SELECT balance FROM users WHERE username = ?1").unwrap(); | ||
| 57 | let mut rows = stmt.query([username]).unwrap(); | ||
| 58 | if let Ok(Some(row)) = rows.next() { | ||
| 59 | Some(row.get::<_, i64>(0).ok()?) | ||
| 60 | } else { | ||
| 61 | None | ||
| 62 | } | ||
| 63 | } | ||
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs index 2132d09..01fd9c9 100644 --- a/src/bin/server/handler.rs +++ b/src/bin/server/handler.rs | |||
| @@ -19,6 +19,7 @@ pub fn handle_client(stream: TcpStream) { | |||
| 19 | println!("[{} @ {}] Received: {}", user, client_addr, line); | 19 | println!("[{} @ {}] Received: {}", user, client_addr, line); |
| 20 | let response = process_message(&line, &mut user); | 20 | let response = process_message(&line, &mut user); |
| 21 | writeln!(writer, "{}", response).unwrap(); | 21 | writeln!(writer, "{}", response).unwrap(); |
| 22 | println!("[{} @ {}] Sent: {}", user, client_addr, response); | ||
| 22 | } | 23 | } |
| 23 | println!("[{} @ {}] Client disconnected", user, client_addr); | 24 | println!("[{} @ {}] Client disconnected", user, client_addr); |
| 24 | } | 25 | } |
| @@ -28,37 +29,49 @@ fn process_message(msg: &str, user: &mut String) -> String { | |||
| 28 | match parts[0] { | 29 | match parts[0] { |
| 29 | "REGISTER" => { | 30 | "REGISTER" => { |
| 30 | if parts.len() != 3 { | 31 | if parts.len() != 3 { |
| 31 | "FAIL Invalid REGISTER format".to_string() | 32 | "ERROR Invalid REGISTER format".to_string() |
| 32 | } else { | 33 | } else { |
| 33 | let username = parts[1]; | 34 | let username = parts[1]; |
| 34 | let hashed_pass = parts[2]; | 35 | let hashed_password = parts[2]; |
| 35 | let db = DB_CONN.lock().unwrap(); | 36 | let db = DB_CONN.lock().unwrap(); |
| 36 | match database::add_user(&db, username, hashed_pass) { | 37 | match database::add_user(&db, username, hashed_password) { |
| 37 | Ok(_) => format!("OK Registered {}", username), | 38 | Ok(_) => format!("REGISTERED {}", username), |
| 38 | Err(_) => "FAIL User already exists".to_string(), | 39 | Err(_) => "ERROR User already exists".to_string(), |
| 39 | } | 40 | } |
| 40 | } | 41 | } |
| 41 | } | 42 | } |
| 42 | "AUTH" => { | 43 | "AUTH" => { |
| 43 | if parts.len() != 3 { | 44 | if parts.len() != 3 { |
| 44 | "FAIL Invalid AUTH format".to_string() | 45 | "ERROR Invalid AUTH format".to_string() |
| 45 | } else { | 46 | } else { |
| 46 | let username = parts[1]; | 47 | let username = parts[1]; |
| 47 | let provided_hash = parts[2]; | 48 | let sent_hash = parts[2]; |
| 48 | let db = DB_CONN.lock().unwrap(); | 49 | let db = DB_CONN.lock().unwrap(); |
| 49 | if let Some(stored_hash) = database::get_hashed_password(&db, username) { | 50 | if let Some(stored_hash) = database::get_hashed_password(&db, username) { |
| 50 | if provided_hash == stored_hash { | 51 | if sent_hash == stored_hash { |
| 51 | *user = username.to_string(); | 52 | *user = username.to_string(); |
| 52 | format!("OK {}", username) | 53 | format!("OK {}", username) |
| 53 | } else { | 54 | } else { |
| 54 | "FAIL Invalid credentials".to_string() | 55 | "ERROR Invalid credentials".to_string() |
| 55 | } | 56 | } |
| 56 | } else { | 57 | } else { |
| 57 | "FAIL User not found".to_string() | 58 | "ERROR User not found".to_string() |
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | "PING" => "PONG".to_string(), | ||
| 63 | "INFO" => { | ||
| 64 | if *user == "guest" { | ||
| 65 | "ERROR Not logged in".to_string() | ||
| 66 | } else { | ||
| 67 | let db = DB_CONN.lock().unwrap(); | ||
| 68 | if let (Some(id), Some(balance)) = (database::get_account_id(&db, user), database::get_balance(&db, user)) { | ||
| 69 | format!("INFO {} {} {}", user, id, balance) | ||
| 70 | } else { | ||
| 71 | "ERROR Error retrieving user info".to_string() | ||
| 58 | } | 72 | } |
| 59 | } | 73 | } |
| 60 | } | 74 | } |
| 61 | "PING" => "PING".to_string(), | ||
| 62 | _ => msg.to_string(), // Echo back for compatibility | 75 | _ => msg.to_string(), // Echo back for compatibility |
| 63 | } | 76 | } |
| 64 | } | 77 | } |
