From 7ca1b5aeacd9633e5a3dca9f7596975dbc686508 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Mar 2026 14:39:56 +0100 Subject: slowly preparing for actual casino --- src/bin/client/connection.rs | 19 ++---- src/bin/client/database.rs | 54 +++------------ src/bin/client/macros.rs | 8 +++ src/bin/client/main.rs | 2 + src/bin/client/repl.rs | 156 ++++++++++++++++++++++++++++++++++--------- src/bin/server/database.rs | 44 ++++++++++-- src/bin/server/handler.rs | 35 +++++++--- 7 files changed, 213 insertions(+), 105 deletions(-) create mode 100644 src/bin/client/macros.rs (limited to 'src/bin') 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; use std::thread; use std::time::{Duration, Instant}; use rusqlite::Connection; -use bcrypt::hash_with_salt; use crate::database; -const SALT: [u8; 16] = *b"fixedsalt1234567"; - -fn hash_password(password: &str) -> String { - let result = hash_with_salt(password, 10, SALT).unwrap(); - result.to_string() -} - -pub fn connect(addr: &str, conn: &Connection) -> Result> { +pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option), Box> { let (tx, rx) = mpsc::channel(); let addr_str = addr.to_string(); thread::spawn(move || { @@ -36,19 +28,20 @@ pub fn connect(addr: &str, conn: &Connection) -> Result Err(Box::new(e)), 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 @@ use rusqlite::Connection; -use aes_gcm::{Aes256Gcm, Key, Nonce}; -use aes_gcm::aead::{Aead, KeyInit}; -use pbkdf2::pbkdf2; -use hmac::Hmac; -use sha2::Sha256; -use rand::Rng; pub fn init_db() -> Connection { let conn = Connection::open("client.db").unwrap(); @@ -19,7 +13,7 @@ pub fn init_db() -> Connection { "CREATE TABLE IF NOT EXISTS credentials ( host TEXT PRIMARY KEY, username TEXT NOT NULL, - encrypted_password BLOB NOT NULL + password TEXT NOT NULL )", [], ).unwrap(); @@ -43,52 +37,22 @@ pub fn save_host(conn: &Connection, host: &str) { ).unwrap(); } -pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), aes_gcm::Error> { - let encrypted = encrypt_password(password)?; +pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), Box> { conn.execute( - "INSERT OR REPLACE INTO credentials (host, username, encrypted_password) VALUES (?1, ?2, ?3)", - (host, username, &encrypted), - ).unwrap(); + "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)", + (host, username, password), + )?; Ok(()) } pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { - let mut stmt = conn.prepare("SELECT username, encrypted_password FROM credentials WHERE host = ?1").unwrap(); + let mut stmt = conn.prepare("SELECT username, password FROM credentials WHERE host = ?1").unwrap(); let mut rows = stmt.query([host]).unwrap(); if let Ok(Some(row)) = rows.next() { let username: String = row.get(0).unwrap(); - let encrypted: Vec = row.get(1).unwrap(); - decrypt_password(&encrypted).map(|password| (username, password)) + let password: String = row.get(1).unwrap(); + Some((username, password)) } else { None } -} - -fn derive_key() -> Key { - let mut key = [0u8; 32]; - let salt = b"client_salt"; // Fixed salt for simplicity - pbkdf2::>(b"client_key", salt, 10000, &mut key).expect("error deriving key"); - key.into() -} - -fn encrypt_password(password: &str) -> Result, aes_gcm::Error> { - let key = derive_key(); - let cipher = Aes256Gcm::new(&key); - let mut nonce_bytes = [0u8; 12]; - rand::thread_rng().fill(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - let ciphertext = cipher.encrypt(nonce, password.as_bytes())?; - let mut result = nonce_bytes.to_vec(); - result.extend(ciphertext); - Ok(result) -} - -fn decrypt_password(encrypted: &[u8]) -> Option { - if encrypted.len() < 12 { - return None; - } - let key = derive_key(); - let cipher = Aes256Gcm::new(&key); - let nonce = Nonce::from_slice(&encrypted[..12]); - cipher.decrypt(nonce, &encrypted[12..]).ok().and_then(|plaintext| String::from_utf8(plaintext).ok()) -} +} \ No newline at end of file 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 @@ +macro_rules! println { + () => { + print!("\r\n") + }; + ($($arg:tt)*) => { + print!("\r{}\n", format_args!($($arg)*)) + }; +} 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 @@ +#[macro_use] +mod macros; mod repl; mod database; 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}; use std::net::TcpStream; use rusqlite::Connection; use std::time::Instant; -use bcrypt::hash_with_salt; use crate::connection; use crate::database; -const SALT: [u8; 16] = *b"fixedsalt1234567"; - -fn hash_password(password: &str) -> String { - let result = hash_with_salt(password, 10, SALT).unwrap(); - result.to_string() -} - use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; -macro_rules! println { - () => { - print!("\r\n") - }; - ($($arg:tt)*) => { - print!("\r{}\n", format_args!($($arg)*)) - }; -} +use sha2::{Sha256, Digest}; pub struct ClientState { pub stream: Option, @@ -32,6 +17,7 @@ pub struct ClientState { pub default_port: u16, pub history: Vec, pub history_index: usize, + pub logged_in_user: Option, } impl ClientState { @@ -50,14 +36,16 @@ impl ClientState { default_port, history: vec![], history_index: 0, + logged_in_user: None, } } pub fn connect_to(&mut self, addr: &str) { self.addr = Some(addr.to_string()); match connection::connect(addr, &self.conn) { - Ok(s) => { + Ok((s, authed_user)) => { self.stream = Some(s); + self.logged_in_user = authed_user; database::save_host(&self.conn, addr); } Err(e) => println!("Failed to connect: {}", e), @@ -142,6 +130,7 @@ impl ClientState { } "disconnect" => { self.stream = None; + self.logged_in_user = None; println!("\nDisconnected"); } "ping" => { @@ -165,7 +154,7 @@ impl ClientState { self.stream = None; println!("\nConnection lost"); } else { - if response.trim() == "PING" { + if response.trim() == "PONG" { let elapsed = start.elapsed(); println!("\nPong ({:?})", elapsed); } else { @@ -187,7 +176,7 @@ impl ClientState { if password.is_empty() { continue 'outer; } - let hashed = hash_password(&password); + let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); let msg = format!("REGISTER {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); @@ -204,7 +193,14 @@ impl ClientState { self.stream = None; println!("\nConnection lost"); } else { - println!("\n{}", response.trim()); + let resp = response.trim(); + if resp == format!("REGISTERED {}", username) { + println!("\nSuccessfully registered as {}", username); + } else if resp.starts_with("ERROR ") { + println!("\nRegistration failed: {}", &resp[6..]); + } else { + println!("\n{}", resp); + } } } "login" => { @@ -212,6 +208,10 @@ impl ClientState { println!("\nNot connected. Use 'connect :' first."); continue 'outer; } + if self.logged_in_user.is_some() { + println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap()); + continue 'outer; + } loop { let username = read_line("\nUsername: "); if username.is_empty() { @@ -221,7 +221,7 @@ impl ClientState { if password.is_empty() { break; } - let hashed = hash_password(&password); + let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); let msg = format!("AUTH {} {}", username, hashed); let s = self.stream.as_mut().unwrap(); let writer_stream = s.try_clone().unwrap(); @@ -240,17 +240,21 @@ impl ClientState { break; } else { let resp = response.trim(); - println!("\n{}", resp); if resp.starts_with("OK") { // Save credentials if let Some(addr) = &self.addr { - if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { + if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { println!("Failed to save credentials: {}", e); } } + self.logged_in_user = Some(username.clone()); + println!("\nLogged in as {}", username); break; + } else if resp.starts_with("ERROR ") { + println!("\nAuthentication failed: {}", &resp[6..]); + println!("Try again or press Enter to cancel."); } else { - println!("Authentication failed, try again or press Enter to cancel."); + println!("\n{}", resp); } } } @@ -268,14 +272,54 @@ impl ClientState { if password.is_empty() { continue 'outer; } + let hashed = format!("{:x}", Sha256::digest(password.as_bytes())); if let Some(addr) = &self.addr { - if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { + if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) { println!("Failed to save credentials: {}", e); } else { println!("Credentials saved for {}", addr); } } } + "logout" => { + self.logged_in_user = None; + println!("\nLogged out"); + } + "info" => { + if self.stream.is_none() { + println!("\nNot connected. Use 'connect :' first."); + continue 'outer; + } + let s = self.stream.as_mut().unwrap(); + let writer_stream = s.try_clone().unwrap(); + let mut writer = writer_stream; + let response_reader_stream = s.try_clone().unwrap(); + let mut response_reader = BufReader::new(response_reader_stream); + if let Err(_) = writeln!(writer, "INFO") { + self.stream = None; + println!("\nConnection lost"); + continue 'outer; + } + let mut response = String::new(); + if let Err(_) = response_reader.read_line(&mut response) { + self.stream = None; + println!("\nConnection lost"); + } else { + let resp = response.trim(); + if resp.starts_with("INFO ") { + let parts: Vec<&str> = resp.split_whitespace().collect(); + if parts.len() == 4 { + println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]); + } else { + println!("\n{}", resp); + } + } else if resp.starts_with("ERROR ") { + println!("\nError: {}", &resp[6..]); + } else { + println!("\n{}", resp); + } + } + } "help" => { help(); } @@ -295,11 +339,16 @@ impl ClientState { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); return; + } else if c == '\t' { + self.handle_tab_completion(&mut current_input); } else { current_input.push(c); redraw(¤t_input); } } + KeyCode::Tab => { + self.handle_tab_completion(&mut current_input); + } _ => {} } _ => {} @@ -309,6 +358,58 @@ impl ClientState { disable_raw_mode().unwrap(); execute!(stdout(), Show).unwrap(); } + + fn handle_tab_completion(&mut self, current_input: &mut String) { + let prefix = current_input.trim(); + let commands = get_commands(); + let matches: Vec<_> = commands.iter().filter(|c| c.starts_with(prefix)).collect(); + if matches.len() == 1 { + *current_input = matches[0].clone(); + redraw(current_input); + } else if matches.len() > 1 { + disable_raw_mode().unwrap(); + execute!(stdout(), Show).unwrap(); + println!("\nPossible completions:"); + for m in &matches { + print!("{} ", m); + } + println!(); + enable_raw_mode().unwrap(); + redraw(current_input); + } + // else do nothing + } +} + +fn get_commands() -> Vec { + vec![ + "connect".to_string(), + "send".to_string(), + "disconnect".to_string(), + "ping".to_string(), + "login".to_string(), + "register".to_string(), + "setcred".to_string(), + "logout".to_string(), + "info".to_string(), + "help".to_string(), + "quit".to_string(), + ] +} + +fn help() { + println!("\nAvailable commands:\r + connect : - connect to the server\r + send - send a message to the server\r + disconnect - disconnect from the server\r + ping - ping the server\r + login - login to the server\r + register - register a new user\r + setcred - set credentials for a host\r + logout - logout from the server\r + info - show login info\r + help - show this message\r + quit - quit the program\n"); } fn redraw(current_input: &str) { @@ -326,10 +427,5 @@ fn read_line(prompt: &str) -> String { let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); enable_raw_mode().unwrap(); - execute!(stdout(), crossterm::cursor::Hide).unwrap(); input.trim().to_string() } - -fn help() { - println!("Available commands: connect :, send , disconnect, ping, login, register, setcred, help, quit"); -} 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 @@ use rusqlite::Connection; pub fn init_db() -> Connection { - let conn = Connection::open("server.db").unwrap(); + let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()); + let conn = Connection::open(&db_path).unwrap(); conn.execute( "CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, - hashed_password TEXT NOT NULL + hashed_password TEXT NOT NULL, + account_id INTEGER UNIQUE, + balance INTEGER DEFAULT 1000 )", [], ).unwrap(); + // Add column if not exists (for migration) + let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []); + let _ = conn.execute("ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", []); conn } pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box> { - conn.execute( - "INSERT INTO users (username, hashed_password) VALUES (?1, ?2)", - [username, hashed_password], - )?; + loop { + let account_id: u32 = rand::random::(); + match conn.execute( + "INSERT INTO users (username, hashed_password, account_id, balance) VALUES (?1, ?2, ?3, ?4)", + [username, hashed_password, &account_id.to_string(), "1000"], + ) { + Ok(_) => break, + Err(_) => continue, // Retry if account_id conflict + } + } Ok(()) } @@ -29,3 +41,23 @@ pub fn get_hashed_password(conn: &Connection, username: &str) -> Option None } } + +pub fn get_account_id(conn: &Connection, username: &str) -> Option { + let mut stmt = conn.prepare("SELECT account_id FROM users WHERE username = ?1").unwrap(); + let mut rows = stmt.query([username]).unwrap(); + if let Ok(Some(row)) = rows.next() { + Some(row.get::<_, u32>(0).ok()?) + } else { + None + } +} + +pub fn get_balance(conn: &Connection, username: &str) -> Option { + let mut stmt = conn.prepare("SELECT balance FROM users WHERE username = ?1").unwrap(); + let mut rows = stmt.query([username]).unwrap(); + if let Ok(Some(row)) = rows.next() { + Some(row.get::<_, i64>(0).ok()?) + } else { + None + } +} 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) { println!("[{} @ {}] Received: {}", user, client_addr, line); let response = process_message(&line, &mut user); writeln!(writer, "{}", response).unwrap(); + println!("[{} @ {}] Sent: {}", user, client_addr, response); } println!("[{} @ {}] Client disconnected", user, client_addr); } @@ -28,37 +29,49 @@ fn process_message(msg: &str, user: &mut String) -> String { match parts[0] { "REGISTER" => { if parts.len() != 3 { - "FAIL Invalid REGISTER format".to_string() + "ERROR Invalid REGISTER format".to_string() } else { let username = parts[1]; - let hashed_pass = parts[2]; + let hashed_password = parts[2]; let db = DB_CONN.lock().unwrap(); - match database::add_user(&db, username, hashed_pass) { - Ok(_) => format!("OK Registered {}", username), - Err(_) => "FAIL User already exists".to_string(), + match database::add_user(&db, username, hashed_password) { + Ok(_) => format!("REGISTERED {}", username), + Err(_) => "ERROR User already exists".to_string(), } } } "AUTH" => { if parts.len() != 3 { - "FAIL Invalid AUTH format".to_string() + "ERROR Invalid AUTH format".to_string() } else { let username = parts[1]; - let provided_hash = parts[2]; + let sent_hash = parts[2]; let db = DB_CONN.lock().unwrap(); if let Some(stored_hash) = database::get_hashed_password(&db, username) { - if provided_hash == stored_hash { + if sent_hash == stored_hash { *user = username.to_string(); format!("OK {}", username) } else { - "FAIL Invalid credentials".to_string() + "ERROR Invalid credentials".to_string() } } else { - "FAIL User not found".to_string() + "ERROR User not found".to_string() + } + } + } + "PING" => "PONG".to_string(), + "INFO" => { + if *user == "guest" { + "ERROR Not logged in".to_string() + } else { + let db = DB_CONN.lock().unwrap(); + if let (Some(id), Some(balance)) = (database::get_account_id(&db, user), database::get_balance(&db, user)) { + format!("INFO {} {} {}", user, id, balance) + } else { + "ERROR Error retrieving user info".to_string() } } } - "PING" => "PING".to_string(), _ => msg.to_string(), // Echo back for compatibility } } -- cgit v1.2.3