Skip to content

Commit e352827

Browse files
committedJan 4, 2023
Parse .cookie, .env, or environment variables for RPC auth details
1 parent 0c2522a commit e352827

File tree

8 files changed

+383
-103
lines changed

8 files changed

+383
-103
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ Cargo.lock
99
# These are backup files generated by rustfmt
1010
**/*.rs.bk
1111
.ldk
12+
13+
# RPC auth
14+
.env

‎src/args.rs

+371
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
use crate::cli::LdkUserInfo;
2+
use bitcoin::network::constants::Network;
3+
use lightning::ln::msgs::NetAddress;
4+
use std::collections::HashMap;
5+
use std::env;
6+
use std::fs;
7+
use std::net::IpAddr;
8+
use std::path::{Path, PathBuf};
9+
use std::str::FromStr;
10+
11+
pub(crate) fn parse_startup_args() -> Result<LdkUserInfo, ()> {
12+
if env::args().len() < 3 {
13+
println!("ldk-tutorial-node requires 3 arguments: `cargo run [<bitcoind-rpc-username>:<bitcoind-rpc-password>@]<bitcoind-rpc-host>:<bitcoind-rpc-port> ldk_storage_directory_path [<ldk-incoming-peer-listening-port>] [bitcoin-network] [announced-node-name announced-listen-addr*]`");
14+
return Err(());
15+
}
16+
let bitcoind_rpc_info = env::args().skip(1).next().unwrap();
17+
let bitcoind_rpc_info_parts: Vec<&str> = bitcoind_rpc_info.rsplitn(2, "@").collect();
18+
19+
// Parse rpc auth after getting network for default .cookie location
20+
let bitcoind_rpc_path: Vec<&str> = bitcoind_rpc_info_parts[0].split(":").collect();
21+
if bitcoind_rpc_path.len() != 2 {
22+
println!("ERROR: bad bitcoind RPC path provided");
23+
return Err(());
24+
}
25+
let bitcoind_rpc_host = bitcoind_rpc_path[0].to_string();
26+
let bitcoind_rpc_port = bitcoind_rpc_path[1].parse::<u16>().unwrap();
27+
28+
let ldk_storage_dir_path = env::args().skip(2).next().unwrap();
29+
30+
let mut ldk_peer_port_set = true;
31+
let ldk_peer_listening_port: u16 = match env::args().skip(3).next().map(|p| p.parse()) {
32+
Some(Ok(p)) => p,
33+
Some(Err(_)) => {
34+
ldk_peer_port_set = false;
35+
9735
36+
}
37+
None => {
38+
ldk_peer_port_set = false;
39+
9735
40+
}
41+
};
42+
43+
let mut arg_idx = match ldk_peer_port_set {
44+
true => 4,
45+
false => 3,
46+
};
47+
let network: Network = match env::args().skip(arg_idx).next().as_ref().map(String::as_str) {
48+
Some("testnet") => Network::Testnet,
49+
Some("regtest") => Network::Regtest,
50+
Some("signet") => Network::Signet,
51+
Some(net) => {
52+
panic!("Unsupported network provided. Options are: `regtest`, `testnet`, and `signet`. Got {}", net);
53+
}
54+
None => Network::Testnet,
55+
};
56+
57+
let (bitcoind_rpc_username, bitcoind_rpc_password) = if bitcoind_rpc_info_parts.len() == 1 {
58+
get_rpc_auth_from_cookie(None, Some(network), None)
59+
.or(get_rpc_auth_from_env_file(None))
60+
.or(get_rpc_auth_from_env_vars())
61+
.or({
62+
println!("ERROR: unable to get bitcoind RPC username and password");
63+
print_rpc_auth_help();
64+
Err(())
65+
})?
66+
} else if bitcoind_rpc_info_parts.len() == 2 {
67+
parse_rpc_auth(bitcoind_rpc_info_parts[1])?
68+
} else {
69+
println!("ERROR: bad bitcoind RPC URL provided");
70+
return Err(());
71+
};
72+
73+
let ldk_announced_node_name = match env::args().skip(arg_idx + 1).next().as_ref() {
74+
Some(s) => {
75+
if s.len() > 32 {
76+
panic!("Node Alias can not be longer than 32 bytes");
77+
}
78+
arg_idx += 1;
79+
let mut bytes = [0; 32];
80+
bytes[..s.len()].copy_from_slice(s.as_bytes());
81+
bytes
82+
}
83+
None => [0; 32],
84+
};
85+
86+
let mut ldk_announced_listen_addr = Vec::new();
87+
loop {
88+
match env::args().skip(arg_idx + 1).next().as_ref() {
89+
Some(s) => match IpAddr::from_str(s) {
90+
Ok(IpAddr::V4(a)) => {
91+
ldk_announced_listen_addr
92+
.push(NetAddress::IPv4 { addr: a.octets(), port: ldk_peer_listening_port });
93+
arg_idx += 1;
94+
}
95+
Ok(IpAddr::V6(a)) => {
96+
ldk_announced_listen_addr
97+
.push(NetAddress::IPv6 { addr: a.octets(), port: ldk_peer_listening_port });
98+
arg_idx += 1;
99+
}
100+
Err(_) => panic!("Failed to parse announced-listen-addr into an IP address"),
101+
},
102+
None => break,
103+
}
104+
}
105+
106+
Ok(LdkUserInfo {
107+
bitcoind_rpc_username,
108+
bitcoind_rpc_password,
109+
bitcoind_rpc_host,
110+
bitcoind_rpc_port,
111+
ldk_storage_dir_path,
112+
ldk_peer_listening_port,
113+
ldk_announced_listen_addr,
114+
ldk_announced_node_name,
115+
network,
116+
})
117+
}
118+
119+
// Default datadir relative to home directory
120+
#[cfg(target_os = "windows")]
121+
const DEFAULT_BITCOIN_DATADIR: &str = "AppData/Roaming/Bitcoin";
122+
#[cfg(target_os = "linux")]
123+
const DEFAULT_BITCOIN_DATADIR: &str = ".bitcoin";
124+
#[cfg(target_os = "macos")]
125+
const DEFAULT_BITCOIN_DATADIR: &str = "Library/Application Support/Bitcoin";
126+
127+
// Environment variable/.env keys
128+
const BITCOIND_RPC_USER_KEY: &str = "RPC_USER";
129+
const BITCOIND_RPC_PASSWORD_KEY: &str = "RPC_PASSWORD";
130+
131+
fn print_rpc_auth_help() {
132+
// Get the default data directory
133+
let home_dir = env::home_dir()
134+
.as_ref()
135+
.map(|ref p| p.to_str())
136+
.flatten()
137+
.unwrap_or("$HOME")
138+
.replace("\\", "/");
139+
let data_dir = format!("{}/{}", home_dir, DEFAULT_BITCOIN_DATADIR);
140+
println!("To provide the bitcoind RPC username and password, you can either:");
141+
println!(
142+
"1. Provide the username and password as the first argument to this program in the format: \
143+
<bitcoind-rpc-username>:<bitcoind-rpc-password>@<bitcoind-rpc-host>:<bitcoind-rpc-port>"
144+
);
145+
println!("2. Provide <bitcoind-rpc-username>:<bitcoind-rpc-password> in a .cookie file in the default \
146+
bitcoind data directory (automatically created by bitcoind on startup): `{}`", data_dir);
147+
println!(
148+
"3. Set the {} and {} environment variables",
149+
BITCOIND_RPC_USER_KEY, BITCOIND_RPC_PASSWORD_KEY
150+
);
151+
println!(
152+
"4. Provide {} and {} fields in a .env file in the current directory",
153+
BITCOIND_RPC_USER_KEY, BITCOIND_RPC_PASSWORD_KEY
154+
);
155+
}
156+
157+
fn parse_rpc_auth(rpc_auth: &str) -> Result<(String, String), ()> {
158+
let rpc_auth_info: Vec<&str> = rpc_auth.split(':').collect();
159+
if rpc_auth_info.len() != 2 {
160+
println!("ERROR: bad bitcoind RPC username/password combo provided");
161+
return Err(());
162+
}
163+
let rpc_username = rpc_auth_info[0].to_string();
164+
let rpc_password = rpc_auth_info[1].to_string();
165+
Ok((rpc_username, rpc_password))
166+
}
167+
168+
fn get_cookie_path(
169+
data_dir: Option<(&str, bool)>, network: Option<Network>, cookie_file_name: Option<&str>,
170+
) -> Result<PathBuf, ()> {
171+
let data_dir_path = match data_dir {
172+
Some((dir, true)) => env::home_dir().ok_or(())?.join(dir),
173+
Some((dir, false)) => PathBuf::from(dir),
174+
None => env::home_dir().ok_or(())?.join(DEFAULT_BITCOIN_DATADIR),
175+
};
176+
177+
let data_dir_path_with_net = match network {
178+
Some(Network::Testnet) => data_dir_path.join("testnet"),
179+
Some(Network::Regtest) => data_dir_path.join("regtest"),
180+
Some(Network::Signet) => data_dir_path.join("signet"),
181+
_ => data_dir_path,
182+
};
183+
184+
let cookie_path = data_dir_path_with_net.join(cookie_file_name.unwrap_or(".cookie"));
185+
186+
Ok(cookie_path)
187+
}
188+
189+
fn get_rpc_auth_from_cookie(
190+
data_dir: Option<(&str, bool)>, network: Option<Network>, cookie_file_name: Option<&str>,
191+
) -> Result<(String, String), ()> {
192+
let cookie_path = get_cookie_path(data_dir, network, cookie_file_name)?;
193+
let cookie_contents = fs::read_to_string(cookie_path).or(Err(()))?;
194+
parse_rpc_auth(&cookie_contents)
195+
}
196+
197+
fn get_rpc_auth_from_env_vars() -> Result<(String, String), ()> {
198+
if let (Ok(username), Ok(password)) =
199+
(env::var(BITCOIND_RPC_USER_KEY), env::var(BITCOIND_RPC_PASSWORD_KEY))
200+
{
201+
Ok((username, password))
202+
} else {
203+
Err(())
204+
}
205+
}
206+
207+
fn get_rpc_auth_from_env_file(env_file_name: Option<&str>) -> Result<(String, String), ()> {
208+
let env_file_map = parse_env_file(env_file_name)?;
209+
if let (Some(username), Some(password)) =
210+
(env_file_map.get(BITCOIND_RPC_USER_KEY), env_file_map.get(BITCOIND_RPC_PASSWORD_KEY))
211+
{
212+
Ok((username.to_string(), password.to_string()))
213+
} else {
214+
Err(())
215+
}
216+
}
217+
218+
fn parse_env_file(env_file_name: Option<&str>) -> Result<HashMap<String, String>, ()> {
219+
// Default .env file name is .env
220+
let env_file_name = match env_file_name {
221+
Some(filename) => filename,
222+
None => ".env",
223+
};
224+
225+
// Read .env file
226+
let env_file_path = Path::new(env_file_name);
227+
let env_file_contents = fs::read_to_string(env_file_path).or(Err(()))?;
228+
229+
// Collect key-value pairs from .env file into a map
230+
let mut env_file_map: HashMap<String, String> = HashMap::new();
231+
for line in env_file_contents.lines() {
232+
let line_parts: Vec<&str> = line.splitn(2, '=').collect();
233+
if line_parts.len() != 2 {
234+
println!("ERROR: bad .env file format");
235+
return Err(());
236+
}
237+
env_file_map.insert(line_parts[0].to_string(), line_parts[1].to_string());
238+
}
239+
240+
Ok(env_file_map)
241+
}
242+
243+
#[cfg(test)]
244+
mod rpc_auth_tests {
245+
use super::*;
246+
247+
const TEST_ENV_FILE: &str = "test_data/test_env_file";
248+
const TEST_ENV_FILE_BAD: &str = "test_data/test_env_file_bad";
249+
const TEST_ABSENT_FILE: &str = "nonexistent_file";
250+
const TEST_DATA_DIR: &str = "test_data";
251+
const TEST_COOKIE: &str = "test_cookie";
252+
const TEST_COOKIE_BAD: &str = "test_cookie_bad";
253+
const EXPECTED_USER: &str = "testuser";
254+
const EXPECTED_PASSWORD: &str = "testpassword";
255+
256+
#[test]
257+
fn test_parse_rpc_auth_success() {
258+
let (username, password) = parse_rpc_auth("testuser:testpassword").unwrap();
259+
assert_eq!(username, EXPECTED_USER);
260+
assert_eq!(password, EXPECTED_PASSWORD);
261+
}
262+
263+
#[test]
264+
fn test_parse_rpc_auth_fail() {
265+
let result = parse_rpc_auth("testuser");
266+
assert!(result.is_err());
267+
}
268+
269+
#[test]
270+
fn test_get_cookie_path_success() {
271+
let test_cases = vec![
272+
(
273+
None,
274+
None,
275+
None,
276+
env::home_dir().unwrap().join(DEFAULT_BITCOIN_DATADIR).join(".cookie"),
277+
),
278+
(
279+
Some((TEST_DATA_DIR, true)),
280+
Some(Network::Testnet),
281+
None,
282+
env::home_dir().unwrap().join(TEST_DATA_DIR).join("testnet").join(".cookie"),
283+
),
284+
(
285+
Some((TEST_DATA_DIR, false)),
286+
Some(Network::Regtest),
287+
Some(TEST_COOKIE),
288+
PathBuf::from(TEST_DATA_DIR).join("regtest").join(TEST_COOKIE),
289+
),
290+
(
291+
Some((TEST_DATA_DIR, false)),
292+
Some(Network::Signet),
293+
None,
294+
PathBuf::from(TEST_DATA_DIR).join("signet").join(".cookie"),
295+
),
296+
(
297+
Some((TEST_DATA_DIR, false)),
298+
Some(Network::Bitcoin),
299+
None,
300+
PathBuf::from(TEST_DATA_DIR).join(".cookie"),
301+
),
302+
];
303+
304+
for (data_dir, network, cookie_file, expected_path) in test_cases {
305+
let path = get_cookie_path(data_dir, network, cookie_file).unwrap();
306+
assert_eq!(path, expected_path);
307+
}
308+
}
309+
310+
#[test]
311+
fn test_get_rpc_auth_from_cookie_success() {
312+
let (username, password) = get_rpc_auth_from_cookie(
313+
Some((TEST_DATA_DIR, false)),
314+
Some(Network::Bitcoin),
315+
Some(TEST_COOKIE),
316+
)
317+
.unwrap();
318+
assert_eq!(username, EXPECTED_USER);
319+
assert_eq!(password, EXPECTED_PASSWORD);
320+
}
321+
322+
#[test]
323+
fn test_get_rpc_auth_from_cookie_fail() {
324+
let result = get_rpc_auth_from_cookie(
325+
Some((TEST_DATA_DIR, false)),
326+
Some(Network::Bitcoin),
327+
Some(TEST_COOKIE_BAD),
328+
);
329+
assert!(result.is_err());
330+
}
331+
332+
#[test]
333+
fn test_parse_env_file_success() {
334+
let env_file_map = parse_env_file(Some(TEST_ENV_FILE)).unwrap();
335+
assert_eq!(env_file_map.get(BITCOIND_RPC_USER_KEY).unwrap(), EXPECTED_USER);
336+
assert_eq!(env_file_map.get(BITCOIND_RPC_PASSWORD_KEY).unwrap(), EXPECTED_PASSWORD);
337+
}
338+
339+
#[test]
340+
fn test_parse_env_file_fail() {
341+
let env_file_map = parse_env_file(Some(TEST_ENV_FILE_BAD));
342+
assert!(env_file_map.is_err());
343+
344+
// Make sure the test file doesn't exist
345+
assert!(!Path::new(TEST_ABSENT_FILE).exists());
346+
let env_file_map = parse_env_file(Some(TEST_ABSENT_FILE));
347+
assert!(env_file_map.is_err());
348+
}
349+
350+
#[test]
351+
fn test_get_rpc_auth_from_env_file_success() {
352+
let (username, password) = get_rpc_auth_from_env_file(Some(TEST_ENV_FILE)).unwrap();
353+
assert_eq!(username, EXPECTED_USER);
354+
assert_eq!(password, EXPECTED_PASSWORD);
355+
}
356+
357+
#[test]
358+
fn test_get_rpc_auth_from_env_file_fail() {
359+
let rpc_user_and_password = get_rpc_auth_from_env_file(Some(TEST_ABSENT_FILE));
360+
assert!(rpc_user_and_password.is_err());
361+
}
362+
363+
#[test]
364+
fn test_get_rpc_auth_from_env_vars_success() {
365+
env::set_var(BITCOIND_RPC_USER_KEY, EXPECTED_USER);
366+
env::set_var(BITCOIND_RPC_PASSWORD_KEY, EXPECTED_PASSWORD);
367+
let (username, password) = get_rpc_auth_from_env_vars().unwrap();
368+
assert_eq!(username, EXPECTED_USER);
369+
assert_eq!(password, EXPECTED_PASSWORD);
370+
}
371+
}

‎src/cli.rs

+1-102
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use lightning_invoice::{utils, Currency, Invoice};
2121
use std::env;
2222
use std::io;
2323
use std::io::Write;
24-
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
24+
use std::net::{SocketAddr, ToSocketAddrs};
2525
use std::ops::Deref;
2626
use std::path::Path;
2727
use std::str::FromStr;
@@ -40,107 +40,6 @@ pub(crate) struct LdkUserInfo {
4040
pub(crate) network: Network,
4141
}
4242

43-
pub(crate) fn parse_startup_args() -> Result<LdkUserInfo, ()> {
44-
if env::args().len() < 3 {
45-
println!("ldk-tutorial-node requires 3 arguments: `cargo run <bitcoind-rpc-username>:<bitcoind-rpc-password>@<bitcoind-rpc-host>:<bitcoind-rpc-port> ldk_storage_directory_path [<ldk-incoming-peer-listening-port>] [bitcoin-network] [announced-node-name announced-listen-addr*]`");
46-
return Err(());
47-
}
48-
let bitcoind_rpc_info = env::args().skip(1).next().unwrap();
49-
let bitcoind_rpc_info_parts: Vec<&str> = bitcoind_rpc_info.rsplitn(2, "@").collect();
50-
if bitcoind_rpc_info_parts.len() != 2 {
51-
println!("ERROR: bad bitcoind RPC URL provided");
52-
return Err(());
53-
}
54-
let rpc_user_and_password: Vec<&str> = bitcoind_rpc_info_parts[1].split(":").collect();
55-
if rpc_user_and_password.len() != 2 {
56-
println!("ERROR: bad bitcoind RPC username/password combo provided");
57-
return Err(());
58-
}
59-
let bitcoind_rpc_username = rpc_user_and_password[0].to_string();
60-
let bitcoind_rpc_password = rpc_user_and_password[1].to_string();
61-
let bitcoind_rpc_path: Vec<&str> = bitcoind_rpc_info_parts[0].split(":").collect();
62-
if bitcoind_rpc_path.len() != 2 {
63-
println!("ERROR: bad bitcoind RPC path provided");
64-
return Err(());
65-
}
66-
let bitcoind_rpc_host = bitcoind_rpc_path[0].to_string();
67-
let bitcoind_rpc_port = bitcoind_rpc_path[1].parse::<u16>().unwrap();
68-
69-
let ldk_storage_dir_path = env::args().skip(2).next().unwrap();
70-
71-
let mut ldk_peer_port_set = true;
72-
let ldk_peer_listening_port: u16 = match env::args().skip(3).next().map(|p| p.parse()) {
73-
Some(Ok(p)) => p,
74-
Some(Err(_)) => {
75-
ldk_peer_port_set = false;
76-
9735
77-
}
78-
None => {
79-
ldk_peer_port_set = false;
80-
9735
81-
}
82-
};
83-
84-
let mut arg_idx = match ldk_peer_port_set {
85-
true => 4,
86-
false => 3,
87-
};
88-
let network: Network = match env::args().skip(arg_idx).next().as_ref().map(String::as_str) {
89-
Some("testnet") => Network::Testnet,
90-
Some("regtest") => Network::Regtest,
91-
Some("signet") => Network::Signet,
92-
Some(net) => {
93-
panic!("Unsupported network provided. Options are: `regtest`, `testnet`, and `signet`. Got {}", net);
94-
}
95-
None => Network::Testnet,
96-
};
97-
98-
let ldk_announced_node_name = match env::args().skip(arg_idx + 1).next().as_ref() {
99-
Some(s) => {
100-
if s.len() > 32 {
101-
panic!("Node Alias can not be longer than 32 bytes");
102-
}
103-
arg_idx += 1;
104-
let mut bytes = [0; 32];
105-
bytes[..s.len()].copy_from_slice(s.as_bytes());
106-
bytes
107-
}
108-
None => [0; 32],
109-
};
110-
111-
let mut ldk_announced_listen_addr = Vec::new();
112-
loop {
113-
match env::args().skip(arg_idx + 1).next().as_ref() {
114-
Some(s) => match IpAddr::from_str(s) {
115-
Ok(IpAddr::V4(a)) => {
116-
ldk_announced_listen_addr
117-
.push(NetAddress::IPv4 { addr: a.octets(), port: ldk_peer_listening_port });
118-
arg_idx += 1;
119-
}
120-
Ok(IpAddr::V6(a)) => {
121-
ldk_announced_listen_addr
122-
.push(NetAddress::IPv6 { addr: a.octets(), port: ldk_peer_listening_port });
123-
arg_idx += 1;
124-
}
125-
Err(_) => panic!("Failed to parse announced-listen-addr into an IP address"),
126-
},
127-
None => break,
128-
}
129-
}
130-
131-
Ok(LdkUserInfo {
132-
bitcoind_rpc_username,
133-
bitcoind_rpc_password,
134-
bitcoind_rpc_host,
135-
bitcoind_rpc_port,
136-
ldk_storage_dir_path,
137-
ldk_peer_listening_port,
138-
ldk_announced_listen_addr,
139-
ldk_announced_node_name,
140-
network,
141-
})
142-
}
143-
14443
struct UserOnionMessageContents {
14544
tlv_type: u64,
14645
data: Vec<u8>,

‎src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod args;
12
pub mod bitcoind_client;
23
mod cli;
34
mod convert;
@@ -358,7 +359,7 @@ async fn handle_ldk_events(
358359
}
359360

360361
async fn start_ldk() {
361-
let args = match cli::parse_startup_args() {
362+
let args = match args::parse_startup_args() {
362363
Ok(user_args) => user_args,
363364
Err(()) => return,
364365
};

‎test_data/test_cookie

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testuser:testpassword

‎test_data/test_cookie_bad

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testuser

‎test_data/test_env_file

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
RPC_USER=testuser
2+
RPC_PASSWORD=testpassword

‎test_data/test_env_file_bad

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
RPC_USER=testuser
2+
RPC_PASSWORD

0 commit comments

Comments
 (0)
Please sign in to comment.