From e5e4aeaff1ed93e6421ecaa819e86ef38bcd85ba Mon Sep 17 00:00:00 2001 From: Himadri Bhattacharjee <107522312+lavafroth@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:51:18 +0530 Subject: [PATCH] feat: implemented commit command --- README.md | 3 +- src/authfile.rs | 2 +- src/main.rs | 214 +++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 196 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a0baf19..2116b09 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ interface or by editing it externally. - [x] Authfile path - [x] Listening port number - [x] `/add` command to add new keys +Run it as `echo "ssh-ed25519 somekey" | ssh chatroom` - [x] `/rename` command -- [ ] `/commit` command to commit in-memory changes to Authfile +- [x] `/commit` command to commit in-memory changes to Authfile - [ ] `#mention` tags ### Authfile diff --git a/src/authfile.rs b/src/authfile.rs index 23e3288..cc63de3 100644 --- a/src/authfile.rs +++ b/src/authfile.rs @@ -70,7 +70,7 @@ pub struct AuthFile { pub key_pool: HashSet, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Entity { name: String, role: Role, diff --git a/src/main.rs b/src/main.rs index 91810cb..95b1f01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,8 @@ pub enum Error { Authfile(#[from] authfile::Error), #[error("failed to resize frame as requested by client {id}")] FrameResize { source: std::io::Error, id: usize }, + #[error("failed to parse command {0:#?}")] + CommandParse(String), } pub struct Client { @@ -204,7 +206,10 @@ impl AppServer { let id = self.id; tokio::spawn(async move { let mut clients = clients.write().await; - let client = clients.get_mut(&id).unwrap(); + let Some(client) = clients.get_mut(&id) else { + log::warn!("failed to get handle on the current client with id: {}", id); + return; + }; let res = client.terminal.draw(|f| { let buf = f.buffer_mut(); @@ -310,6 +315,7 @@ impl Handler for AppServer { data: &[u8], _session: &mut Session, ) -> Result<(), Self::Error> { + log::info!("you sent me: {data:#?}"); match data { // Sending Ctrl+C ends the session and disconnects the client [3] => return Err(russh::Error::Disconnect.into()), @@ -326,8 +332,7 @@ impl Handler for AppServer { }; let text = current_client.textarea.lines().to_vec().join("\n"); - // HACK: Clear the textarea on send. - // Select all, delete. + // HACK: Clear the textarea on send. Select all, delete. current_client.textarea.select_all(); current_client .textarea @@ -337,8 +342,15 @@ impl Handler for AppServer { text }; let name = self.entity().await.read().await.name().to_string(); - // TODO: handle commands - let Some(command) = Command::parse(&text) else { + + let maybe_command = match Command::parse(&text) { + Ok(c) => c, + Err(e) => { + log::error!("{e:?}"); + return Ok(()); + } + }; + let Some(command) = maybe_command else { let message = format!("[{name}]: {text}"); self.app.write().await.history.push(message); self.render().await; @@ -347,15 +359,27 @@ impl Handler for AppServer { match command { Command::Add(entity) => { + if self.entity().await.read().await.role() != authfile::Role::Admin { + // TODO: tell em "you're not an admin lol kekw" + return Ok(()); + } + log::info!("attempting to add {:#?}", entity); let mut keychain = self.keychain.write().await; let mut key_data_pool = self.key_data_pool.write().await; + let mut key_data_to_user = self.key_data_to_user.write().await; let key_data = entity.key_data(); - keychain.push(new_atomic(entity)); - key_data_pool.insert(key_data); + let entity = new_atomic(entity); + keychain.push(entity.clone()); + key_data_pool.insert(key_data.clone()); + key_data_to_user.insert(key_data, entity); } Command::Rename { from, to } => { + if self.entity().await.read().await.role() != authfile::Role::Admin { + // TODO: tell em "you're not an admin lol kekw" + return Ok(()); + } let from = from .split_once(':') .map(|(sanitized, _ignore_role)| sanitized.to_string()) @@ -378,6 +402,8 @@ impl Handler for AppServer { let ent = ent.read().await.clone(); let kd_2_id = self.key_data_to_id.read().await; + // TODO: remove stray ids from key_data_to_id + // and id_to_user let ids = kd_2_id.get(&ent.key_data()).unwrap(); for id in ids { let mut clients = self.clients.write().await; @@ -385,7 +411,7 @@ impl Handler for AppServer { log::warn!( "failed to get handle on client with id: {id}, considering them disconnected" ); - return Ok(()); + continue; }; let block = Block::bordered() @@ -395,6 +421,33 @@ impl Handler for AppServer { } } } + Command::Commit => { + if self.entity().await.read().await.role() != authfile::Role::Admin { + // TODO: tell em "you're not an admin lol kekw" + return Ok(()); + } + let keychain = self.keychain.read().await; + let mut pubkeys = vec![]; + for entity in keychain.iter() { + let ent_str = entity.read().await.to_pubkey().to_string(); + pubkeys.push(ent_str); + } + let pubkeys = pubkeys.join("\n"); + let mut tmpfile = self.args.authfile.clone(); + tmpfile.push('~'); + if let Err(e) = std::fs::write(&tmpfile, pubkeys) { + log::error!( + "failed to create temporary file to commit in-memory authorized keys: {e:#?}" + ); + return Ok(()); + }; + if let Err(e) = std::fs::rename(tmpfile, &self.args.authfile) { + log::error!( + "failed to move temporary file to original authfile: {e:#?}: do we have write permissions to it?" + ); + return Ok(()); + }; + } } // re-render self.render().await; @@ -415,6 +468,118 @@ impl Handler for AppServer { self.render_textarea().await; } + data if data.ends_with(&[10]) => { + let name = self.entity().await.read().await.name().to_string(); + let text = std::str::from_utf8(&data[..data.len() - 1]).unwrap(); + + let maybe_command = match Command::parse(&text) { + Ok(c) => c, + Err(e) => { + log::error!("{e:?}"); + return Ok(()); + } + }; + let Some(command) = maybe_command else { + let message = format!("[{name}]: {text}"); + self.app.write().await.history.push(message); + self.render().await; + return Ok(()); + }; + + match command { + Command::Add(entity) => { + if self.entity().await.read().await.role() != authfile::Role::Admin { + // TODO: tell em "you're not an admin lol kekw" + return Ok(()); + } + log::info!("attempting to add {:#?}", entity); + let mut keychain = self.keychain.write().await; + let mut key_data_pool = self.key_data_pool.write().await; + let mut key_data_to_user = self.key_data_to_user.write().await; + + let key_data = entity.key_data(); + + let entity = new_atomic(entity); + keychain.push(entity.clone()); + key_data_pool.insert(key_data.clone()); + key_data_to_user.insert(key_data, entity); + } + Command::Rename { from, to } => { + if self.entity().await.read().await.role() != authfile::Role::Admin { + // TODO: tell em "you're not an admin lol kekw" + return Ok(()); + } + let from = from + .split_once(':') + .map(|(sanitized, _ignore_role)| sanitized.to_string()) + .unwrap_or(from); + + let to = to + .split_once(':') + .map(|(sanitized, _ignore_role)| sanitized.to_string()) + .unwrap_or(to); + + log::info!("renaming {:?} to {:?}", from, to); + + let kc = self.keychain.read().await; + for ent in kc.iter() { + if ent.read().await.name() != from { + continue; + } + + ent.write().await.set_name(&to); + + let ent = ent.read().await.clone(); + let kd_2_id = self.key_data_to_id.read().await; + // TODO: remove stray ids from key_data_to_id + // and id_to_user + let ids = kd_2_id.get(&ent.key_data()).unwrap(); + for id in ids { + let mut clients = self.clients.write().await; + let Some(client) = clients.get_mut(id) else { + log::warn!( + "failed to get handle on client with id: {id}, considering them disconnected" + ); + continue; + }; + + let block = Block::bordered() + .border_type(BorderType::Rounded) + .title(ent.title()); + client.textarea.set_block(block); + } + } + } + Command::Commit => { + if self.entity().await.read().await.role() != authfile::Role::Admin { + // TODO: tell em "you're not an admin lol kekw" + return Ok(()); + } + let keychain = self.keychain.read().await; + let mut pubkeys = vec![]; + for entity in keychain.iter() { + let ent_str = entity.read().await.to_pubkey().to_string(); + pubkeys.push(ent_str); + } + let pubkeys = pubkeys.join("\n"); + let mut tmpfile = self.args.authfile.clone(); + tmpfile.push('~'); + if let Err(e) = std::fs::write(&tmpfile, pubkeys) { + log::error!( + "failed to create temporary file to commit in-memory authorized keys: {e:#?}" + ); + return Ok(()); + }; + if let Err(e) = std::fs::rename(tmpfile, &self.args.authfile) { + log::error!( + "failed to move temporary file to original authfile: {e:#?}: do we have write permissions to it?" + ); + return Ok(()); + }; + } + } + } + data if !data.is_empty() => { let mut iterator = data.iter().skip(1).map(|d| Ok(*d)); match ratatui::termion::event::parse_event(data[0], &mut iterator) { @@ -423,13 +588,15 @@ impl Handler for AppServer { self.check_role_and_reload().await?; } Ok(keycode) => { - self.clients - .write() - .await - .get_mut(&self.id) - .unwrap() - .textarea - .input(keycode); + let mut clients = self.clients.write().await; + let Some(client) = clients.get_mut(&self.id) else { + log::warn!( + "failed to get handle on the current client with id: {}", + self.id + ); + return Ok(()); + }; + client.textarea.input(keycode); } Err(e) => { log::warn!("failed to parse keyboard input data: {:?}: {e}", data); @@ -535,13 +702,18 @@ impl Drop for AppServer { pub enum Command { Add(Entity), Rename { from: String, to: String }, + Commit, } impl Command { - fn parse(text: &str) -> Option { + fn parse(text: &str) -> Result, Error> { let split = text.split_once(char::is_whitespace); - Some(match split { - Some(("/add", payload)) => Self::Add(payload.try_into().unwrap()), + if text == "/commit" { + return Ok(Some(Self::Commit)); + } + + Ok(Some(match split { + Some(("/add", payload)) => Self::Add(payload.try_into()?), Some(("/rename", payload)) => { let split_payload: Vec<&str> = payload.split_whitespace().collect(); match split_payload.as_slice() { @@ -549,11 +721,11 @@ impl Command { to: to.to_string(), from: from.to_string(), }, - _ => return None, // TODO: return an error message + _ => return Err(Error::CommandParse(text.to_string())), } } - _ => return None, - }) + _ => return Ok(None), + })) } }