Merge #890: config: add user:password option for RPC authentication

552a49edb2dbccda5204f80188212fc6b59e029e config: add user:password option for RPC authentication (jp1ac4)

Pull request description:

  As a first step towards #356, this adds the option to specify a user and password in Liana's config file for bitcoind RPC authentication.

  The GUI installer & settings will need to be updated in a follow-up PR to add this option.

ACKs for top commit:
  darosior:
    ACK 552a49edb2dbccda5204f80188212fc6b59e029e

Tree-SHA512: 2a1b5214935a313d36b85c310b28c8e1946375d80af53514ed44987b26383a3824a28f1e70c079ed933c53707a7fb672e3a48dc77f7e585acca217b04d376f77
This commit is contained in:
Antoine Poinsot 2023-12-21 14:22:50 +01:00
commit 21e064c50e
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 177 additions and 21 deletions

View File

@ -31,8 +31,10 @@ poll_interval_secs = 30
# This section is specific to the bitcoind implementation of the Bitcoin backend. This is the only # This section is specific to the bitcoind implementation of the Bitcoin backend. This is the only
# implementation available for now. # implementation available for now.
# In order to be able to connect to bitcoind, it needs to know on what port it is listening as well # In order to be able to connect to bitcoind, it needs to know on what port it is listening and
# as where the authentication cookie is located. # how to authenticate, either by specifying the cookie location with "cookie_path" or otherwise
# passing a colon-separated user and password with "auth".
[bitcoind_config] [bitcoind_config]
addr = "127.0.0.1:18332" addr = "127.0.0.1:18332"
cookie_path = "/home/wizardsardine/.bitcoin/testnet3/.cookie" cookie_path = "/home/wizardsardine/.bitcoin/testnet3/.cookie"
# auth = "my_user:my_password"

View File

