Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ readme = "README.md"
[workspace.dependencies]
# All dependencies MUST be pinned precisely with `X.Y.z`
axum = { version = "=0.8.9", default-features = false, features = ["http1", "json", "tracing", "tokio"] }
base64 = { version = "=0.22.1", default-features = false, features = ["std"] }
bitcoin = { version = "=0.32.8", default-features = false }
clap = { version = "=4.6.1", default-features = false, features = ["derive", "std"] }
corepc-types = "=0.11.0"
Expand Down
1 change: 1 addition & 0 deletions bin/floresta-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ categories = ["cryptography::cryptocurrencies", "command-line-utilities"]
anyhow = "=1.0.102"
bitcoin = { workspace = true }
clap = { workspace = true, features = ["help"] }
dirs = { version = "=4.0.0", default-features = false }
serde_json = { workspace = true }

# Local dependencies
Expand Down
66 changes: 64 additions & 2 deletions bin/floresta-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use core::fmt::Debug;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
mod parsers;

use anyhow::Context;
use anyhow::Ok;
use bitcoin::BlockHash;
use bitcoin::Network;
use bitcoin::Txid;
use clap::Parser;
use clap::Subcommand;
use floresta_rpc::jsonrpc_client::Client;
use floresta_rpc::jsonrpc_client::JsonRPCConfig;
use floresta_rpc::rpc::FlorestaRPC;
use floresta_rpc::rpc_types::AddNodeCommand;
use floresta_rpc::rpc_types::RescanConfidence;
Expand All @@ -20,8 +24,16 @@ fn main() -> anyhow::Result<()> {
// Parse command line arguments into a Cli struct
let cli = Cli::parse();

// Create a new JSON-RPC client using the host from the CLI arguments
let client = Client::new(get_host(&cli));
// Resolve basic-auth credentials: explicit flags win, otherwise read the
// cookie file written by florestad at startup.
let (user, pass) = resolve_credentials(&cli)?;

// Create a new JSON-RPC client using the host and credentials.
let client = Client::new_with_config(JsonRPCConfig {
url: get_host(&cli),
user: Some(user),
pass: Some(pass),
});

// Perform the requested RPC call and get the result
let res = do_request(&cli, client)?;
Expand All @@ -33,6 +45,53 @@ fn main() -> anyhow::Result<()> {
anyhow::Ok(())
}

// Resolve the basic-auth credentials for the RPC call.
//
// Precedence:
// 1. `--rpc-user` and `--rpc-password` both set: use them.
// 2. Otherwise read the cookie file at `--rpc-cookie-file` (or the network's
// default location) and split on the first `:` into (user, pass).
fn resolve_credentials(cli: &Cli) -> anyhow::Result<(String, String)> {
if let (Some(user), Some(pass)) = (cli.rpc_user.clone(), cli.rpc_password.clone()) {
return anyhow::Ok((user, pass));
}
let cookie_path = cli
.rpc_cookie_file
.clone()
.unwrap_or_else(|| default_cookie_path(cli.network));
read_cookie(&cookie_path)
}

// Read a cookie file and split it into (user, pass) on the first `:`.
fn read_cookie(path: &Path) -> anyhow::Result<(String, String)> {
let contents = fs::read_to_string(path).with_context(|| {
format!(
"failed to read RPC cookie file at {}; start florestad first or pass --rpc-user/--rpc-password",
path.display()
)
})?;
let (user, pass) = contents
.split_once(':')
.with_context(|| format!("cookie file at {} is malformed", path.display()))?;
anyhow::Ok((user.to_string(), pass.to_string()))
}

// Compute the default cookie file path for the given network. Mirrors
// florestad's `datadir_path` layout: `~/.floresta/[<net>/].cookie`.
fn default_cookie_path(network: Network) -> PathBuf {
let base = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".floresta");
let net_dir = match network {
Network::Bitcoin => base,
Network::Signet => base.join("signet"),
Network::Testnet => base.join("testnet3"),
Network::Testnet4 => base.join("testnet4"),
Network::Regtest => base.join("regtest"),
};
net_dir.join(".cookie")
}

