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