From b3a04c4a143c34ba92008cf018eed159f87a0c6e Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Fri, 10 Apr 2020 00:12:40 -0400 Subject: move some basic stuff into config --- src/actions.rs | 6 +++-- src/api.rs | 30 ++++++---------------- src/bin/agent.rs | 13 +++++++--- src/bin/rbw.rs | 63 +++++++++++++++++++++++++++++++++++++++++++--- src/config.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 15 +++++++++++ src/lib.rs | 1 + 7 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 src/config.rs diff --git a/src/actions.rs b/src/actions.rs index 3658139..14f140e 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -9,8 +9,9 @@ pub async fn login( crate::cipherstring::CipherString, crate::locked::Keys, )> { + let config = crate::config::Config::load_async().await?; let client = - crate::api::Client::new_self_hosted("https://bitwarden.tozt.net"); + crate::api::Client::new(&config.base_url(), &config.identity_url()); let iterations = client.prelogin(email).await?; let identity = @@ -48,7 +49,8 @@ pub async fn unlock( pub async fn sync( access_token: &str, ) -> Result<(String, Vec)> { + let config = crate::config::Config::load_async().await?; let client = - crate::api::Client::new_self_hosted("https://bitwarden.tozt.net"); + crate::api::Client::new(&config.base_url(), &config.identity_url()); client.sync(access_token).await } diff --git a/src/api.rs b/src/api.rs index fc59d25..e70c45b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -72,31 +72,15 @@ pub struct Login { #[derive(Debug)] pub struct Client { - api_url_base: String, - identity_url_base: String, -} - -impl Default for Client { - fn default() -> Self { - Self { - api_url_base: "https://api.bitwarden.com".to_string(), - identity_url_base: "https://identity.bitwarden.com".to_string(), - } - } + base_url: String, + identity_url: String, } impl Client { - #[allow(dead_code)] - #[must_use] - pub fn new() -> Self { - Self::default() - } - - #[must_use] - pub fn new_self_hosted(base_url: &str) -> Self { + pub fn new(base_url: &str, identity_url: &str) -> Self { Self { - api_url_base: format!("{}/api", base_url), - identity_url_base: format!("{}/identity", base_url), + base_url: base_url.to_string(), + identity_url: identity_url.to_string(), } } @@ -167,10 +151,10 @@ impl Client { } fn api_url(&self, path: &str) -> String { - format!("{}{}", self.api_url_base, path) + format!("{}{}", self.base_url, path) } fn identity_url(&self, path: &str) -> String { - format!("{}{}", self.identity_url_base, path) + format!("{}{}", self.identity_url, path) } } diff --git a/src/bin/agent.rs b/src/bin/agent.rs index 9ee5f32..0c65da6 100644 --- a/src/bin/agent.rs +++ b/src/bin/agent.rs @@ -38,11 +38,11 @@ async fn login( tty: Option<&str>, ) { let mut state = state.write().await; - let email = "bitwarden@tozt.net"; // XXX read from config + let email = config_email().await; let password = rbw::pinentry::getpin("prompt", "desc", tty).await.unwrap(); let (access_token, iterations, protected_key, keys) = - rbw::actions::login(email, &password).await.unwrap(); + rbw::actions::login(&email, &password).await.unwrap(); state.access_token = Some(access_token); state.iterations = Some(iterations); state.protected_key = Some(protected_key); @@ -67,11 +67,11 @@ async fn unlock( tty: Option<&str>, ) { let mut state = state.write().await; - let email = "bitwarden@tozt.net"; // XXX read from config + let email = config_email().await; let password = rbw::pinentry::getpin("prompt", "desc", tty).await.unwrap(); let keys = rbw::actions::unlock( - email, + &email, &password, state.iterations.unwrap(), state.protected_key.as_ref().unwrap(), @@ -141,6 +141,11 @@ async fn handle_sock( } } +async fn config_email() -> String { + let config = rbw::config::Config::load_async().await.unwrap(); + config.email.unwrap() +} + struct Agent { timeout: tokio::time::Delay, state: std::sync::Arc>, diff --git a/src/bin/rbw.rs b/src/bin/rbw.rs index 0cb9fe7..3aae676 100644 --- a/src/bin/rbw.rs +++ b/src/bin/rbw.rs @@ -39,7 +39,27 @@ fn recv(sock: &mut std::os::unix::net::UnixStream) -> rbw::agent::Response { serde_json::from_str(&line).unwrap() } +fn config_show() { + let config = rbw::config::Config::load().unwrap(); + serde_json::to_writer_pretty(std::io::stdout(), &config).unwrap(); + println!(); +} + +fn config_set(key: &str, value: &str) { + let mut config = rbw::config::Config::load() + .unwrap_or_else(|_| rbw::config::Config::new()); + match key { + "email" => config.email = Some(value.to_string()), + "base_url" => config.base_url = Some(value.to_string()), + "identity_url" => config.identity_url = Some(value.to_string()), + _ => unimplemented!(), + } + config.save().unwrap(); +} + fn login() { + ensure_agent(); + let mut sock = connect(); send( &mut sock, @@ -59,6 +79,8 @@ fn login() { } fn unlock() { + ensure_agent(); + let mut sock = connect(); send( &mut sock, @@ -78,6 +100,8 @@ fn unlock() { } fn sync() { + ensure_agent(); + let mut sock = connect(); send( &mut sock, @@ -97,26 +121,38 @@ fn sync() { } fn list() { + ensure_agent(); + todo!() } fn get() { + ensure_agent(); + todo!() } fn add() { + ensure_agent(); + todo!() } fn generate() { + ensure_agent(); + todo!() } fn edit() { + ensure_agent(); + todo!() } fn remove() { + ensure_agent(); + todo!() } @@ -144,6 +180,15 @@ fn main() { .about("unofficial bitwarden cli") .author(clap::crate_authors!()) .version(clap::crate_version!()) + .subcommand( + clap::SubCommand::with_name("config") + .subcommand(clap::SubCommand::with_name("show")) + .subcommand( + clap::SubCommand::with_name("set") + .arg(clap::Arg::with_name("key")) + .arg(clap::Arg::with_name("value")), + ), + ) .subcommand(clap::SubCommand::with_name("login")) .subcommand(clap::SubCommand::with_name("unlock")) .subcommand(clap::SubCommand::with_name("sync")) @@ -158,9 +203,18 @@ fn main() { .subcommand(clap::SubCommand::with_name("stop-agent")) .get_matches(); - ensure_agent(); - match matches.subcommand() { + ("config", Some(smatches)) => match smatches.subcommand() { + ("show", Some(_)) => config_show(), + ("set", Some(ssmatches)) => config_set( + ssmatches.value_of("key").unwrap(), + ssmatches.value_of("value").unwrap(), + ), + _ => { + eprintln!("{}", smatches.usage()); + std::process::exit(1); + } + }, ("login", Some(_)) => login(), ("unlock", Some(_)) => unlock(), ("sync", Some(_)) => sync(), @@ -173,6 +227,9 @@ fn main() { ("lock", Some(_)) => lock(), ("purge", Some(_)) => purge(), ("stop-agent", Some(_)) => stop_agent(), - _ => unimplemented!(), + _ => { + eprintln!("{}", matches.usage()); + std::process::exit(1); + } } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d02bc11 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,76 @@ +use crate::prelude::*; + +use std::io::{Read as _, Write as _}; +use tokio::io::AsyncReadExt as _; + +#[derive(serde::Serialize, serde::Deserialize, Default, Debug)] +pub struct Config { + pub email: Option, + pub base_url: Option, + pub identity_url: Option, +} + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn load() -> Result { + let mut fh = std::fs::File::open(Self::filename()) + .context(crate::error::LoadConfig)?; + let mut json = String::new(); + fh.read_to_string(&mut json) + .context(crate::error::LoadConfig)?; + let slf: Self = serde_json::from_str(&json) + .context(crate::error::LoadConfigJson)?; + Ok(slf) + } + + pub async fn load_async() -> Result { + let mut fh = tokio::fs::File::open(Self::filename()) + .await + .context(crate::error::LoadConfigAsync)?; + let mut json = String::new(); + fh.read_to_string(&mut json) + .await + .context(crate::error::LoadConfigAsync)?; + let slf: Self = serde_json::from_str(&json) + .context(crate::error::LoadConfigJson)?; + Ok(slf) + } + + pub fn save(&self) -> Result<()> { + let filename = Self::filename(); + std::fs::create_dir_all(filename.parent().unwrap()) + .context(crate::error::SaveConfig)?; + let mut fh = std::fs::File::create(filename) + .context(crate::error::SaveConfig)?; + fh.write_all( + serde_json::to_string(self) + .context(crate::error::SaveConfigJson)? + .as_bytes(), + ) + .context(crate::error::SaveConfig)?; + Ok(()) + } + + pub fn base_url(&self) -> String { + self.base_url.clone().map_or_else( + || "https://api.bitwarden.com".to_string(), + |url| format!("{}/api", url), + ) + } + + pub fn identity_url(&self) -> String { + self.identity_url.clone().unwrap_or_else(|| { + self.base_url.clone().map_or_else( + || "https://identity.bitwarden.com".to_string(), + |url| format!("{}/identity", url), + ) + }) + } + + fn filename() -> std::path::PathBuf { + crate::dirs::config_dir().join("config.json") + } +} diff --git a/src/error.rs b/src/error.rs index 8642e5e..4554b14 100644 --- a/src/error.rs +++ b/src/error.rs @@ -39,6 +39,15 @@ pub enum Error { #[snafu(display("invalid mac key"))] InvalidMacKey, + #[snafu(display("failed to load config: {}", source))] + LoadConfig { source: std::io::Error }, + + #[snafu(display("failed to load config: {}", source))] + LoadConfigAsync { source: tokio::io::Error }, + + #[snafu(display("failed to load config: {}", source))] + LoadConfigJson { source: serde_json::Error }, + #[snafu(display("error reading pinentry output: {}", source))] PinentryReadOutput { source: tokio::io::Error }, @@ -48,6 +57,12 @@ pub enum Error { #[snafu(display("error making api request: {}", source))] Reqwest { source: reqwest::Error }, + #[snafu(display("failed to save config: {}", source))] + SaveConfig { source: std::io::Error }, + + #[snafu(display("failed to save config: {}", source))] + SaveConfigJson { source: serde_json::Error }, + #[snafu(display("error spawning pinentry: {}", source))] Spawn { source: tokio::io::Error }, diff --git a/src/lib.rs b/src/lib.rs index 340de00..62a06c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod actions; pub mod agent; pub mod api; pub mod cipherstring; +pub mod config; pub mod dirs; mod error; pub mod identity; -- cgit v1.2.3-54-g00ecf