b6ae379afb487452996b311e17b52b8e49e5f1dc
[ldk-sample] / src / args.rs
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 at least 2 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_env_vars()
59                         .or(get_rpc_auth_from_env_file(None))
60                         .or(get_rpc_auth_from_cookie(None, Some(network), None))
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("testnet3"),
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("testnet3").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 }