Skip to main content

soroban_cli/commands/network/
ls.rs

1use super::locator;
2
3#[derive(thiserror::Error, Debug)]
4pub enum Error {
5    #[error(transparent)]
6    Config(#[from] locator::Error),
7}
8
9#[derive(Debug, clap::Parser, Clone)]
10#[group(skip)]
11pub struct Cmd {
12    #[command(flatten)]
13    pub config_locator: locator::Args,
14    /// Get more info about the networks
15    #[arg(long, short = 'l')]
16    pub long: bool,
17}
18
19impl Cmd {
20    pub fn run(&self) -> Result<(), Error> {
21        let sep = if self.long { "\n\n" } else { "\n" };
22        let res = if self.long { self.ls_l() } else { self.ls() }?.join(sep);
23        println!("{res}");
24        Ok(())
25    }
26
27    pub fn ls(&self) -> Result<Vec<String>, Error> {
28        Ok(self.config_locator.list_networks()?)
29    }
30
31    pub fn ls_l(&self) -> Result<Vec<String>, Error> {
32        Ok(self
33            .config_locator
34            .list_networks_long()?
35            .iter()
36            .map(|(name, network, _)| {
37                let headers = if network.rpc_headers.is_empty() {
38                    " not set".to_string()
39                } else {
40                    let lines: Vec<String> = network
41                        .rpc_headers
42                        .iter()
43                        .map(|(k, _)| format!("  {k}: <concealed>"))
44                        .collect();
45                    format!("\n{}", lines.join("\n"))
46                };
47
48                format!(
49                    "Name: {name}\nRPC url: {rpc_url}\nRPC headers:{headers}\nNetwork passphrase: {passphrase}",
50                    rpc_url = crate::utils::url::redact_url(&network.rpc_url),
51                    passphrase = network.network_passphrase,
52                )
53            })
54            .collect())
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::config::network::Network;
62    use crate::test_utils::{with_cwd_guard, with_env_guard};
63    use serial_test::serial;
64
65    #[test]
66    #[serial]
67    fn ls_l_redacts_rpc_url_password() {
68        let tmp = tempfile::tempdir().unwrap();
69
70        with_env_guard(&["STELLAR_CONFIG_HOME", "XDG_CONFIG_HOME"], || {
71            with_cwd_guard(|| {
72                let global_cfg = tmp.path().join("global");
73                std::fs::create_dir_all(&global_cfg).unwrap();
74                std::env::set_var("STELLAR_CONFIG_HOME", &global_cfg);
75
76                let work = tmp.path().join("work");
77                std::fs::create_dir_all(&work).unwrap();
78                std::env::set_current_dir(&work).unwrap();
79
80                let cmd = Cmd {
81                    config_locator: locator::Args { config_dir: None },
82                    long: true,
83                };
84
85                let network = Network {
86                    rpc_url: "https://alice:supersecret@rpc.example.com/soroban".to_string(),
87                    rpc_headers: Vec::new(),
88                    network_passphrase: "Test SDF Network ; September 2015".to_string(),
89                };
90                cmd.config_locator.write_network("corp", &network).unwrap();
91
92                let rendered = cmd.ls_l().unwrap().join("\n\n");
93
94                assert!(
95                    !rendered.contains("supersecret"),
96                    "password leaked into `network ls -l` output: {rendered}"
97                );
98                assert!(
99                    rendered.contains("alice:redacted"),
100                    "expected `alice:redacted` in `network ls -l` output: {rendered}"
101                );
102                assert!(
103                    rendered.contains("rpc.example.com/soroban"),
104                    "expected host and path preserved: {rendered}"
105                );
106            });
107        });
108    }
109}