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:
commit
21e064c50e
@ -31,8 +31,10 @@ poll_interval_secs = 30
|
||||
|
||||
# This section is specific to the bitcoind implementation of the Bitcoin backend. This is the only
|
||||
# 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
|
||||
# as where the authentication cookie is located.
|
||||
# In order to be able to connect to bitcoind, it needs to know on what port it is listening and
|
||||
# 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]
|
||||
addr = "127.0.0.1:18332"
|
||||
cookie_path = "/home/wizardsardine/.bitcoin/testnet3/.cookie"
|
||||
# auth = "my_user:my_password"
|
||||
|
||||
@ -228,34 +228,43 @@ impl BitcoinD {
|
||||
config: &config::BitcoindConfig,
|
||||
watchonly_wallet_path: String,
|
||||
) -> 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 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.
|
||||
let dummy_node_client = Client::with_transport(
|
||||
MinreqHttpTransport::builder()
|
||||
builder
|
||||
.clone()
|
||||
.url(&node_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(3))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let sendonly_client = Client::with_transport(
|
||||
MinreqHttpTransport::builder()
|
||||
builder
|
||||
.clone()
|
||||
.url(&watchonly_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(1))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let dummy_wo_client = Client::with_transport(
|
||||
MinreqHttpTransport::builder()
|
||||
builder
|
||||
.clone()
|
||||
.url(&watchonly_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(3))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let dummy_bitcoind = BitcoinD {
|
||||
@ -271,27 +280,26 @@ impl BitcoinD {
|
||||
|
||||
// Now the connection is checked, create the clients with an appropriate timeout.
|
||||
let node_client = Client::with_transport(
|
||||
MinreqHttpTransport::builder()
|
||||
builder
|
||||
.clone()
|
||||
.url(&node_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let sendonly_client = Client::with_transport(
|
||||
MinreqHttpTransport::builder()
|
||||
builder
|
||||
.clone()
|
||||
.url(&watchonly_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(1))
|
||||
.cookie_auth(cookie_string.clone())
|
||||
.build(),
|
||||
);
|
||||
let watchonly_client = Client::with_transport(
|
||||
MinreqHttpTransport::builder()
|
||||
builder
|
||||
.url(&watchonly_url)
|
||||
.map_err(BitcoindError::from)?
|
||||
.timeout(Duration::from_secs(RPC_SOCKET_TIMEOUT))
|
||||
.cookie_auth(cookie_string)
|
||||
.build(),
|
||||
);
|
||||
Ok(BitcoinD {
|
||||
|
||||
152
src/config.rs
152
src/config.rs
@ -35,6 +35,43 @@ pub fn serialize_duration<S: Serializer>(duration: &Duration, s: S) -> Result<S:
|
||||
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 {
|
||||
log::LevelFilter::Info
|
||||
}
|
||||
@ -48,11 +85,23 @@ fn default_daemon() -> bool {
|
||||
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
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct BitcoindConfig {
|
||||
/// Path to bitcoind's cookie file, to authenticate the RPC connection
|
||||
pub cookie_path: PathBuf,
|
||||
/// Authentication credentials for bitcoind's RPC server.
|
||||
#[serde(flatten, deserialize_with = "deserialize_rpc_auth")]
|
||||
pub rpc_auth: BitcoindRpcAuth,
|
||||
/// The IP:port bitcoind's RPC is listening on
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
@ -217,7 +266,9 @@ impl Config {
|
||||
|
||||
#[cfg(test)]
|
||||
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]
|
||||
@ -259,6 +310,26 @@ mod tests {
|
||||
#[cfg(unix)] // On non-UNIX there is no 'daemon' member.
|
||||
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
|
||||
let toml_str = r#"
|
||||
daemon = false
|
||||
@ -298,6 +369,81 @@ mod tests {
|
||||
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]
|
||||
fn config_directory() {
|
||||
let filepath = config_file_path().expect("Getting config file path");
|
||||
|
||||
@ -433,7 +433,7 @@ impl DaemonHandle {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
config::{BitcoinConfig, BitcoindConfig},
|
||||
config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth},
|
||||
descriptors::LianaDescriptor,
|
||||
testutils::*,
|
||||
};
|
||||
@ -659,7 +659,7 @@ mod tests {
|
||||
};
|
||||
let bitcoind_config = BitcoindConfig {
|
||||
addr,
|
||||
cookie_path: cookie,
|
||||
rpc_auth: BitcoindRpcAuth::CookieFile(cookie),
|
||||
};
|
||||
|
||||
// Create a dummy config with this bitcoind
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user