@ -228,34 +228,43 @@ impl BitcoinD {
config: &config::BitcoindConfig, config: &config::BitcoindConfig,
watchonly_wallet_path: String, watchonly_wallet_path: String,
) -> Result<BitcoinD, BitcoindError> { ) -> Result<BitcoinD, BitcoindError> {
let cookie_string =
fs::read_to_string(&config.cookie_path).map_err(BitcoindError::CookieFile)?;
let node_url = format!("http://{}", config.addr); let node_url = format!("http://{}", config.addr);
let watchonly_url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path); let watchonly_url = format!("http://{}/wallet/{}", config.addr, watchonly_wallet_path);
let builder = match &config.rpc_auth {
config::BitcoindRpcAuth::CookieFile(cookie_path) => {
let cookie_string =
fs::read_to_string(cookie_path).map_err(BitcoindError::CookieFile)?;
MinreqHttpTransport::builder().cookie_auth(cookie_string)
}
config::BitcoindRpcAuth::UserPass(user, pass) => {
MinreqHttpTransport::builder().basic_auth(user.clone(), Some(pass.clone()))
}
};
// Create a dummy bitcoind with clients using a low timeout to sanity check the connection. // Create a dummy bitcoind with clients using a low timeout to sanity check the connection.
let dummy_node_client = Client::with_transport( let dummy_node_client = Client::with_transport(
MinreqHttpTransport::builder() builder
.clone()
.url(&node_url) .url(&node_url)
.map_err(BitcoindError::from)? .map_err(BitcoindError::from)?
.timeout(Duration::from_secs(3)) .timeout(Duration::from_secs(3))
.cookie_auth(cookie_string.clone())
.build(), .build(),
); );
let sendonly_client = Client::with_transport( let sendonly_client = Client::with_transport(
MinreqHttpTransport::builder() builder
.clone()
.url(&watchonly_url) .url(&watchonly_url)
.map_err(BitcoindError::from)? .map_err(BitcoindError::from)?
.timeout(Duration::from_secs(1)) .timeout(Duration::from_secs(1))
.cookie_auth(cookie_string.clone())
.build(), .build(),
); );
let dummy_wo_client = Client::with_transport( let dummy_wo_client = Client::with_transport(
MinreqHttpTransport::builder() builder
.clone()
.url(&watchonly_url) .url(&watchonly_url)
.map_err(BitcoindError::from)? .map_err(BitcoindError::from)?
.timeout(Duration::from_secs(3)) .timeout(Duration::from_secs(3))
.cookie_auth(cookie_string.clone())
.build(), .build(),
); );
let dummy_bitcoind = BitcoinD { let dummy_bitcoind = BitcoinD {
@ -271,27 +280,26 @@ impl BitcoinD {
// Now the connection is checked, create the clients with an appropriate timeout. // Now the connection is checked, create the clients with an appropriate timeout.
let node_client = Client::with_transport( let node_client = Client::with_transport(
MinreqHttpTransport::builder() builder
.clone()
.url(&node_url) .url(&node_url)
.map_err(BitcoindError::from)? .map_err(BitcoindError::from)?
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT)) .timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
.cookie_auth(cookie_string.clone())
.build(), .build(),
); );
let sendonly_client = Client::with_transport( let sendonly_client = Client::with_transport(
MinreqHttpTransport::builder() builder
.clone()
.url(&watchonly_url) .url(&watchonly_url)
.map_err(BitcoindError::from)? .map_err(BitcoindError::from)?
.timeout(Duration::from_secs(1)) .timeout(Duration::from_secs(1))
.cookie_auth(cookie_string.clone())
.build(), .build(),
); );
let watchonly_client = Client::with_transport( let watchonly_client = Client::with_transport(
MinreqHttpTransport::builder() builder
.url(&watchonly_url) .url(&watchonly_url)
.map_err(BitcoindError::from)? .map_err(BitcoindError::from)?
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT)) .timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
.cookie_auth(cookie_string)
.build(), .build(),
); );
Ok(BitcoinD { Ok(BitcoinD {

View File

@ -35,6 +35,43 @@ pub fn serialize_duration<S: Serializer>(duration: &Duration, s: S) -> Result<S:
s.serialize_u64(duration.as_secs()) s.serialize_u64(duration.as_secs())
} }
fn deserialize_rpc_auth<'de, D>(deserializer: D) -> Result<BitcoindRpcAuth, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
pub struct BitcoindRpcAuthHelper {
cookie_path: Option<PathBuf>,
auth: Option<String>,
}
let BitcoindRpcAuthHelper { cookie_path, auth } =
BitcoindRpcAuthHelper::deserialize(deserializer)?;
let rpc_auth = match (cookie_path, auth) {
(Some(_), Some(_)) => {
return Err(de::Error::custom(
"must not set both `cookie_path` and `auth`",
));
}
(Some(path), None) => BitcoindRpcAuth::CookieFile(path),
(None, Some(auth)) => auth
.split_once(':')
.ok_or(de::Error::custom("`auth` must be 'user:password'"))
.map(|(user, pass)| BitcoindRpcAuth::UserPass(user.to_string(), pass.to_string()))?,
(None, None) => {
return Err(de::Error::custom("must set either `cookie_path` or `auth`"));
}
};
Ok(rpc_auth)
}
fn serialize_userpass<S: Serializer>(
user: &String,
password: &String,
s: S,
) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{}:{}", user, password))
}
fn default_loglevel() -> log::LevelFilter { fn default_loglevel() -> log::LevelFilter {
log::LevelFilter::Info log::LevelFilter::Info
} }
@ -48,11 +85,23 @@ fn default_daemon() -> bool {
false false
} }
/// RPC authentication options.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum BitcoindRpcAuth {
/// Path to bitcoind's cookie file.
#[serde(rename = "cookie_path")]
CookieFile(PathBuf),
/// "USER:PASSWORD" for authentication.
#[serde(rename = "auth", serialize_with = "serialize_userpass")]
UserPass(String, String),
}
/// Everything we need to know for talking to bitcoind serenely /// Everything we need to know for talking to bitcoind serenely
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BitcoindConfig { pub struct BitcoindConfig {
/// Path to bitcoind's cookie file, to authenticate the RPC connection /// Authentication credentials for bitcoind's RPC server.
pub cookie_path: PathBuf, #[serde(flatten, deserialize_with = "deserialize_rpc_auth")]
pub rpc_auth: BitcoindRpcAuth,
/// The IP:port bitcoind's RPC is listening on /// The IP:port bitcoind's RPC is listening on
pub addr: SocketAddr, pub addr: SocketAddr,
} }
@ -217,7 +266,9 @@ impl Config {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{config_file_path, Config}; use std::path::PathBuf;
use super::{config_file_path, BitcoindConfig, BitcoindRpcAuth, Config};
// Test the format of the configuration file // Test the format of the configuration file
#[test] #[test]
@ -259,6 +310,26 @@ mod tests {
#[cfg(unix)] // On non-UNIX there is no 'daemon' member. #[cfg(unix)] // On non-UNIX there is no 'daemon' member.
assert_eq!(toml_str, serialized); assert_eq!(toml_str, serialized);
// A valid, round-tripping, config with `auth` instead of `cookie_path`
let toml_str = r#"
data_dir = '/home/wizardsardine/custom/folder/'
daemon = false
log_level = 'TRACE'
main_descriptor = 'wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs'
[bitcoin_config]
network = 'bitcoin'
poll_interval_secs = 18
[bitcoind_config]
auth = 'my_user:my_password'
addr = '127.0.0.1:8332'
"#.trim_start().replace(" ", "");
let parsed = toml::from_str::<Config>(&toml_str).expect("Deserializing toml_str");
let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml");
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
assert_eq!(toml_str, serialized);
// Invalid desc checksum // Invalid desc checksum
let toml_str = r#" let toml_str = r#"
daemon = false daemon = false
@ -298,6 +369,81 @@ mod tests {
config_res.expect_err("Deserializing an invalid toml_str"); config_res.expect_err("Deserializing an invalid toml_str");
} }
// Test the format of the bitcoind_config section
#[test]
fn toml_bitcoind_config() {
// A valid config with cookie_path
let toml_str = r#"
cookie_path = '/home/user/.bitcoin/.cookie'
addr = '127.0.0.1:8332'
"#
.trim_start()
.replace(" ", "");
toml::from_str::<BitcoindConfig>(&toml_str).expect("Deserializing toml_str");
let parsed = toml::from_str::<BitcoindConfig>(&toml_str).expect("Deserializing toml_str");
let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml");
assert_eq!(toml_str, serialized);
assert_eq!(
parsed.rpc_auth,
BitcoindRpcAuth::CookieFile(PathBuf::from("/home/user/.bitcoin/.cookie"))
);
// A valid config with auth
let toml_str = r#"
auth = 'my_user:my_password'
addr = '127.0.0.1:8332'
"#
.trim_start()
.replace(" ", "");
toml::from_str::<BitcoindConfig>(&toml_str).expect("Deserializing toml_str");
let parsed = toml::from_str::<BitcoindConfig>(&toml_str).expect("Deserializing toml_str");
let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml");
assert_eq!(toml_str, serialized);
assert_eq!(
parsed.rpc_auth,
BitcoindRpcAuth::UserPass("my_user".to_string(), "my_password".to_string())
);
// Must not set both cookie_file and auth
let toml_str = r#"
cookie_path = '/home/user/.bitcoin/.cookie'
auth = 'my_user:my_password'
addr = '127.0.0.1:8332'
"#
.trim_start()
.replace(" ", "");
let config_err = toml::from_str::<BitcoindConfig>(&toml_str)
.expect_err("Deserializing an invalid toml_str");
assert!(config_err
.to_string()
.contains("must not set both `cookie_path` and `auth`"));
// Missing RPC credentials
let toml_str = r#"
addr = '127.0.0.1:8332'
"#
.trim_start()
.replace(" ", "");
let config_err = toml::from_str::<BitcoindConfig>(&toml_str)
.expect_err("Deserializing an invalid toml_str");
assert!(config_err
.to_string()
.contains("must set either `cookie_path` or `auth`"));
// Missing colon in auth
let toml_str = r#"
auth = 'my_usermy_password'
addr = '127.0.0.1:8332'
"#
.trim_start()
.replace(" ", "");
let config_err = toml::from_str::<BitcoindConfig>(&toml_str)
.expect_err("Deserializing an invalid toml_str");
assert!(config_err
.to_string()
.contains("`auth` must be 'user:password'"));
}
#[test] #[test]
fn config_directory() { fn config_directory() {
let filepath = config_file_path().expect("Getting config file path"); let filepath = config_file_path().expect("Getting config file path");

View File

@ -433,7 +433,7 @@ impl DaemonHandle {
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use crate::{
config::{BitcoinConfig, BitcoindConfig}, config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth},
descriptors::LianaDescriptor, descriptors::LianaDescriptor,
testutils::*, testutils::*,
}; };
@ -659,7 +659,7 @@ mod tests {
}; };
let bitcoind_config = BitcoindConfig { let bitcoind_config = BitcoindConfig {
addr, addr,
cookie_path: cookie, rpc_auth: BitcoindRpcAuth::CookieFile(cookie),
}; };
// Create a dummy config with this bitcoind // Create a dummy config with this bitcoind