use std::{ fs, io, path::{Path, PathBuf}, }; use brk_error::{Error, Result}; use brk_rpc::{Auth, Client}; use brk_server::{CdnCacheMode, DEFAULT_MAX_UTXOS, DEFAULT_MAX_WEIGHT, Website}; use brk_types::Port; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; use crate::{default_brk_path, dot_brk_path, fix_user_path}; #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { #[serde(default)] brkdir: Option, #[serde(default)] brkport: Option, #[serde(default)] website: Option, #[serde(default)] cdn: Option, #[serde(default)] maxweight: Option, #[serde(default)] maxutxos: Option, #[serde(default)] bitcoindir: Option, #[serde(default)] blocksdir: Option, #[serde(default)] rpcconnect: Option, #[serde(default)] rpcport: Option, #[serde(default)] rpccookiefile: Option, #[serde(default)] rpcuser: Option, #[serde(default)] rpcpassword: Option, } impl Config { pub fn import() -> Result { let config_args = Self::parse_args(); let path = dot_brk_path(); let _ = fs::create_dir_all(&path); let path = path.join("config.toml"); let mut config = Self::read(&path); if let Some(v) = config_args.brkdir { config.brkdir = Some(v); } if let Some(v) = config_args.brkport { config.brkport = Some(v); } if let Some(v) = config_args.website { config.website = Some(v); } if let Some(v) = config_args.cdn { config.cdn = Some(v); } if let Some(v) = config_args.maxweight { config.maxweight = Some(v); } if let Some(v) = config_args.maxutxos { config.maxutxos = Some(v); } if let Some(v) = config_args.bitcoindir { config.bitcoindir = Some(v); } if let Some(v) = config_args.blocksdir { config.blocksdir = Some(v); } if let Some(v) = config_args.rpcconnect { config.rpcconnect = Some(v); } if let Some(v) = config_args.rpcport { config.rpcport = Some(v); } if let Some(v) = config_args.rpccookiefile { config.rpccookiefile = Some(v); } if let Some(v) = config_args.rpcuser { config.rpcuser = Some(v); } if let Some(v) = config_args.rpcpassword { config.rpcpassword = Some(v); } config.check(); Ok(config) } fn parse_args() -> Self { use lexopt::prelude::*; let mut config = Self::default(); let mut parser = lexopt::Parser::from_env(); while let Some(arg) = parser.next().unwrap() { match arg { Short('h') | Long("help") => { Self::print_help(); std::process::exit(0); } Short('V') | Long("version") => { println!("brk {}", env!("CARGO_PKG_VERSION")); std::process::exit(0); } Long("brkdir") => config.brkdir = Some(parser.value().unwrap().parse().unwrap()), Long("brkport") => config.brkport = Some(parser.value().unwrap().parse().unwrap()), Long("website") => config.website = Some(parser.value().unwrap().parse().unwrap()), Long("cdn") => config.cdn = Some(parser.value().unwrap().parse().unwrap()), Long("maxweight") => { config.maxweight = Some(parser.value().unwrap().parse().unwrap()) } Long("maxutxos") => { config.maxutxos = Some(parser.value().unwrap().parse().unwrap()) } Long("bitcoindir") => { config.bitcoindir = Some(parser.value().unwrap().parse().unwrap()) } Long("blocksdir") => { config.blocksdir = Some(parser.value().unwrap().parse().unwrap()) } Long("rpcconnect") => { config.rpcconnect = Some(parser.value().unwrap().parse().unwrap()) } Long("rpcport") => config.rpcport = Some(parser.value().unwrap().parse().unwrap()), Long("rpccookiefile") => { config.rpccookiefile = Some(parser.value().unwrap().parse().unwrap()) } Long("rpcuser") => config.rpcuser = Some(parser.value().unwrap().parse().unwrap()), Long("rpcpassword") => { config.rpcpassword = Some(parser.value().unwrap().parse().unwrap()) } _ => { eprintln!("{}", arg.unexpected()); std::process::exit(1); } } } config } fn print_help() { let v = env!("CARGO_PKG_VERSION"); println!("{} {}", "brk".bold(), v.bright_black()); println!("Bitcoin Research Kit"); println!(); println!("{}", "USAGE:".bold()); println!( " {} brk {}", "[ENV]".bright_black(), "[OPTIONS]".bright_black() ); println!(); println!("{}", "OPTIONS:".bold()); println!(" -h, --help Print help"); println!(" -V, --version Print version"); println!(); println!( " --brkdir {} Output directory {}", "".bright_black(), "[~/.brk]".bright_black() ); println!( " --brkport {} Server port {}", "".bright_black(), "[3110]".bright_black() ); println!( " --website {} Website {}", "".bright_black(), "[true]".bright_black() ); println!( " --cdn {} Aggressive CDN cache, requires purge on deploy {}", "".bright_black(), "[false]".bright_black() ); println!( " --maxweight {} Server cap on series response weight in bytes; rejects /api/{{series,metric}}/... over the limit {}", "".bright_black(), format!("[{}]", DEFAULT_MAX_WEIGHT).bright_black() ); println!( " --maxutxos {} Server cap on UTXOs per address; /api/address/{{addr}}/utxo errors past the limit {}", "".bright_black(), format!("[{}]", DEFAULT_MAX_UTXOS).bright_black() ); println!(); println!( " --bitcoindir {} Bitcoin directory {}", "".bright_black(), "[OS default]".bright_black() ); println!( " --blocksdir {} Blocks directory {}", "".bright_black(), "[/blocks]".bright_black() ); println!(); println!( " --rpcconnect {} RPC host {}", "".bright_black(), "[localhost]".bright_black() ); println!( " --rpcport {} RPC port {}", "".bright_black(), "[8332]".bright_black() ); println!( " --rpccookiefile {} RPC cookie file {}", "".bright_black(), "[/.cookie]".bright_black() ); println!( " --rpcuser {} RPC username", "".bright_black() ); println!( " --rpcpassword {} RPC password", "".bright_black() ); println!(); println!("{}", "ENVIRONMENT:".bold()); println!( " LOG={} Log level {}", "".bright_black(), "[info]".bright_black() ); println!( " RUST_LOG={} Full log filter", "".bright_black() ); println!(); println!("{}", "CONFIG:".bold()); println!( " Edit {} to persist settings:", "~/.brk/config.toml".bright_black() ); println!(" {}", "brkdir = \"/path/to/data\"".bright_black()); println!( " {}", "bitcoindir = \"/path/to/.bitcoin\"".bright_black() ); } fn check(&self) { if !self.bitcoindir().is_dir() { println!("{:?} isn't a valid directory", self.bitcoindir()); println!("Please use the --bitcoindir parameter to set a valid path."); println!("Run the program with '-h' for help."); std::process::exit(1); } if !self.blocksdir().is_dir() { println!("{:?} isn't a valid directory", self.blocksdir()); println!("Please use the --blocksdir parameter to set a valid path."); println!("Run the program with '-h' for help."); std::process::exit(1); } if !self.brkdir().is_dir() { println!("{:?} isn't a valid directory", self.brkdir()); println!("Please use the --brkdir parameter to set a valid path."); println!("Run the program with '-h' for help."); std::process::exit(1); } if self.rpc_auth().is_err() { println!( "Unsuccessful authentication with the RPC client. First make sure that `bitcoind` is running. If it is then please either set --rpccookiefile or --rpcuser and --rpcpassword as the default values seemed to have failed. Finally, you can run the program with '-h' for help." ); std::process::exit(1); } } fn read(path: &Path) -> Self { let contents = match fs::read_to_string(path) { Ok(contents) => contents, Err(e) if e.kind() == io::ErrorKind::NotFound => return Config::default(), Err(e) => { eprintln!("Cannot read {}: {e}", path.display()); std::process::exit(1); } }; toml::from_str(&contents).unwrap_or_else(|e| { eprintln!("Invalid {}:\n{e}", path.display()); std::process::exit(1); }) } pub fn rpc(&self) -> Result { Client::new( &format!( "http://{}:{}", self.rpcconnect().unwrap_or(&"localhost".to_string()), self.rpcport().unwrap_or(8332) ), self.rpc_auth()?, ) } fn rpc_auth(&self) -> Result { let cookie = self.path_cookiefile(); if cookie.is_file() { Ok(Auth::CookieFile(cookie)) } else if self.rpcuser.is_some() && self.rpcpassword.is_some() { Ok(Auth::UserPass( self.rpcuser.clone().unwrap(), self.rpcpassword.clone().unwrap(), )) } else { Err(Error::AuthFailed) } } fn rpcconnect(&self) -> Option<&String> { self.rpcconnect.as_ref() } fn rpcport(&self) -> Option { self.rpcport } pub fn bitcoindir(&self) -> PathBuf { self.bitcoindir .as_ref() .map_or_else(Client::default_bitcoin_path, |s| fix_user_path(s.as_ref())) } pub fn blocksdir(&self) -> PathBuf { self.blocksdir.as_ref().map_or_else( || self.bitcoindir().join("blocks"), |blocksdir| fix_user_path(blocksdir.as_str()), ) } pub fn brkdir(&self) -> PathBuf { self.brkdir .as_ref() .map_or_else(default_brk_path, |s| fix_user_path(s.as_ref())) } fn path_cookiefile(&self) -> PathBuf { self.rpccookiefile.as_ref().map_or_else( || self.bitcoindir().join(".cookie"), |p| fix_user_path(p.as_str()), ) } pub fn website(&self) -> Website { self.website.clone().unwrap_or_default() } pub fn cdn_cache_mode(&self) -> CdnCacheMode { if self.cdn.unwrap_or(false) { CdnCacheMode::Aggressive } else { CdnCacheMode::Live } } pub fn max_weight(&self) -> usize { self.maxweight.unwrap_or(DEFAULT_MAX_WEIGHT) } pub fn max_utxos(&self) -> usize { self.maxutxos.unwrap_or(DEFAULT_MAX_UTXOS) } pub fn brkport(&self) -> Option { self.brkport } }