use anyhow::Result; use crate::{ ConfigManager, config::Host, input }; use dialoguer::{ Select, Input, Confirm, Editor, theme::ColorfulTheme }; use serde::{ Serialize, Deserialize }; use ssh_key::PrivateKey; use std::fs; #[derive(Clone, Debug)] pub struct PlatformKey { pub file: String, pub display: String, } impl std::fmt::Display for PlatformKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.display) } } // todo: GPG support #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum Platform { SSH } impl std::fmt::Display for Platform { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Platform::SSH => "SSH", }) } } impl Platform { pub fn all() -> Vec { vec![ Self::SSH ] } pub fn new_host(&self, config: &mut ConfigManager) -> Result { match self { Platform::SSH => self.ssh_new_host(config) } } pub fn get_keys(&self, config: &ConfigManager) -> Vec { match self { Platform::SSH => self.ssh_get_keys(config), } } pub fn run(&self, host: &mut Host, config: &mut ConfigManager) -> Result<()> { match self { Platform::SSH => self.ssh_run(host, config), } } // --- --- --- // SSH // --- --- --- fn ssh_new_config(&self, host: String, port: i16, keyid: PlatformKey) -> String { format!(r#"# Generated by Keyman Host {host} HostName {host} Port {port} IdentityFile {} "#, keyid.file) } // New host fn ssh_new_host(&self, config: &mut ConfigManager) -> Result { println!("- Creating new host entry"); let keyid = input::get_key(config, self.to_owned())?; let userhost = loop { let inp: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("User@Host") .interact_text()?; let split = inp.split("@"); let split: Vec = split.map(|a| a.to_string()).collect(); if split.len() < 2 { println!("Incorrect user@host given. Example: paul@example.com:34; @[:port]"); } else { break split } }; let user: String = userhost.get(0).unwrap().to_string(); let mut host: String = userhost.get(1).unwrap().to_string(); let mut port: i16 = 22; let _host = host.to_owned(); let h: Vec<&str> = _host.split(":").collect(); if h.len() > 1 { let portstr = h.get(1).unwrap(); port = i16::from_str_radix(portstr, 10)?; host = h.get(0).unwrap().to_string(); } let config_dir = config.config_dir.join("hosts/").join(host.to_owned()); fs::create_dir_all(config_dir.to_owned()).expect(&format!("Couldnt create_dir_all on {:?}", config_dir.to_owned())); let config_file = config_dir.join("config"); fs::write(config_file.to_owned(), self.ssh_new_config(host.to_owned(), port, keyid.to_owned())).expect(&format!("Couldn't write config at {:?}", config_file.to_owned())); Ok(Host { platform: Platform::SSH, id: keyid.file, user, host, port, config: config_file.to_owned().to_string_lossy().to_string() }) } // Get keys fn ssh_get_keys(&self, config: &ConfigManager) -> Vec { let mut keys = vec![]; let Ok(dir) = std::fs::read_dir(config.search_path.to_owned()) else { panic!("Couldn't read {:?}", config.search_path.to_owned()) }; for file in dir { let Ok(f) = file else { continue }; match PrivateKey::read_openssh_file(&f.path()) { Ok(p) => keys.push(PlatformKey { file: format!("{}", f.path().to_string_lossy()), display: format!("{}: {}", p.fingerprint(ssh_key::HashAlg::Sha256), f.file_name().to_string_lossy()) }), // Not a private key Err(_) => continue }; } keys } // Run fn ssh_run(&self, host: &mut Host, config: &mut ConfigManager) -> Result<()> { Ok(loop { let action = Select::with_theme(&ColorfulTheme::default()) .with_prompt(format!("{}@{}", host.user.to_owned(), host.host.to_owned())) .item("Connect") .item("Edit config") .item("Edit definition") .item("Delete") .item("Back") .default(0) .interact()?; match action { 0 => { // Connect // TODO: clear screen before connect std::process::Command::new("ssh") .arg("-F") .arg(host.config.to_owned()) .arg("-l") .arg(host.user.to_owned()) .arg(host.host.to_owned()).spawn()?.wait()?; } 1 => { // Edit config // TODO: replace this with configuer::editor // if let Some(cnf) = Editor::new().edit(fs::read_to_string(config.config_dir.join("config"))?.as_str())? { // fs::write(config.config_dir.join("config"), cnf)?; // } let editor = std::env::var("EDITOR").expect("EDITOR is not set"); std::process::Command::new(editor) .arg(host.config.to_owned()) .spawn()?.wait()?; } 2 => { // Edit definition let edited_host = host.edit()?; config.configs.remove(host); config.configs.insert(edited_host.to_owned()); host.clone_from(&edited_host); config.save(); } 3 => { // Delete let confirm = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Are you sure you want to delete this definition?") .default(false) .interact()?; if confirm { let mut conf = std::path::PathBuf::from(host.config.to_owned()); conf.pop(); config.configs.remove(host); config.save(); fs::remove_dir_all(conf)?; break; } } // Back 4 => break, _ => () } }) } // --- --- --- // GPG // --- --- --- }