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/client/repl.rs | |
| parent | Revert "folder restructuring" (diff) | |
| download | zagan-bet-7ca1b5aeacd9633e5a3dca9f7596975dbc686508.tar.gz zagan-bet-7ca1b5aeacd9633e5a3dca9f7596975dbc686508.zip | |
slowly preparing for actual casino
Diffstat (limited to 'src/bin/client/repl.rs')
| -rw-r--r-- | src/bin/client/repl.rs | 156 |
1 files changed, 126 insertions, 30 deletions
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 | } | ||
