keyman/src/platform.rs

227 lines
7.6 KiB
Rust

use anyhow::Result;
use colored::Colorize;
use crate::{ ConfigManager, config::Host, input };
use dialoguer::{ Select, Input, Confirm, Editor, theme::ColorfulTheme };
use serde::{ Serialize, Deserialize };
use ssh2_config::{ SshConfig, ParseRule };
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<Platform> {
vec![
Self::SSH
]
}
pub fn new_host(&self, config: &mut ConfigManager) -> Result<Host> {
match self {
Platform::SSH => self.ssh_new_host(config)
}
}
pub fn get_keys(&self, config: &ConfigManager) -> Vec<PlatformKey> {
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<Host> {
println!(" ## Creating new SSH 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<String> = split.map(|a| a.to_string()).collect();
if split.len() < 2 {
println!("{} Example: paul@example.com:34 - <user>@<host>[:port]", "Incorrect user@host given.".red());
} 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<PlatformKey> {
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 t = mktemp::Temp::new_file()?;
fs::copy(&host.config, &t)?;
let editor = std::env::var("EDITOR").expect("EDITOR is not set");
std::process::Command::new(editor)
.arg(&t.to_str().unwrap())
.spawn()?.wait()?;
let mut file = std::io::BufReader::new(fs::File::open(&t).expect(&format!("Couldn't open config ({})", &host.config)));
match SshConfig::default().parse(&mut file, ParseRule::STRICT) {
Ok(_) => {
println!(" ## {} ## ", "Config OK".green());
fs::copy(&t, &host.config)?;
},
Err(e) => println!(" ## {}: {} ## ", "Rejecting config".red(), e),
}
}
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
// --- --- ---
}