mirror of
https://github.com/romanz/electrs.git
synced 2026-08-17 13:06:27 +02:00
323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
use bitcoin::network::constants::Network;
|
|
use bitcoincore_rpc::Auth;
|
|
use dirs_next::home_dir;
|
|
|
|
use std::ffi::{OsStr, OsString};
|
|
use std::fmt;
|
|
use std::net::SocketAddr;
|
|
use std::net::ToSocketAddrs;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
|
|
use std::time::Duration;
|
|
|
|
const DEFAULT_SERVER_ADDRESS: [u8; 4] = [127, 0, 0, 1]; // by default, serve on IPv4 localhost
|
|
|
|
mod internal {
|
|
#![allow(unused)]
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/configure_me_config.rs"));
|
|
}
|
|
|
|
/// A simple error type representing invalid UTF-8 input.
|
|
pub struct InvalidUtf8(OsString);
|
|
|
|
impl fmt::Display for InvalidUtf8 {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{:?} isn't a valid UTF-8 sequence", self.0)
|
|
}
|
|
}
|
|
|
|
/// An error that might happen when resolving an address
|
|
pub enum AddressError {
|
|
ResolvError { addr: String, err: std::io::Error },
|
|
NoAddrError(String),
|
|
}
|
|
|
|
impl fmt::Display for AddressError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
AddressError::ResolvError { addr, err } => {
|
|
write!(f, "Failed to resolve address {}: {}", addr, err)
|
|
}
|
|
AddressError::NoAddrError(addr) => write!(f, "No address found for {}", addr),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Newtype for an address that is parsed as `String`
|
|
///
|
|
/// The main point of this newtype is to provide better description than what `String` type
|
|
/// provides.
|
|
#[derive(Deserialize)]
|
|
pub struct ResolvAddr(String);
|
|
|
|
impl ::configure_me::parse_arg::ParseArg for ResolvAddr {
|
|
type Error = InvalidUtf8;
|
|
|
|
fn parse_arg(arg: &OsStr) -> std::result::Result<Self, Self::Error> {
|
|
Self::parse_owned_arg(arg.to_owned())
|
|
}
|
|
|
|
fn parse_owned_arg(arg: OsString) -> std::result::Result<Self, Self::Error> {
|
|
arg.into_string().map_err(InvalidUtf8).map(ResolvAddr)
|
|
}
|
|
|
|
fn describe_type<W: fmt::Write>(mut writer: W) -> fmt::Result {
|
|
write!(writer, "a network address (will be resolved if needed)")
|
|
}
|
|
}
|
|
|
|
impl ResolvAddr {
|
|
/// Resolves the address.
|
|
fn resolve(self) -> std::result::Result<SocketAddr, AddressError> {
|
|
match self.0.to_socket_addrs() {
|
|
Ok(mut iter) => iter.next().ok_or(AddressError::NoAddrError(self.0)),
|
|
Err(err) => Err(AddressError::ResolvError { addr: self.0, err }),
|
|
}
|
|
}
|
|
|
|
/// Resolves the address, but prints error and exits in case of failure.
|
|
fn resolve_or_exit(self) -> SocketAddr {
|
|
self.resolve().unwrap_or_else(|err| {
|
|
eprintln!("Error: {}", err);
|
|
std::process::exit(1)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// This newtype implements `ParseArg` for `Network`.
|
|
#[derive(Deserialize)]
|
|
pub struct BitcoinNetwork(Network);
|
|
|
|
impl Default for BitcoinNetwork {
|
|
fn default() -> Self {
|
|
BitcoinNetwork(Network::Bitcoin)
|
|
}
|
|
}
|
|
|
|
impl FromStr for BitcoinNetwork {
|
|
type Err = <Network as FromStr>::Err;
|
|
|
|
fn from_str(string: &str) -> std::result::Result<Self, Self::Err> {
|
|
Network::from_str(string).map(BitcoinNetwork)
|
|
}
|
|
}
|
|
|
|
impl ::configure_me::parse_arg::ParseArgFromStr for BitcoinNetwork {
|
|
fn describe_type<W: fmt::Write>(mut writer: W) -> fmt::Result {
|
|
write!(writer, "either 'bitcoin', 'testnet', 'regtest' or 'signet'")
|
|
}
|
|
}
|
|
|
|
impl From<BitcoinNetwork> for Network {
|
|
fn from(network: BitcoinNetwork) -> Network {
|
|
network.0
|
|
}
|
|
}
|
|
|
|
/// Parsed and post-processed configuration
|
|
#[derive(Debug)]
|
|
pub struct Config {
|
|
// See below for the documentation of each field:
|
|
pub network: Network,
|
|
pub db_path: PathBuf,
|
|
pub daemon_dir: PathBuf,
|
|
pub daemon_auth: SensitiveAuth,
|
|
pub daemon_rpc_addr: SocketAddr,
|
|
pub daemon_p2p_addr: SocketAddr,
|
|
pub electrum_rpc_addr: SocketAddr,
|
|
pub monitoring_addr: SocketAddr,
|
|
pub wait_duration: Duration,
|
|
pub index_batch_size: usize,
|
|
pub ignore_mempool: bool,
|
|
pub server_banner: String,
|
|
pub args: Vec<String>,
|
|
}
|
|
|
|
pub struct SensitiveAuth(pub Auth);
|
|
|
|
impl SensitiveAuth {
|
|
pub(crate) fn get_auth(&self) -> Auth {
|
|
self.0.clone()
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for SensitiveAuth {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self.0 {
|
|
Auth::UserPass(ref user, _) => f
|
|
.debug_tuple("UserPass")
|
|
.field(&user)
|
|
.field(&"<sensitive>")
|
|
.finish(),
|
|
_ => write!(f, "{:?}", self.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns default daemon directory
|
|
fn default_daemon_dir() -> PathBuf {
|
|
let mut home = home_dir().unwrap_or_else(|| {
|
|
eprintln!("Error: unknown home directory");
|
|
std::process::exit(1)
|
|
});
|
|
home.push(".bitcoin");
|
|
home
|
|
}
|
|
|
|
impl Config {
|
|
/// Parses args, env vars, config files and post-processes them
|
|
pub fn from_args() -> Config {
|
|
use internal::ResultExt;
|
|
|
|
let system_config: &OsStr = "/etc/electrs/config.toml".as_ref();
|
|
let home_config = home_dir().map(|mut dir| {
|
|
dir.extend(&[".electrs", "config.toml"]);
|
|
dir
|
|
});
|
|
let cwd_config: &OsStr = "electrs.toml".as_ref();
|
|
let configs = std::iter::once(cwd_config)
|
|
.chain(home_config.as_ref().map(AsRef::as_ref))
|
|
.chain(std::iter::once(system_config));
|
|
|
|
let (mut config, args) =
|
|
internal::Config::including_optional_config_files(configs).unwrap_or_exit();
|
|
|
|
let db_subdir = match config.network {
|
|
Network::Bitcoin => "bitcoin",
|
|
Network::Testnet => "testnet",
|
|
Network::Regtest => "regtest",
|
|
Network::Signet => "signet",
|
|
};
|
|
|
|
config.db_dir.push(db_subdir);
|
|
|
|
let default_daemon_rpc_port = match config.network {
|
|
Network::Bitcoin => 8332,
|
|
Network::Testnet => 18332,
|
|
Network::Regtest => 18443,
|
|
Network::Signet => 38332,
|
|
};
|
|
let default_daemon_p2p_port = match config.network {
|
|
Network::Bitcoin => 8333,
|
|
Network::Testnet => 18333,
|
|
Network::Regtest => 18444,
|
|
Network::Signet => 38333,
|
|
};
|
|
let default_electrum_port = match config.network {
|
|
Network::Bitcoin => 50001,
|
|
Network::Testnet => 60001,
|
|
Network::Regtest => 60401,
|
|
Network::Signet => 60601,
|
|
};
|
|
let default_monitoring_port = match config.network {
|
|
Network::Bitcoin => 4224,
|
|
Network::Testnet => 14224,
|
|
Network::Regtest => 24224,
|
|
Network::Signet => 34224,
|
|
};
|
|
|
|
let daemon_rpc_addr: SocketAddr = config.daemon_rpc_addr.map_or(
|
|
(DEFAULT_SERVER_ADDRESS, default_daemon_rpc_port).into(),
|
|
ResolvAddr::resolve_or_exit,
|
|
);
|
|
let daemon_p2p_addr: SocketAddr = config.daemon_p2p_addr.map_or(
|
|
(DEFAULT_SERVER_ADDRESS, default_daemon_p2p_port).into(),
|
|
ResolvAddr::resolve_or_exit,
|
|
);
|
|
let electrum_rpc_addr: SocketAddr = config.electrum_rpc_addr.map_or(
|
|
(DEFAULT_SERVER_ADDRESS, default_electrum_port).into(),
|
|
ResolvAddr::resolve_or_exit,
|
|
);
|
|
#[cfg(not(feature = "metrics"))]
|
|
{
|
|
if config.monitoring_addr.is_some() {
|
|
eprintln!("Error: enable \"metrics\" feature to specify monitoring_addr");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
let monitoring_addr: SocketAddr = config.monitoring_addr.map_or(
|
|
(DEFAULT_SERVER_ADDRESS, default_monitoring_port).into(),
|
|
ResolvAddr::resolve_or_exit,
|
|
);
|
|
|
|
match config.network {
|
|
Network::Bitcoin => (),
|
|
Network::Testnet => config.daemon_dir.push("testnet3"),
|
|
Network::Regtest => config.daemon_dir.push("regtest"),
|
|
Network::Signet => config.daemon_dir.push("signet"),
|
|
}
|
|
|
|
let daemon_dir = &config.daemon_dir;
|
|
let daemon_auth = SensitiveAuth(match (config.auth, config.cookie_file) {
|
|
(None, None) => Auth::CookieFile(daemon_dir.join(".cookie")),
|
|
(None, Some(cookie_file)) => Auth::CookieFile(cookie_file),
|
|
(Some(auth), None) => {
|
|
let parts: Vec<&str> = auth.splitn(2, ":").collect();
|
|
if parts.len() != 2 {
|
|
eprintln!("Error: auth cookie doesn't contain colon");
|
|
std::process::exit(1);
|
|
}
|
|
Auth::UserPass(parts[0].to_owned(), parts[1].to_owned())
|
|
}
|
|
(Some(_), Some(_)) => {
|
|
eprintln!("Error: ambigous configuration - auth and cookie_file can't be specified at the same time");
|
|
std::process::exit(1);
|
|
}
|
|
});
|
|
|
|
let level = match config.verbose {
|
|
0 => log::LevelFilter::Info,
|
|
1 => log::LevelFilter::Debug,
|
|
_ => log::LevelFilter::Trace,
|
|
};
|
|
|
|
let config = Config {
|
|
network: config.network,
|
|
db_path: config.db_dir,
|
|
daemon_dir: config.daemon_dir,
|
|
daemon_auth,
|
|
daemon_rpc_addr,
|
|
daemon_p2p_addr,
|
|
electrum_rpc_addr,
|
|
monitoring_addr,
|
|
wait_duration: Duration::from_secs(config.wait_duration_secs),
|
|
index_batch_size: config.index_batch_size,
|
|
ignore_mempool: config.ignore_mempool,
|
|
server_banner: config.server_banner,
|
|
args: args.map(|a| a.into_string().unwrap()).collect(),
|
|
};
|
|
eprintln!("{:?}", config);
|
|
env_logger::Builder::from_default_env()
|
|
.default_format()
|
|
.format_timestamp_millis()
|
|
.filter_level(level)
|
|
.init();
|
|
config
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{Auth, SensitiveAuth};
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn test_auth_debug() {
|
|
let auth = Auth::None;
|
|
assert_eq!(format!("{:?}", SensitiveAuth(auth)), "None");
|
|
|
|
let auth = Auth::CookieFile(Path::new("/foo/bar/.cookie").to_path_buf());
|
|
assert_eq!(
|
|
format!("{:?}", SensitiveAuth(auth)),
|
|
"CookieFile(\"/foo/bar/.cookie\")"
|
|
);
|
|
|
|
let auth = Auth::UserPass("user".to_owned(), "pass".to_owned());
|
|
assert_eq!(
|
|
format!("{:?}", SensitiveAuth(auth)),
|
|
"UserPass(\"user\", \"<sensitive>\")"
|
|
);
|
|
}
|
|
}
|