Initial commit
This commit is contained in:
commit
9be5bebf78
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1341
Cargo.lock
generated
Normal file
1341
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "keyman"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argparse = "0.2.2"
|
||||
dialoguer = { version = "0.10.4", features = ["fuzzy-select"] }
|
||||
dirs = "5.0.1"
|
||||
flexbuffers = "2.0.0"
|
||||
mktemp = "0.5.0"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
ssh-key = { version = "0.5.1", features = ["rsa", "ed25519", "dsa", "p256", "p384"] }
|
||||
termion = "2.0.1"
|
||||
toml = "0.7.4"
|
||||
tui = { version = "0.19.0", default-features = false, features = ["termion"] }
|
107
src/config.rs
Normal file
107
src/config.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use anyhow::Result;
|
||||
use crate::{ platform::Platform, input };
|
||||
use flexbuffers::{ FlexbufferSerializer, Reader };
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path;
|
||||
use serde::{ Serialize, Deserialize };
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct Host {
|
||||
pub platform: Platform,
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub host: String,
|
||||
pub port: i16,
|
||||
pub config: String
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Host {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(std::format_args!("{} via {}@{}", self.platform, self.user, self.host))
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn edit(&mut self) -> Result<Self> {
|
||||
let mut data = toml::to_string_pretty(self)?;
|
||||
|
||||
{
|
||||
let t = mktemp::Temp::new_file().unwrap();
|
||||
fs::write(t.to_owned(), data)?;
|
||||
|
||||
let editor = std::env::var("EDITOR").expect("EDITOR is not set");
|
||||
std::process::Command::new(editor)
|
||||
.arg(t.to_owned().to_string_lossy().to_string())
|
||||
.spawn()?.wait()?;
|
||||
|
||||
data = fs::read_to_string(t)?;
|
||||
}
|
||||
|
||||
Ok(toml::from_str(&data)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ConfigManager {
|
||||
#[serde(skip)]
|
||||
pub config_dir: path::PathBuf,
|
||||
#[serde(skip)]
|
||||
pub search_path: path::PathBuf,
|
||||
pub configs: BTreeSet<Host>,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
pub fn new(given_config_root: Option<String>, search_path: Option<String>) -> Result<Self> {
|
||||
let config_root = match given_config_root {
|
||||
Some(c) => path::Path::new(&c).to_owned(),
|
||||
None => dirs::data_local_dir().unwrap().join("keyman/"),
|
||||
};
|
||||
|
||||
if !fs::try_exists(config_root.to_owned()).unwrap_or(false) {
|
||||
fs::create_dir_all(config_root.to_owned())?;
|
||||
}
|
||||
|
||||
let search_path = match search_path {
|
||||
Some(s) => std::path::Path::new(&s).to_owned(),
|
||||
None => dirs::home_dir().unwrap().join(".ssh/"),
|
||||
};
|
||||
|
||||
match fs::read(config_root.join("config")) {
|
||||
Ok(c) => {
|
||||
let root = Reader::get_root(c.as_slice())?;
|
||||
match ConfigManager::deserialize(root) {
|
||||
Ok(mut mgr) => {
|
||||
mgr.config_dir = config_root;
|
||||
mgr.search_path = search_path;
|
||||
Ok(mgr)
|
||||
},
|
||||
Err(_) => Ok(Self { config_dir: config_root, search_path, configs: BTreeSet::new() }),
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(Self { config_dir: config_root, search_path, configs: BTreeSet::new() }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self) {
|
||||
let mut s = FlexbufferSerializer::new();
|
||||
self.serialize(&mut s).unwrap();
|
||||
fs::write(self.config_dir.to_owned().join("config"), s.view()).unwrap();
|
||||
}
|
||||
|
||||
pub fn new_host(&mut self) -> Result<Host> {
|
||||
let platform = input::get_platform()?;
|
||||
let host = platform.new_host(self)?;
|
||||
self.configs.insert(host.to_owned());
|
||||
self.save();
|
||||
Ok(host)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigManager {
|
||||
fn drop(&mut self) {
|
||||
self.save();
|
||||
}
|
||||
}
|
88
src/input.rs
Normal file
88
src/input.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use anyhow::Result;
|
||||
use crate::{ platform::{ Platform, PlatformKey }, config::{ ConfigManager, Host } };
|
||||
use dialoguer::{ Select, FuzzySelect, Input, theme::ColorfulTheme };
|
||||
use ssh_key::PrivateKey;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
pub fn get_host(config: &mut ConfigManager) -> Result<Host> {
|
||||
if config.configs.len() < 1 {
|
||||
let _ = config.new_host()?;
|
||||
}
|
||||
|
||||
let hosts = config.configs.to_owned();
|
||||
let mut hosts: Vec<Host> = hosts.into_iter().collect();
|
||||
|
||||
let theme = ColorfulTheme::default();
|
||||
let mut select = Select::with_theme(&theme);
|
||||
select.with_prompt("Host")
|
||||
.items(hosts.as_slice())
|
||||
.item("New host definition...")
|
||||
.default(0);
|
||||
|
||||
let c = loop {
|
||||
let c = select.interact()?;
|
||||
|
||||
if c == hosts.len() {
|
||||
let _ = config.new_host()?;
|
||||
let _hosts = config.configs.to_owned();
|
||||
hosts = _hosts.into_iter().collect();
|
||||
} else {
|
||||
break c
|
||||
}
|
||||
};
|
||||
|
||||
Ok(hosts.get(c).unwrap().to_owned())
|
||||
}
|
||||
|
||||
pub fn get_platform() -> Result<Platform> {
|
||||
let platforms = Platform::all();
|
||||
let c = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Platform")
|
||||
.items(platforms.as_slice())
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
Ok(platforms.get(c).unwrap().to_owned())
|
||||
}
|
||||
|
||||
pub fn get_key(config: &ConfigManager, platform: Platform) -> Result<PlatformKey> {
|
||||
let keys: Vec<PlatformKey> = platform.get_keys(config);
|
||||
let c = FuzzySelect::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Key")
|
||||
.items(keys.as_slice())
|
||||
.item("Other...")
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
match c == keys.len() {
|
||||
true => get_custom_key(),
|
||||
false => Ok(keys.get(c).unwrap().to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_custom_key() -> Result<PlatformKey> {
|
||||
let theme = ColorfulTheme::default();
|
||||
let mut input = Input::with_theme(&theme);
|
||||
input.with_prompt("Path").with_initial_text(dirs::home_dir().unwrap().join(".ssh/").to_string_lossy().to_string());
|
||||
|
||||
Ok(loop {
|
||||
let buffer: String = input.interact_text()?;
|
||||
|
||||
if fs::try_exists(buffer.to_owned()).unwrap_or(false) {
|
||||
break match PrivateKey::read_openssh_file(&PathBuf::from(buffer.to_owned())) {
|
||||
Ok(p) => PlatformKey {
|
||||
file: buffer.to_owned(),
|
||||
display: format!("{}: {}", p.fingerprint(ssh_key::HashAlg::Sha256), buffer)
|
||||
},
|
||||
Err(_) => {
|
||||
println!("File is not SSH private key");
|
||||
continue;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
println!("File doesn't exist")
|
||||
}
|
||||
})
|
||||
}
|
45
src/main.rs
Normal file
45
src/main.rs
Normal file
@ -0,0 +1,45 @@
|
||||
#![feature(fs_try_exists)]
|
||||
|
||||
use anyhow::Result;
|
||||
use config::ConfigManager;
|
||||
use std::io;
|
||||
use termion::screen::IntoAlternateScreen;
|
||||
use tui::{ backend::TermionBackend, Terminal };
|
||||
|
||||
pub mod config;
|
||||
pub mod input;
|
||||
pub mod platform;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut search_path: Option<String> = None;
|
||||
let mut config_root: Option<String> = None;
|
||||
let mut dry_run: bool = false;
|
||||
{
|
||||
let mut parser = argparse::ArgumentParser::new();
|
||||
|
||||
parser.set_description("Key manager for command line programs");
|
||||
|
||||
parser.add_option(&["-V", "--version"], argparse::Print(env!("CARGO_PKG_VERSION").to_string()), "Show version");
|
||||
parser.refer(&mut search_path).add_option(&["-s", "--search"], argparse::StoreOption, "Search path for keys");
|
||||
parser.refer(&mut config_root).add_option(&["-c", "--config"], argparse::StoreOption, "Path to keyman's config dir");
|
||||
parser.refer(&mut dry_run).add_option(&["-d", "--dry-run", "--dryrun"], argparse::StoreTrue, "Do a dry run");
|
||||
|
||||
parser.parse_args_or_exit();
|
||||
}
|
||||
|
||||
let mut config: ConfigManager = ConfigManager::new(config_root, search_path)?;
|
||||
|
||||
let backend = TermionBackend::new(io::stdout().into_alternate_screen()?);
|
||||
let mut term = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
term.clear()?;
|
||||
|
||||
let mut host = input::get_host(&mut config)?;
|
||||
|
||||
if !dry_run {
|
||||
let platform = host.platform.to_owned();
|
||||
platform.run(&mut host, &mut config)?;
|
||||
}
|
||||
}
|
||||
}
|
195
src/platform.rs
Normal file
195
src/platform.rs
Normal file
@ -0,0 +1,195 @@
|
||||
use anyhow::{ Result, bail };
|
||||
use crate::{ ConfigManager, config::Host, input };
|
||||
use dialoguer::{ Select, Input, 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<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 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 {
|
||||
bail!("Incorrect user@host given. Example: paul@example.com:34; <user>@<host>[: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<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
|
||||
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
|
||||
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);
|
||||
config.save();
|
||||
}
|
||||
3 => { // Delete
|
||||
config.configs.remove(host);
|
||||
config.save();
|
||||
break;
|
||||
} // Back
|
||||
4 => break,
|
||||
_ => ()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// --- --- ---
|
||||
// GPG
|
||||
// --- --- ---
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user