// Function to determine the RPC host based on CLI arguments and network type
fn get_host(cmd: &Cli) -> String {
// If a specific RPC host is provided, use it
Expand Down Expand Up @@ -164,6 +223,9 @@ pub struct Cli {
/// The RPC password to use
#[arg(short = 'P', long, value_name = "PASSWORD")]
pub rpc_password: Option<String>,
/// Path to the RPC cookie file. Defaults to `<datadir>/[<net>/].cookie`.
#[arg(long, value_name = "PATH")]
pub rpc_cookie_file: Option<PathBuf>,
/// An actual RPC command to run
#[command(subcommand)]
pub methods: Methods,
Expand Down
14 changes: 14 additions & 0 deletions bin/florestad/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ pub struct Cli {
/// The address where our json-rpc server should listen to, in the format `<address>[:<port>]`
pub rpc_address: Option<String>,

#[arg(long, value_name = "USER")]
/// Username for `-rpcuser`/`-rpcpassword` auth.
pub rpc_user: Option<String>,

#[arg(long, value_name = "PASS")]
/// Password for `-rpcuser`/`-rpcpassword` auth; setting it disables cookie auth.
pub rpc_password: Option<String>,

#[arg(long, value_name = "LINE", action = clap::ArgAction::Append)]
/// Pre-hashed credential entry in `<user>:<salt_hex>$<hash_hex>` format.
/// Generate with Bitcoin Core's `share/rpcauth/rpcauth.py`. Repeat for
/// multiple users.
pub rpc_auth: Vec<String>,

#[arg(long, value_name = "HEIGHT")]
/// Download block filters starting at this height. Negative numbers are relative to the current tip.
pub filters_start_height: Option<i32>,
Expand Down
6 changes: 6 additions & 0 deletions bin/florestad/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ fn main() {
zmq_address: params.zmq_address,
#[cfg(feature = "json-rpc")]
json_rpc_address: params.rpc_address,
#[cfg(feature = "json-rpc")]
rpc_user: params.rpc_user,
#[cfg(feature = "json-rpc")]
rpc_password: params.rpc_password,
#[cfg(feature = "json-rpc")]
rpc_auth: params.rpc_auth,
generate_cert: params.generate_cert,
wallet_descriptor: params.wallet_descriptor,
filters_start_height: params.filters_start_height,
Expand Down
4 changes: 3 additions & 1 deletion crates/floresta-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ authors = ["Floresta Developers <contact@getfloresta.org>"]
[dependencies]
# All dependencies MUST be pinned precisely with `X.Y.z`
axum = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }
bitcoin = { workspace = true }
corepc-types = { workspace = true }
dns-lookup = { workspace = true }
miniscript = { workspace = true, features = ["std"] }
rand = { workspace = true }
rcgen = { workspace = true }
rustreexo = { workspace = true }
serde = { workspace = true }
Expand Down Expand Up @@ -48,7 +50,7 @@ libc = "=0.2.186"
[features]
compact-filters = ["dep:floresta-compact-filters"]
zmq-server = ["dep:zmq"]
json-rpc = ["dep:axum", "dep:tower-http", "compact-filters"]
json-rpc = ["dep:axum", "dep:base64", "dep:tower-http", "compact-filters"]
default = ["json-rpc"]
metrics = ["dep:metrics", "floresta-wire/metrics", "floresta-chain/metrics"]

Expand Down
12 changes: 12 additions & 0 deletions crates/floresta-node/src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,21 @@ pub struct Wallet {
pub addresses: Option<Vec<String>>,
}

#[cfg(feature = "json-rpc")]
#[derive(Default, Debug, Deserialize)]
pub struct Rpc {
pub user: Option<String>,
pub password: Option<String>,
#[serde(default)]
pub auth: Vec<String>,
}

#[derive(Default, Debug, Deserialize)]
pub struct ConfigFile {
pub wallet: Wallet,
#[cfg(feature = "json-rpc")]
#[serde(default)]
pub rpc: Rpc,
}

impl ConfigFile {
Expand Down
6 changes: 6 additions & 0 deletions crates/floresta-node/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ pub enum FlorestadError {

/// Load a flat chain store error.
CouldNotLoadFlatChainStore(BlockchainError),

/// A malformed `-rpcauth` entry was provided at startup.
InvalidRpcAuth(String),
}

impl Display for FlorestadError {
Expand Down Expand Up @@ -225,6 +228,9 @@ impl Display for FlorestadError {
FlorestadError::CouldNotLoadFlatChainStore(err) => {
write!(f, "Failure while loading flat chainstore: {err:?}")
}
FlorestadError::InvalidRpcAuth(line) => {
write!(f, "Invalid -rpcauth argument: {line}")
}
}
}
}
Expand Down
95 changes: 95 additions & 0 deletions crates/floresta-node/src/florestad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ pub struct Config {
/// The address our json-rpc should listen to
pub json_rpc_address: Option<String>,

/// Username for `-rpcuser`/`-rpcpassword` auth.
#[cfg(feature = "json-rpc")]
pub rpc_user: Option<String>,

/// Password for `-rpcuser`/`-rpcpassword` auth; setting it disables cookie auth.
#[cfg(feature = "json-rpc")]
pub rpc_password: Option<String>,

/// `-rpcauth` lines: `<user>:<salt_hex>$<hash_hex>`. Repeated zero or more
/// times. Merged additively with config-file entries.
#[cfg(feature = "json-rpc")]
pub rpc_auth: Vec<String>,

/// Whether we should write logs to `stdout`.
pub log_to_stdout: bool,

Expand Down Expand Up @@ -241,6 +254,12 @@ impl Config {
connect: Vec::new(),
#[cfg(feature = "json-rpc")]
json_rpc_address: None,
#[cfg(feature = "json-rpc")]
rpc_user: None,
#[cfg(feature = "json-rpc")]
rpc_password: None,
#[cfg(feature = "json-rpc")]
rpc_auth: Vec::new(),
log_to_stdout: false,
log_to_file: false,
assume_utreexo: false,
Expand Down Expand Up @@ -273,6 +292,11 @@ pub struct Florestad {
#[cfg(feature = "json-rpc")]
/// A handle to our json-rpc server
json_rpc: OnceLock<tokio::task::JoinHandle<()>>,

#[cfg(feature = "json-rpc")]
/// Set once after this process writes the RPC cookie file. Gates cookie
/// deletion at shutdown so we never remove a cookie written by a prior run.
cookie_generated: OnceLock<()>,
}

impl Florestad {
Expand Down Expand Up @@ -304,6 +328,12 @@ impl Florestad {
if let Some(chan) = chan {
try_and_log!(chan.await);
}

#[cfg(feature = "json-rpc")]
if self.cookie_generated.get().is_some() {
let cookie_path = self.config.datadir.join(json_rpc::auth::COOKIE_FILE_NAME);
try_and_log!(json_rpc::auth::delete_cookie(&cookie_path));
}
}

/// Parses an address in the format `<hostname>[<:port>]` and returns a
Expand Down Expand Up @@ -460,6 +490,39 @@ impl Florestad {
// JSON-RPC
#[cfg(feature = "json-rpc")]
{
let mut entries: Vec<json_rpc::auth::RpcAuth> = Vec::new();

if let Some(pass) = self.get_rpc_password() {
let user = self.get_rpc_user().unwrap_or_default();
info!("RPC password auth configured for user '{user}'");
entries.push(json_rpc::auth::RpcAuth::from_password(&user, &pass));
} else {
let cookie_path = datadir.join(json_rpc::auth::COOKIE_FILE_NAME);
let (cookie_user, cookie_pass) = json_rpc::auth::generate_cookie(&cookie_path)?;
let _ = self.cookie_generated.set(());
info!("RPC cookie file written to {}", cookie_path.display());
entries.push(json_rpc::auth::RpcAuth::from_password(
&cookie_user,
&cookie_pass,
));
}

for line in self.get_rpc_auth() {
match json_rpc::auth::RpcAuth::parse(&line) {
Ok(entry) => entries.push(entry),
Err(e) => {
error!("{e}");
return Err(FlorestadError::InvalidRpcAuth(line));
}
}
}

if entries.len() > 1 {
info!("RPC: {} rpcauth entries loaded", entries.len() - 1);
}

let credentials = Arc::new(json_rpc::auth::Auth::new(entries));

let server = tokio::spawn(json_rpc::server::RpcImpl::create(
blockchain_state.clone(),
wallet.clone(),
Expand All @@ -475,6 +538,7 @@ impl Florestad {
datadir.join("debug.log"),
self.config.user_agent.clone(),
proxy,
credentials,
));

if self.json_rpc.set(server).is_err() {
Expand Down Expand Up @@ -758,6 +822,35 @@ impl Florestad {
Ok(wallet)
}

/// Get the configured RPC user; CLI/struct value wins over the config file.
#[cfg(feature = "json-rpc")]
fn get_rpc_user(&self) -> Option<String> {
self.config
.rpc_user
.clone()
.or_else(|| self.get_config_file().rpc.user)
}

/// Get the configured RPC password; CLI/struct value wins over the config file.
#[cfg(feature = "json-rpc")]
fn get_rpc_password(&self) -> Option<String> {
self.config
.rpc_password
.clone()
.or_else(|| self.get_config_file().rpc.password)
}

/// Get all `-rpcauth` lines; CLI vec is merged additively with config-file vec.
#[cfg(feature = "json-rpc")]
fn get_rpc_auth(&self) -> Vec<String> {
self.config
.rpc_auth
.iter()
.chain(self.get_config_file().rpc.auth.iter())
.cloned()
.collect()
}

/// Get the wallet descriptors from the config file
fn get_descriptors(&self) -> Vec<String> {
self.config
Expand Down Expand Up @@ -898,6 +991,8 @@ impl From<Config> for Florestad {
stop_notify: Arc::new(Mutex::new(None)),
#[cfg(feature = "json-rpc")]
json_rpc: OnceLock::new(),
#[cfg(feature = "json-rpc")]
cookie_generated: OnceLock::new(),
}
}
}
Loading
Loading