summaryrefslogtreecommitdiffstats
path: root/src/bin/client/repl.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-03-01 14:39:56 +0100
committersalmonik <raisersoft@proton.me>2026-03-01 14:39:56 +0100
commit7ca1b5aeacd9633e5a3dca9f7596975dbc686508 (patch)
tree62de1c6d904e4c2dc1cf60539a16ed1dbb07458b /src/bin/client/repl.rs
parentRevert "folder restructuring" (diff)
downloadzagan-bet-7ca1b5aeacd9633e5a3dca9f7596975dbc686508.tar.gz
zagan-bet-7ca1b5aeacd9633e5a3dca9f7596975dbc686508.zip
slowly preparing for actual casino
Diffstat (limited to '')
-rw-r--r--src/bin/client/repl.rs156
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};
2use std::net::TcpStream; 2use std::net::TcpStream;
3use rusqlite::Connection; 3use rusqlite::Connection;
4use std::time::Instant; 4use std::time::Instant;
5use bcrypt::hash_with_salt;
6 5
7use crate::connection; 6use crate::connection;
8use crate::database; 7use crate::database;
9 8
10const SALT: [u8; 16] = *b"fixedsalt1234567";
11
12fn hash_password(password: &str) -> String {
13 let result = hash_with_salt(password, 10, SALT).unwrap();
14 result.to_string()
15}
16
17use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; 9use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute};
18 10
19macro_rules! println { 11use sha2::{Sha256, Digest};
20 () => {
21 print!("\r\n")
22 };
23 ($($arg:tt)*) => {
24 print!("\r{}\n", format_args!($($arg)*))
25 };
26}
27 12
28pub struct ClientState { 13pub 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
37impl ClientState { 23impl 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(&current_input); 346 redraw(&current_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
384fn 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
400fn 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
314fn redraw(current_input: &str) { 415fn 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
333fn help() {
334 println!("Available commands: connect <ip>:<port>, send <message>, disconnect, ping, login, register, setcred, help, quit");
335}