diff options
Diffstat (limited to 'src/client/repl.rs')
| -rw-r--r-- | src/client/repl.rs | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/src/client/repl.rs b/src/client/repl.rs new file mode 100644 index 0000000..5bcb2a3 --- /dev/null +++ b/src/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 | pub 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 | } | ||
