summaryrefslogtreecommitdiffstats
path: root/src/bin/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/client')
-rw-r--r--src/bin/client/connection.rs57
-rw-r--r--src/bin/client/database.rs94
-rw-r--r--src/bin/client/main.rs17
-rw-r--r--src/bin/client/mod.rs4
-rw-r--r--src/bin/client/platform.rs28
-rw-r--r--src/bin/client/repl.rs335
6 files changed, 535 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 @@
1use std::io::{BufRead, BufReader, Write};
2use std::net::TcpStream;
3use std::sync::mpsc;
4use std::thread;
5use std::time::{Duration, Instant};
6use rusqlite::Connection;
7use bcrypt::hash_with_salt;
8use crate::database;
9
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
17pub 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 @@
1use rusqlite::Connection;
2use aes_gcm::{Aes256Gcm, Key, Nonce};
3use aes_gcm::aead::{Aead, KeyInit};
4use pbkdf2::pbkdf2;
5use hmac::Hmac;
6use sha2::Sha256;
7use rand::Rng;
8
9pub 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
29pub 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
39pub 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
46pub 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
55pub 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
67fn 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
74fn 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
86fn 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 @@
1mod repl;
2mod database;
3mod connection;
4mod platform;
5
6use repl::ClientState;
7
8fn 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 @@
1pub mod database;
2pub mod connection;
3pub mod platform;
4pub 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)]
2pub 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 @@
1use std::io::{BufRead, BufReader, Write, stdout};
2use std::net::TcpStream;
3use rusqlite::Connection;
4use std::time::Instant;
5use bcrypt::hash_with_salt;
6
7use crate::connection;
8use crate::database;
9
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};
18
19macro_rules! println {
20 () => {
21 print!("\r\n")
22 };
23 ($($arg:tt)*) => {
24 print!("\r{}\n", format_args!($($arg)*))
25 };
26}
27
28pub 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
37impl 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(&current_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(&current_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(&current_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(&current_input);
301 }
302 }
303 _ => {}
304 }
305 _ => {}
306 }
307 }
308 }
309 disable_raw_mode().unwrap();
310 execute!(stdout(), Show).unwrap();
311 }
312}
313
314fn 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
321fn 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
333fn help() {
334 println!("Available commands: connect <ip>:<port>, send <message>, disconnect, ping, login, register, setcred, help, quit");
335}