use std::fs::File; use std::io::BufReader; use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; use std::sync::Arc; use clap::ArgMatches; use color_eyre::eyre::eyre; use cookie::Cookie; use cookie_store::CookieStore; use josekit::jwk::Jwk; use reqwest::Proxy; use reqwest_cookie_store::CookieStoreMutex; use rpc_toolkit::reqwest::{Client, Url}; use rpc_toolkit::url::Host; use rpc_toolkit::Context; use serde::Deserialize; use tracing::instrument; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::util::config::{load_config_from_paths, local_config_path}; use crate::ResultExt; use super::setup::CURRENT_SECRET; #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct CliContextConfig { pub host: Option, #[serde(deserialize_with = "crate::util::serde::deserialize_from_str_opt")] #[serde(default)] pub proxy: Option, pub cookie_path: Option, } #[derive(Debug)] pub struct CliContextSeed { pub base_url: Url, pub rpc_url: Url, pub client: Client, pub cookie_store: Arc, pub cookie_path: PathBuf, } impl Drop for CliContextSeed { fn drop(&mut self) { let tmp = format!("{}.tmp", self.cookie_path.display()); let parent_dir = self.cookie_path.parent().unwrap_or(Path::new("/")); if !parent_dir.exists() { std::fs::create_dir_all(&parent_dir).unwrap(); } let mut writer = fd_lock_rs::FdLock::lock( File::create(&tmp).unwrap(), fd_lock_rs::LockType::Exclusive, true, ) .unwrap(); let store = self.cookie_store.lock().unwrap(); store.save_json(&mut *writer).unwrap(); writer.sync_all().unwrap(); std::fs::rename(tmp, &self.cookie_path).unwrap(); } } const DEFAULT_HOST: Host<&'static str> = Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)); const DEFAULT_PORT: u16 = 5959; #[derive(Debug, Clone)] pub struct CliContext(Arc); impl CliContext { /// BLOCKING #[instrument(skip(matches))] pub fn init(matches: &ArgMatches) -> Result { let local_config_path = local_config_path(); let base: CliContextConfig = load_config_from_paths( matches .values_of("config") .into_iter() .flatten() .map(|p| Path::new(p)) .chain(local_config_path.as_deref().into_iter()) .chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))), )?; let mut url = if let Some(host) = matches.value_of("host") { host.parse()? } else if let Some(host) = base.host { host } else { "http://localhost".parse()? }; let proxy = if let Some(proxy) = matches.value_of("proxy") { Some(proxy.parse()?) } else { base.proxy }; let cookie_path = base.cookie_path.unwrap_or_else(|| { local_config_path .as_deref() .unwrap_or_else(|| Path::new(crate::util::config::CONFIG_PATH)) .parent() .unwrap_or(Path::new("/")) .join(".cookies.json") }); let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() { let mut store = CookieStore::load_json(BufReader::new(File::open(&cookie_path)?)) .map_err(|e| eyre!("{}", e)) .with_kind(crate::ErrorKind::Deserialization)?; if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) { store .insert_raw(&Cookie::new("local", local), &"http://localhost".parse()?) .with_kind(crate::ErrorKind::Network)?; } store } else { CookieStore::default() })); Ok(CliContext(Arc::new(CliContextSeed { base_url: url.clone(), rpc_url: { url.path_segments_mut() .map_err(|_| eyre!("Url cannot be base")) .with_kind(crate::ErrorKind::ParseUrl)? .push("rpc") .push("v1"); url }, client: { let mut builder = Client::builder().cookie_provider(cookie_store.clone()); if let Some(proxy) = proxy { builder = builder.proxy(Proxy::all(proxy).with_kind(crate::ErrorKind::ParseUrl)?) } builder.build().expect("cannot fail") }, cookie_store, cookie_path, }))) } } impl AsRef for CliContext { fn as_ref(&self) -> &Jwk { &*CURRENT_SECRET } } impl std::ops::Deref for CliContext { type Target = CliContextSeed; fn deref(&self) -> &Self::Target { &*self.0 } } impl Context for CliContext { fn protocol(&self) -> &str { self.0.base_url.scheme() } fn host(&self) -> Host<&str> { self.0.base_url.host().unwrap_or(DEFAULT_HOST) } fn port(&self) -> u16 { self.0.base_url.port().unwrap_or(DEFAULT_PORT) } fn path(&self) -> &str { self.0.rpc_url.path() } fn url(&self) -> Url { self.0.rpc_url.clone() } fn client(&self) -> &Client { &self.0.client } } /// When we had an empty proxy the system wasn't working like it used to, which allowed empty proxy #[test] fn test_cli_proxy_empty() { serde_yaml::from_str::( " bind_rpc: ", ) .unwrap(); }