diff options
| author | salmonik <raisersoft@proton.me> | 2026-02-28 21:10:20 +0100 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-02-28 21:10:20 +0100 |
| commit | 9e3757eea25b71461c45521dd1ef6c17be34744f (patch) | |
| tree | 74f598beb0ccf8370c920031df7b981294bcdefe /src/bin | |
| parent | folder restructuring (diff) | |
| download | zagan-bet-9e3757eea25b71461c45521dd1ef6c17be34744f.tar.gz zagan-bet-9e3757eea25b71461c45521dd1ef6c17be34744f.zip | |
Revert "folder restructuring"
This reverts commit 17082597464198fe84be80313b7d51e1e57de639.
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/client/connection.rs | 57 | ||||
| -rw-r--r-- | src/bin/client/database.rs | 94 | ||||
| -rw-r--r-- | src/bin/client/main.rs | 17 | ||||
| -rw-r--r-- | src/bin/client/mod.rs | 4 | ||||
| -rw-r--r-- | src/bin/client/platform.rs | 28 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 335 | ||||
| -rw-r--r-- | src/bin/server/args.rs | 41 | ||||
| -rw-r--r-- | src/bin/server/database.rs | 31 | ||||
| -rw-r--r-- | src/bin/server/handler.rs | 64 | ||||
| -rw-r--r-- | src/bin/server/main.rs | 24 | ||||
| -rw-r--r-- | src/bin/server/mod.rs | 3 |
11 files changed, 698 insertions, 0 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs new file mode 100644 index 0000000..497ed36 --- /dev/null +++ b/src/bin/client/connection.rs | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | use std::io::{BufRead, BufReader, Write}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | use std::sync::mpsc; | ||
| 4 | use std::thread; | ||
| 5 | use std::time::{Duration, Instant}; | ||
| 6 | use rusqlite::Connection; | ||
| 7 | use bcrypt::hash_with_salt; | ||
| 8 | use crate::database; | ||
| 9 | |||
| 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 | pub fn connect(addr: &str, conn: &Connection) -> Result<TcpStream, Box<dyn std::error::Error>> { | ||
| 18 | let (tx, rx) = mpsc::channel(); | ||
| 19 | let addr_str = addr.to_string(); | ||
| 20 | thread::spawn(move || { | ||
| 21 | match TcpStream::connect(&addr_str) { | ||
| 22 | Ok(s) => tx.send(Ok(s)).unwrap(), | ||
| 23 | Err(e) => tx.send(Err(e)).unwrap(), | ||
| 24 | } | ||
| 25 | }); | ||
| 26 | match rx.recv_timeout(Duration::from_secs(3)) { | ||
| 27 | Ok(Ok(s)) => { | ||
| 28 | let mut ping_writer = s.try_clone().unwrap(); | ||
| 29 | let mut ping_reader = BufReader::new(s.try_clone().unwrap()); | ||
| 30 | let start = Instant::now(); | ||
| 31 | writeln!(ping_writer, "PING").unwrap(); | ||
| 32 | let mut response = String::new(); | ||
| 33 | ping_reader.read_line(&mut response).unwrap(); | ||
| 34 | let elapsed = start.elapsed(); | ||
| 35 | println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed); | ||
| 36 | |||
| 37 | // Check for credentials and auto-auth | ||
| 38 | if let Some((username, password)) = database::get_credentials(conn, addr) { | ||
| 39 | let hashed = hash_password(&password); | ||
| 40 | let auth_msg = format!("AUTH {} {}", username, hashed); | ||
| 41 | writeln!(ping_writer, "{}", auth_msg).unwrap(); | ||
| 42 | let mut auth_response = String::new(); | ||
| 43 | ping_reader.read_line(&mut auth_response).unwrap(); | ||
| 44 | if auth_response.trim().starts_with("OK") { | ||
| 45 | println!("Authenticated as {}", username); | ||
| 46 | } else { | ||
| 47 | println!("Authentication failed: {}", auth_response.trim()); | ||
| 48 | // Do not fail, allow manual login | ||
| 49 | } | ||
| 50 | } | ||
| 51 | Ok(s) | ||
| 52 | } | ||
| 53 | Ok(Err(e)) => Err(Box::new(e)), | ||
| 54 | Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), | ||
| 55 | Err(_) => Err("Connection attempt failed".into()), | ||
| 56 | } | ||
| 57 | } | ||
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs new file mode 100644 index 0000000..03a6c45 --- /dev/null +++ b/src/bin/client/database.rs | |||
| @@ -0,0 +1,94 @@ | |||
| 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 | |||
| 9 | pub fn init_db() -> Connection { | ||
| 10 | let conn = Connection::open("client.db").unwrap(); | ||
| 11 | conn.execute( | ||
| 12 | "CREATE TABLE IF NOT EXISTS hosts ( | ||
| 13 | host TEXT PRIMARY KEY, | ||
| 14 | last_used DATETIME DEFAULT CURRENT_TIMESTAMP | ||
| 15 | )", | ||
| 16 | [], | ||
| 17 | ).unwrap(); | ||
| 18 | conn.execute( | ||
| 19 | "CREATE TABLE IF NOT EXISTS credentials ( | ||
| 20 | host TEXT PRIMARY KEY, | ||
| 21 | username TEXT NOT NULL, | ||
| 22 | encrypted_password BLOB NOT NULL | ||
| 23 | )", | ||
| 24 | [], | ||
| 25 | ).unwrap(); | ||
| 26 | conn | ||
| 27 | } | ||
| 28 | |||
| 29 | pub fn get_last_host(conn: &Connection) -> Option<String> { | ||
| 30 | let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap(); | ||
| 31 | let mut rows = stmt.query([]).unwrap(); | ||
| 32 | if let Ok(Some(row)) = rows.next() { | ||
| 33 | row.get(0).ok() | ||
| 34 | } else { | ||
| 35 | None | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | pub fn save_host(conn: &Connection, host: &str) { | ||
| 40 | conn.execute( | ||
| 41 | "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", | ||
| 42 | [host], | ||
| 43 | ).unwrap(); | ||
| 44 | } | ||
| 45 | |||
| 46 | pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), aes_gcm::Error> { | ||
| 47 | let encrypted = encrypt_password(password)?; | ||
| 48 | conn.execute( | ||
| 49 | "INSERT OR REPLACE INTO credentials (host, username, encrypted_password) VALUES (?1, ?2, ?3)", | ||
| 50 | (host, username, &encrypted), | ||
| 51 | ).unwrap(); | ||
| 52 | Ok(()) | ||
| 53 | } | ||
| 54 | |||
| 55 | 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(); | ||
| 57 | let mut rows = stmt.query([host]).unwrap(); | ||
| 58 | if let Ok(Some(row)) = rows.next() { | ||
| 59 | let username: String = row.get(0).unwrap(); | ||
| 60 | let encrypted: Vec<u8> = row.get(1).unwrap(); | ||
| 61 | decrypt_password(&encrypted).map(|password| (username, password)) | ||
| 62 | } else { | ||
| 63 | None | ||
| 64 | } | ||
| 65 | } | ||
| 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/main.rs b/src/bin/client/main.rs new file mode 100644 index 0000000..ce9c92f --- /dev/null +++ b/src/bin/client/main.rs | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | mod repl; | ||
| 2 | mod database; | ||
| 3 | mod connection; | ||
| 4 | mod platform; | ||
| 5 | |||
| 6 | use repl::ClientState; | ||
| 7 | |||
| 8 | fn main() { | ||
| 9 | #[cfg(windows)] | ||
| 10 | platform::win::enable_vt().unwrap(); | ||
| 11 | |||
| 12 | let mut state = ClientState::new(); | ||
| 13 | if let Some(host) = state.addr.clone() { | ||
| 14 | state.connect_to(&host); | ||
| 15 | } | ||
| 16 | state.run(); | ||
| 17 | } | ||
diff --git a/src/bin/client/mod.rs b/src/bin/client/mod.rs new file mode 100644 index 0000000..938d862 --- /dev/null +++ b/src/bin/client/mod.rs | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | pub mod database; | ||
| 2 | pub mod connection; | ||
| 3 | pub mod platform; | ||
| 4 | pub mod repl; | ||
diff --git a/src/bin/client/platform.rs b/src/bin/client/platform.rs new file mode 100644 index 0000000..817c39d --- /dev/null +++ b/src/bin/client/platform.rs | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | #[cfg(windows)] | ||
| 2 | pub mod win { | ||
| 3 | use std::io; | ||
| 4 | use winapi::um::consoleapi::GetConsoleMode; | ||
| 5 | use winapi::um::consoleapi::SetConsoleMode; | ||
| 6 | use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; | ||
| 7 | use winapi::um::wincon::ENABLE_ECHO_INPUT; | ||
| 8 | use winapi::um::winbase::STD_OUTPUT_HANDLE; | ||
| 9 | use winapi::um::processenv::GetStdHandle; | ||
| 10 | use winapi::shared::minwindef::DWORD; | ||
| 11 | |||
| 12 | pub fn enable_vt() -> io::Result<()> { | ||
| 13 | unsafe { | ||
| 14 | let h = GetStdHandle(STD_OUTPUT_HANDLE); | ||
| 15 | if h.is_null() { return Err(io::Error::last_os_error()); } | ||
| 16 | |||
| 17 | let mut mode: DWORD = 0; | ||
| 18 | if GetConsoleMode(h, &mut mode) == 0 { | ||
| 19 | return Err(io::Error::last_os_error()); | ||
| 20 | } | ||
| 21 | let new = (mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) & !ENABLE_ECHO_INPUT; | ||
| 22 | if SetConsoleMode(h, new) == 0 { | ||
| 23 | return Err(io::Error::last_os_error()); | ||
| 24 | } | ||
| 25 | Ok(()) | ||
| 26 | } | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs new file mode 100644 index 0000000..5559cfd --- /dev/null +++ b/src/bin/client/repl.rs | |||
| @@ -0,0 +1,335 @@ | |||
| 1 | use std::io::{BufRead, BufReader, Write, stdout}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | use rusqlite::Connection; | ||
| 4 | use std::time::Instant; | ||
| 5 | use bcrypt::hash_with_salt; | ||
| 6 | |||
| 7 | use crate::connection; | ||
| 8 | use crate::database; | ||
| 9 | |||
| 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}; | ||
| 18 | |||
| 19 | macro_rules! println { | ||
| 20 | () => { | ||
| 21 | print!("\r\n") | ||
| 22 | }; | ||
| 23 | ($($arg:tt)*) => { | ||
| 24 | print!("\r{}\n", format_args!($($arg)*)) | ||
| 25 | }; | ||
| 26 | } | ||
| 27 | |||
| 28 | pub struct ClientState { | ||
| 29 | pub stream: Option<TcpStream>, | ||
| 30 | pub conn: Connection, | ||
| 31 | pub addr: Option<String>, | ||
| 32 | pub default_port: u16, | ||
| 33 | pub history: Vec<String>, | ||
| 34 | pub history_index: usize, | ||
| 35 | } | ||
| 36 | |||
| 37 | impl ClientState { | ||
| 38 | pub fn new() -> Self { | ||
| 39 | let conn = database::init_db(); | ||
| 40 | let default_ip = "127.0.0.1".to_string(); | ||
| 41 | let default_port = 11525; | ||
| 42 | let last_host = database::get_last_host(&conn); | ||
| 43 | let default_host = format!("{}:{}", default_ip, default_port); | ||
| 44 | let addr = last_host.or(Some(default_host)); | ||
| 45 | |||
| 46 | Self { | ||
| 47 | stream: None, | ||
| 48 | conn, | ||
| 49 | addr, | ||
| 50 | default_port, | ||
| 51 | history: vec![], | ||
| 52 | history_index: 0, | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | pub fn connect_to(&mut self, addr: &str) { | ||
| 57 | self.addr = Some(addr.to_string()); | ||
| 58 | match connection::connect(addr, &self.conn) { | ||
| 59 | Ok(s) => { | ||
| 60 | self.stream = Some(s); | ||
| 61 | database::save_host(&self.conn, addr); | ||
| 62 | } | ||
| 63 | Err(e) => println!("Failed to connect: {}", e), | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | pub fn run(&mut self) { | ||
| 68 | enable_raw_mode().unwrap(); | ||
| 69 | 'outer: loop { | ||
| 70 | let mut current_input = String::new(); | ||
| 71 | self.history_index = self.history.len(); | ||
| 72 | redraw(""); | ||
| 73 | loop { | ||
| 74 | match read().unwrap() { | ||
| 75 | Event::Key(key) => match key.code { | ||
| 76 | KeyCode::Up => { | ||
| 77 | if self.history_index > 0 { | ||
| 78 | self.history_index -= 1; | ||
| 79 | current_input = self.history[self.history_index].clone(); | ||
| 80 | redraw(¤t_input); | ||
| 81 | } | ||
| 82 | } | ||
| 83 | KeyCode::Down => { | ||
| 84 | if self.history_index < self.history.len() { | ||
| 85 | self.history_index += 1; | ||
| 86 | current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() }; | ||
| 87 | redraw(¤t_input); | ||
| 88 | } | ||
| 89 | } | ||
| 90 | KeyCode::Enter => { | ||
| 91 | let line = current_input.trim().to_string(); | ||
| 92 | if !line.is_empty() { | ||
| 93 | self.history.push(line.clone()); | ||
| 94 | } | ||
| 95 | // Process command | ||
| 96 | let parts: Vec<&str> = line.splitn(2, ' ').collect(); | ||
| 97 | match parts[0] { | ||
| 98 | "connect" => { | ||
| 99 | let addr_str = if parts.len() < 2 { | ||
| 100 | if let Some(h) = &self.addr { | ||
| 101 | h.clone() | ||
| 102 | } else { | ||
| 103 | println!("\nNo host specified and no previous connection. Usage: connect <ip>:<port>"); | ||
| 104 | continue 'outer; | ||
| 105 | } | ||
| 106 | } else { | ||
| 107 | let mut a = parts[1].to_string(); | ||
| 108 | if !a.contains(':') { | ||
| 109 | a = format!("{}:{}", a, self.default_port); | ||
| 110 | } | ||
| 111 | a | ||
| 112 | }; | ||
| 113 | self.connect_to(&addr_str); | ||
| 114 | } | ||
| 115 | "send" => { | ||
| 116 | if self.stream.is_none() { | ||
| 117 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 118 | continue 'outer; | ||
| 119 | } | ||
| 120 | if parts.len() < 2 { | ||
| 121 | println!("\nUsage: send <message>"); | ||
| 122 | continue 'outer; | ||
| 123 | } | ||
| 124 | let msg = parts[1]; | ||
| 125 | let s = self.stream.as_mut().unwrap(); | ||
| 126 | let writer_stream = s.try_clone().unwrap(); | ||
| 127 | let mut writer = writer_stream; | ||
| 128 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 129 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 130 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 131 | self.stream = None; | ||
| 132 | println!("\nConnection lost"); | ||
| 133 | continue 'outer; | ||
| 134 | } | ||
| 135 | let mut response = String::new(); | ||
| 136 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 137 | self.stream = None; | ||
| 138 | println!("\nConnection lost"); | ||
| 139 | } else { | ||
| 140 | println!("\nEcho: {}", response.trim()); | ||
| 141 | } | ||
| 142 | } | ||
| 143 | "disconnect" => { | ||
| 144 | self.stream = None; | ||
| 145 | println!("\nDisconnected"); | ||
| 146 | } | ||
| 147 | "ping" => { | ||
| 148 | if self.stream.is_none() { | ||
| 149 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 150 | continue 'outer; | ||
| 151 | } | ||
| 152 | let s = self.stream.as_mut().unwrap(); | ||
| 153 | let writer_stream = s.try_clone().unwrap(); | ||
| 154 | let mut writer = writer_stream; | ||
| 155 | let start = Instant::now(); | ||
| 156 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 157 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 158 | if let Err(_) = writeln!(writer, "PING") { | ||
| 159 | self.stream = None; | ||
| 160 | println!("\nConnection lost"); | ||
| 161 | continue 'outer; | ||
| 162 | } | ||
| 163 | let mut response = String::new(); | ||
| 164 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 165 | self.stream = None; | ||
| 166 | println!("\nConnection lost"); | ||
| 167 | } else { | ||
| 168 | if response.trim() == "PING" { | ||
| 169 | let elapsed = start.elapsed(); | ||
| 170 | println!("\nPong ({:?})", elapsed); | ||
| 171 | } else { | ||
| 172 | self.stream = None; | ||
| 173 | println!("\nConnection lost"); | ||
| 174 | } | ||
| 175 | } | ||
| 176 | } | ||
| 177 | "register" => { | ||
| 178 | if self.stream.is_none() { | ||
| 179 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 180 | continue 'outer; | ||
| 181 | } | ||
| 182 | let username = read_line("\nUsername: "); | ||
| 183 | if username.is_empty() { | ||
| 184 | continue 'outer; | ||
| 185 | } | ||
| 186 | let password = read_line("Password: "); | ||
| 187 | if password.is_empty() { | ||
| 188 | continue 'outer; | ||
| 189 | } | ||
| 190 | let hashed = hash_password(&password); | ||
| 191 | let msg = format!("REGISTER {} {}", username, hashed); | ||
| 192 | let s = self.stream.as_mut().unwrap(); | ||
| 193 | let writer_stream = s.try_clone().unwrap(); | ||
| 194 | let mut writer = writer_stream; | ||
| 195 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 196 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 197 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 198 | self.stream = None; | ||
| 199 | println!("\nConnection lost"); | ||
| 200 | continue 'outer; | ||
| 201 | } | ||
| 202 | let mut response = String::new(); | ||
| 203 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 204 | self.stream = None; | ||
| 205 | println!("\nConnection lost"); | ||
| 206 | } else { | ||
| 207 | println!("\n{}", response.trim()); | ||
| 208 | } | ||
| 209 | } | ||
| 210 | "login" => { | ||
| 211 | if self.stream.is_none() { | ||
| 212 | println!("\nNot connected. Use 'connect <ip>:<port>' first."); | ||
| 213 | continue 'outer; | ||
| 214 | } | ||
| 215 | loop { | ||
| 216 | let username = read_line("\nUsername: "); | ||
| 217 | if username.is_empty() { | ||
| 218 | break; | ||
| 219 | } | ||
| 220 | let password = read_line("Password: "); | ||
| 221 | if password.is_empty() { | ||
| 222 | break; | ||
| 223 | } | ||
| 224 | let hashed = hash_password(&password); | ||
| 225 | let msg = format!("AUTH {} {}", username, hashed); | ||
| 226 | let s = self.stream.as_mut().unwrap(); | ||
| 227 | let writer_stream = s.try_clone().unwrap(); | ||
| 228 | let mut writer = writer_stream; | ||
| 229 | let response_reader_stream = s.try_clone().unwrap(); | ||
| 230 | let mut response_reader = BufReader::new(response_reader_stream); | ||
| 231 | if let Err(_) = writeln!(writer, "{}", msg) { | ||
| 232 | self.stream = None; | ||
| 233 | println!("\nConnection lost"); | ||
| 234 | break; | ||
| 235 | } | ||
| 236 | let mut response = String::new(); | ||
| 237 | if let Err(_) = response_reader.read_line(&mut response) { | ||
| 238 | self.stream = None; | ||
| 239 | println!("\nConnection lost"); | ||
| 240 | break; | ||
| 241 | } else { | ||
| 242 | let resp = response.trim(); | ||
| 243 | println!("\n{}", resp); | ||
| 244 | if resp.starts_with("OK") { | ||
| 245 | // Save credentials | ||
| 246 | if let Some(addr) = &self.addr { | ||
| 247 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { | ||
| 248 | println!("Failed to save credentials: {}", e); | ||
| 249 | } | ||
| 250 | } | ||
| 251 | break; | ||
| 252 | } else { | ||
| 253 | println!("Authentication failed, try again or press Enter to cancel."); | ||
| 254 | } | ||
| 255 | } | ||
| 256 | } | ||
| 257 | } | ||
| 258 | "setcred" => { | ||
| 259 | if self.addr.is_none() { | ||
| 260 | println!("\nNo host connected. Use 'connect <ip>:<port>' first."); | ||
| 261 | continue 'outer; | ||
| 262 | } | ||
| 263 | let username = read_line("\nUsername: "); | ||
| 264 | if username.is_empty() { | ||
| 265 | continue 'outer; | ||
| 266 | } | ||
| 267 | let password = read_line("Password: "); | ||
| 268 | if password.is_empty() { | ||
| 269 | continue 'outer; | ||
| 270 | } | ||
| 271 | if let Some(addr) = &self.addr { | ||
| 272 | if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { | ||
| 273 | println!("Failed to save credentials: {}", e); | ||
| 274 | } else { | ||
| 275 | println!("Credentials saved for {}", addr); | ||
| 276 | } | ||
| 277 | } | ||
| 278 | } | ||
| 279 | "help" => { | ||
| 280 | help(); | ||
| 281 | } | ||
| 282 | "quit" => break 'outer, | ||
| 283 | _ => help(), | ||
| 284 | } | ||
| 285 | break; | ||
| 286 | } | ||
| 287 | KeyCode::Backspace => { | ||
| 288 | if !current_input.is_empty() { | ||
| 289 | current_input.pop(); | ||
| 290 | redraw(¤t_input); | ||
| 291 | } | ||
| 292 | } | ||
| 293 | KeyCode::Char(c) => { | ||
| 294 | if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { | ||
| 295 | disable_raw_mode().unwrap(); | ||
| 296 | execute!(stdout(), Show).unwrap(); | ||
| 297 | return; | ||
| 298 | } else { | ||
| 299 | current_input.push(c); | ||
| 300 | redraw(¤t_input); | ||
| 301 | } | ||
| 302 | } | ||
| 303 | _ => {} | ||
| 304 | } | ||
| 305 | _ => {} | ||
| 306 | } | ||
| 307 | } | ||
| 308 | } | ||
| 309 | disable_raw_mode().unwrap(); | ||
| 310 | execute!(stdout(), Show).unwrap(); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | fn redraw(current_input: &str) { | ||
| 315 | execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); | ||
| 316 | print!("> {}", current_input); | ||
| 317 | stdout().flush().unwrap(); | ||
| 318 | execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap(); | ||
| 319 | } | ||
| 320 | |||
| 321 | fn read_line(prompt: &str) -> String { | ||
| 322 | disable_raw_mode().unwrap(); | ||
| 323 | execute!(stdout(), Show).unwrap(); | ||
| 324 | print!("{}", prompt); | ||
| 325 | stdout().flush().unwrap(); | ||
| 326 | let mut input = String::new(); | ||
| 327 | std::io::stdin().read_line(&mut input).unwrap(); | ||
| 328 | enable_raw_mode().unwrap(); | ||
| 329 | execute!(stdout(), crossterm::cursor::Hide).unwrap(); | ||
| 330 | input.trim().to_string() | ||
| 331 | } | ||
| 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/args.rs b/src/bin/server/args.rs new file mode 100644 index 0000000..31c0acc --- /dev/null +++ b/src/bin/server/args.rs | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | pub struct Args { | ||
| 2 | pub ip: String, | ||
| 3 | pub port: u16, | ||
| 4 | } | ||
| 5 | |||
| 6 | pub fn parse_args() -> Args { | ||
| 7 | let args: Vec<String> = std::env::args().collect(); | ||
| 8 | let mut ip = "127.0.0.1".to_string(); | ||
| 9 | let mut port = 11525; | ||
| 10 | let mut i = 1; | ||
| 11 | while i < args.len() { | ||
| 12 | if args[i] == "-p" { | ||
| 13 | if i + 1 < args.len() { | ||
| 14 | port = args[i + 1].parse::<u16>().unwrap_or_else(|_| { | ||
| 15 | eprintln!("Invalid port number: {}", args[i + 1]); | ||
| 16 | std::process::exit(1); | ||
| 17 | }); | ||
| 18 | i += 2; | ||
| 19 | } else { | ||
| 20 | eprintln!("-p requires a port number"); | ||
| 21 | std::process::exit(1); | ||
| 22 | } | ||
| 23 | } else if args[i] == "-i" { | ||
| 24 | if i + 1 < args.len() { | ||
| 25 | ip = args[i + 1].clone(); | ||
| 26 | if ip != "127.0.0.1" && ip != "0.0.0.0" { | ||
| 27 | eprintln!("Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0", ip); | ||
| 28 | std::process::exit(1); | ||
| 29 | } | ||
| 30 | i += 2; | ||
| 31 | } else { | ||
| 32 | eprintln!("-i requires an IP address"); | ||
| 33 | std::process::exit(1); | ||
| 34 | } | ||
| 35 | } else { | ||
| 36 | eprintln!("Unknown argument: {}", args[i]); | ||
| 37 | std::process::exit(1); | ||
| 38 | } | ||
| 39 | } | ||
| 40 | Args { ip, port } | ||
| 41 | } | ||
diff --git a/src/bin/server/database.rs b/src/bin/server/database.rs new file mode 100644 index 0000000..9c51997 --- /dev/null +++ b/src/bin/server/database.rs | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | use rusqlite::Connection; | ||
| 2 | |||
| 3 | pub fn init_db() -> Connection { | ||
| 4 | let conn = Connection::open("server.db").unwrap(); | ||
| 5 | conn.execute( | ||
| 6 | "CREATE TABLE IF NOT EXISTS users ( | ||
| 7 | username TEXT PRIMARY KEY, | ||
| 8 | hashed_password TEXT NOT NULL | ||
| 9 | )", | ||
| 10 | [], | ||
| 11 | ).unwrap(); | ||
| 12 | conn | ||
| 13 | } | ||
| 14 | |||
| 15 | pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> { | ||
| 16 | conn.execute( | ||
| 17 | "INSERT INTO users (username, hashed_password) VALUES (?1, ?2)", | ||
| 18 | [username, hashed_password], | ||
| 19 | )?; | ||
| 20 | Ok(()) | ||
| 21 | } | ||
| 22 | |||
| 23 | pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> { | ||
| 24 | let mut stmt = conn.prepare("SELECT hashed_password FROM users WHERE username = ?1").unwrap(); | ||
| 25 | let mut rows = stmt.query([username]).unwrap(); | ||
| 26 | if let Ok(Some(row)) = rows.next() { | ||
| 27 | row.get(0).ok() | ||
| 28 | } else { | ||
| 29 | None | ||
| 30 | } | ||
| 31 | } | ||
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs new file mode 100644 index 0000000..2132d09 --- /dev/null +++ b/src/bin/server/handler.rs | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | use std::io::{BufRead, BufReader, Write}; | ||
| 2 | use std::net::TcpStream; | ||
| 3 | use std::sync::Mutex; | ||
| 4 | use once_cell::sync::Lazy; | ||
| 5 | use rusqlite::Connection; | ||
| 6 | use crate::database; | ||
| 7 | |||
| 8 | static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db())); | ||
| 9 | |||
| 10 | pub fn handle_client(stream: TcpStream) { | ||
| 11 | let client_addr = stream.peer_addr().unwrap().to_string(); | ||
| 12 | let mut user = "guest".to_string(); | ||
| 13 | println!("[{} @ {}] Client connected", user, client_addr); | ||
| 14 | let reader_stream = stream.try_clone().unwrap(); | ||
| 15 | let reader = BufReader::new(reader_stream); | ||
| 16 | let mut writer = stream; | ||
| 17 | for line in reader.lines() { | ||
| 18 | let line = line.unwrap(); | ||
| 19 | println!("[{} @ {}] Received: {}", user, client_addr, line); | ||
| 20 | let response = process_message(&line, &mut user); | ||
| 21 | writeln!(writer, "{}", response).unwrap(); | ||
| 22 | } | ||
| 23 | println!("[{} @ {}] Client disconnected", user, client_addr); | ||
| 24 | } | ||
| 25 | |||
| 26 | fn process_message(msg: &str, user: &mut String) -> String { | ||
| 27 | let parts: Vec<&str> = msg.splitn(3, ' ').collect(); | ||
| 28 | match parts[0] { | ||
| 29 | "REGISTER" => { | ||
| 30 | if parts.len() != 3 { | ||
| 31 | "FAIL Invalid REGISTER format".to_string() | ||
| 32 | } else { | ||
| 33 | let username = parts[1]; | ||
| 34 | let hashed_pass = parts[2]; | ||
| 35 | let db = DB_CONN.lock().unwrap(); | ||
| 36 | match database::add_user(&db, username, hashed_pass) { | ||
| 37 | Ok(_) => format!("OK Registered {}", username), | ||
| 38 | Err(_) => "FAIL User already exists".to_string(), | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
| 42 | "AUTH" => { | ||
| 43 | if parts.len() != 3 { | ||
| 44 | "FAIL Invalid AUTH format".to_string() | ||
| 45 | } else { | ||
| 46 | let username = parts[1]; | ||
| 47 | let provided_hash = parts[2]; | ||
| 48 | let db = DB_CONN.lock().unwrap(); | ||
| 49 | if let Some(stored_hash) = database::get_hashed_password(&db, username) { | ||
| 50 | if provided_hash == stored_hash { | ||
| 51 | *user = username.to_string(); | ||
| 52 | format!("OK {}", username) | ||
| 53 | } else { | ||
| 54 | "FAIL Invalid credentials".to_string() | ||
| 55 | } | ||
| 56 | } else { | ||
| 57 | "FAIL User not found".to_string() | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | "PING" => "PING".to_string(), | ||
| 62 | _ => msg.to_string(), // Echo back for compatibility | ||
| 63 | } | ||
| 64 | } | ||
diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs new file mode 100644 index 0000000..7693069 --- /dev/null +++ b/src/bin/server/main.rs | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | mod args; | ||
| 2 | mod handler; | ||
| 3 | mod database; | ||
| 4 | |||
| 5 | use std::net::TcpListener; | ||
| 6 | use std::thread; | ||
| 7 | |||
| 8 | fn main() { | ||
| 9 | let args = args::parse_args(); | ||
| 10 | let listener = match TcpListener::bind(format!("{}:{}", args.ip, args.port)) { | ||
| 11 | Ok(l) => l, | ||
| 12 | Err(e) => { | ||
| 13 | eprintln!("Failed to bind to {}:{}: {}", args.ip, args.port, e); | ||
| 14 | std::process::exit(1); | ||
| 15 | } | ||
| 16 | }; | ||
| 17 | println!("Server listening on {}", listener.local_addr().unwrap()); | ||
| 18 | for stream in listener.incoming() { | ||
| 19 | let stream = stream.unwrap(); | ||
| 20 | thread::spawn(move || { | ||
| 21 | handler::handle_client(stream); | ||
| 22 | }); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/src/bin/server/mod.rs b/src/bin/server/mod.rs new file mode 100644 index 0000000..b2d622e --- /dev/null +++ b/src/bin/server/mod.rs | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | pub mod args; | ||
| 2 | pub mod handler; | ||
| 3 | pub mod database; | ||
