hoprd/
cli.rs

1use std::{net::IpAddr, str::FromStr};
2
3use clap::{ArgAction, Parser, builder::ValueParser};
4use hopr_chain_connector::Address;
5use hopr_lib::config::{HostConfig, HostType, looks_like_domain};
6use hoprd_api::config::Auth;
7use serde::{Deserialize, Serialize};
8use tracing::debug;
9
10use crate::{config::HoprdConfig, errors::HoprdError};
11
12pub const DEFAULT_API_HOST: &str = "localhost";
13pub const DEFAULT_API_PORT: u16 = 3001;
14
15pub const MINIMAL_API_TOKEN_LENGTH: usize = 8;
16
17fn parse_host(s: &str) -> Result<HostConfig, String> {
18    let host = s.split_once(':').map_or(s, |(h, _)| h);
19    if !(validator::ValidateIp::validate_ipv4(&host) || looks_like_domain(host)) {
20        return Err(format!(
21            "Given string {s} is not a valid host, should have a format: <ip>:<port> or <domain>(:<port>)"
22        ));
23    }
24
25    HostConfig::from_str(s)
26}
27
28fn parse_api_token(mut s: &str) -> Result<String, String> {
29    if s.len() < MINIMAL_API_TOKEN_LENGTH {
30        return Err(format!(
31            "Length of API token is too short, minimally required {MINIMAL_API_TOKEN_LENGTH} but given {}",
32            s.len()
33        ));
34    }
35
36    match (s.starts_with('\''), s.ends_with('\'')) {
37        (true, true) => {
38            s = s.strip_prefix('\'').ok_or("failed to parse strip prefix part")?;
39            s = s.strip_suffix('\'').ok_or("failed to parse strip suffix part")?;
40
41            Ok(s.into())
42        }
43        (true, false) => Err("Found leading quote but no trailing quote".into()),
44        (false, true) => Err("Found trailing quote but no leading quote".into()),
45        (false, false) => Ok(s.into()),
46    }
47}
48
49/// Takes all CLI arguments whose structure is known at compile-time.
50/// Arguments whose structure, e.g., their default values depend on
51/// file contents, need to be specified using `clap`'s builder API
52#[derive(Serialize, Deserialize, Clone, Parser)]
53#[command(author, version, about, long_about = None)]
54pub struct CliArgs {
55    // Identity details
56    #[arg(
57        long,
58        env = "HOPRD_IDENTITY",
59        help = "The path to the identity file",
60        required = false
61    )]
62    pub identity: Option<String>,
63
64    // Identity details
65    #[arg(
66        long,
67        env = "HOPRD_DATA",
68        help = "Specifies the directory to hold all the data",
69        required = false
70    )]
71    pub data: Option<String>,
72
73    #[arg(
74        long,
75        env = "HOPRD_HOST",
76        help = "Host to listen on for P2P connections",
77        value_parser = ValueParser::new(parse_host),
78    )]
79    pub host: Option<HostConfig>,
80
81    #[arg(
82        long,
83        env = "HOPRD_ANNOUNCE",
84        help = "Announce the node on chain with a public address",
85        action = ArgAction::Count
86    )]
87    pub announce: u8,
88
89    #[arg(
90        long,
91        env = "HOPRD_API",
92        help = format!("Expose the API on {}:{}", DEFAULT_API_HOST, DEFAULT_API_PORT),
93        action = ArgAction::Count
94    )]
95    pub api: u8,
96
97    #[arg(
98        long = "apiHost",
99        value_name = "HOST",
100        help = "Set host IP to which the API server will bind",
101        env = "HOPRD_API_HOST"
102    )]
103    pub api_host: Option<String>,
104
105    #[arg(
106        long = "apiPort",
107        value_parser = clap::value_parser ! (u16),
108        value_name = "PORT",
109        help = "Set port to which the API server will bind",
110        env = "HOPRD_API_PORT"
111    )]
112    pub api_port: Option<u16>,
113
114    #[arg(
115        long = "defaultSessionListenHost",
116        env = "HOPRD_DEFAULT_SESSION_LISTEN_HOST",
117        help = "Default Session listening host for Session IP forwarding",
118        value_parser = ValueParser::new(parse_host),
119    )]
120    pub default_session_listen_host: Option<HostConfig>,
121
122    #[arg(
123        long = "disableApiAuthentication",
124        help = "Completely disables the token authentication for the API, overrides any apiToken if set",
125        env = "HOPRD_DISABLE_API_AUTHENTICATION",
126        hide = true,
127        action = ArgAction::Count
128    )]
129    pub disable_api_authentication: u8,
130
131    #[arg(
132        long = "apiToken",
133        alias = "api-token",
134        help = "A REST API token and for user authentication",
135        value_name = "TOKEN",
136        value_parser = ValueParser::new(parse_api_token),
137        env = "HOPRD_API_TOKEN"
138    )]
139    pub api_token: Option<String>,
140
141    #[arg(
142        long,
143        env = "HOPRD_PASSWORD",
144        help = "A password to encrypt your keys",
145        value_name = "PASSWORD"
146    )]
147    pub password: Option<String>,
148
149    #[arg(
150        long,
151        help = "URL for Blokli provider to be used for the node to connect to blockchain",
152        env = "HOPRD_BLOKLI_URL",
153        value_name = "BLOKLI_URL"
154    )]
155    pub blokli_url: Option<String>,
156
157    #[arg(
158        long,
159        help = "initialize a database if it doesn't already exist",
160        env = "HOPRD_INIT",
161        action = ArgAction::Count
162    )]
163    pub init: u8,
164
165    #[arg(
166        long = "forceInit",
167        help = "initialize a database, even if it already exists",
168        env = "HOPRD_FORCE_INIT",
169        action = ArgAction::Count
170    )]
171    pub force_init: u8,
172
173    #[arg(
174        long = "privateKey",
175        hide = true,
176        help = "A private key to be used for the node",
177        env = "HOPRD_PRIVATE_KEY",
178        value_name = "PRIVATE_KEY"
179    )]
180    pub private_key: Option<String>,
181
182    #[arg(
183        long = "testAnnounceLocalAddresses",
184        env = "HOPRD_TEST_ANNOUNCE_LOCAL_ADDRESSES",
185        help = "For testing local testnets. Announce local addresses",
186        hide = true,
187        action = ArgAction::Count
188    )]
189    pub test_announce_local_addresses: u8,
190
191    #[arg(
192        long = "testPreferLocalAddresses",
193        env = "HOPRD_TEST_PREFER_LOCAL_ADDRESSES",
194        help = "For testing local testnets. Prefer local peers to remote",
195        hide = true,
196        action = ArgAction::Count
197    )]
198    pub test_prefer_local_addresses: u8,
199
200    #[arg(
201        long = "probeRecheckThreshold",
202        help = "Timeframe in seconds after which it is reasonable to recheck the nearest neighbor",
203        value_name = "SECONDS",
204        value_parser = clap::value_parser ! (u64),
205        env = "HOPRD_PROBE_RECHECK_THRESHOLD",
206    )]
207    pub probe_recheck_threshold: Option<u64>,
208
209    #[arg(
210        long = "configurationFilePath",
211        required = false,
212        help = "Path to a file containing the entire HOPRd configuration",
213        value_name = "CONFIG_FILE_PATH",
214        value_parser = clap::value_parser ! (String),
215        env = "HOPRD_CONFIGURATION_FILE_PATH"
216    )]
217    pub configuration_file_path: Option<String>,
218
219    #[arg(
220        long = "safeAddress",
221        value_name = "HOPRD_SAFE_ADDR",
222        help = "Address of Safe that safeguards tokens",
223        env = "HOPRD_SAFE_ADDRESS"
224    )]
225    pub safe_address: Option<String>,
226
227    #[arg(
228        long = "moduleAddress",
229        value_name = "HOPRD_MODULE_ADDR",
230        help = "Address of the node management module",
231        env = "HOPRD_MODULE_ADDRESS"
232    )]
233    pub module_address: Option<String>,
234}
235
236impl TryFrom<CliArgs> for HoprdConfig {
237    type Error = HoprdError;
238
239    fn try_from(value: CliArgs) -> Result<Self, Self::Error> {
240        let mut cfg: HoprdConfig = if let Some(cfg_path) = value.configuration_file_path {
241            debug!(cfg_path, "fetching configuration from file");
242            let yaml_configuration = std::fs::read_to_string(cfg_path.as_str())
243                .map_err(|e| crate::errors::HoprdError::ConfigError(e.to_string()))?;
244            serde_yaml::from_str(&yaml_configuration)
245                .map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))?
246        } else {
247            debug!("loading default configuration");
248            HoprdConfig::default()
249        };
250
251        // host
252        if let Some(x) = value.host {
253            cfg.hopr.host = x
254        };
255
256        // hopr.transport
257        if value.test_announce_local_addresses > 0 {
258            cfg.hopr.network.announce_local_addresses = true;
259        }
260        if value.test_prefer_local_addresses > 0 {
261            cfg.hopr.network.prefer_local_addresses = true;
262        }
263
264        if let Some(host) = value.default_session_listen_host {
265            cfg.session_ip_forwarding.default_entry_listen_host = match host.address {
266                HostType::IPv4(addr) => IpAddr::from_str(&addr)
267                    .map(|ip| std::net::SocketAddr::new(ip, host.port))
268                    .map_err(|_| HoprdError::ConfigError("invalid default session listen IP address".into())),
269                HostType::Domain(_) => Err(HoprdError::ConfigError("default session listen must be an IP".into())),
270            }?;
271        }
272
273        // db
274        if let Some(data) = value.data {
275            cfg.db.data = data
276        }
277        if value.init > 0 {
278            cfg.db.initialize = true;
279        }
280        if value.force_init > 0 {
281            cfg.db.force_initialize = true;
282        }
283
284        // api
285        if value.api > 0 {
286            cfg.api.enable = true;
287        }
288        if value.disable_api_authentication > 0 && cfg.api.auth != Auth::None {
289            cfg.api.auth = Auth::None;
290        };
291        if let Some(x) = value.api_token {
292            cfg.api.auth = Auth::Token(x);
293        };
294        if let Some(x) = value.api_host {
295            cfg.api.host =
296                HostConfig::from_str(format!("{}:{}", x.as_str(), hoprd_api::config::DEFAULT_API_PORT).as_str())
297                    .map_err(crate::errors::HoprdError::ValidationError)?;
298        }
299        if let Some(x) = value.api_port {
300            cfg.api.host.port = x
301        };
302
303        // probe
304        if let Some(x) = value.probe_recheck_threshold {
305            cfg.hopr.network.probe_recheck_threshold = std::time::Duration::from_secs(x)
306        };
307
308        // identity
309        if let Some(identity) = value.identity {
310            cfg.identity.file = identity;
311        }
312        if let Some(x) = value.password {
313            cfg.identity.password = x
314        };
315        if let Some(x) = value.private_key {
316            cfg.identity.private_key = Some(x)
317        };
318
319        // chain
320        if value.announce > 0 {
321            cfg.hopr.announce = true;
322        }
323
324        if let Some(x) = value.blokli_url {
325            cfg.blokli_url = Some(x);
326        }
327
328        if let Some(x) = value.safe_address {
329            cfg.hopr.safe_module.safe_address =
330                Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
331        };
332        if let Some(x) = value.module_address {
333            cfg.hopr.safe_module.module_address =
334                Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
335        };
336
337        // additional updates
338        let home_symbol = '~';
339        if cfg.db.data.starts_with(home_symbol) {
340            cfg.db.data = home::home_dir()
341                .map(|h| h.as_path().display().to_string())
342                .expect("home dir for a user must be specified")
343                + &cfg.db.data[1..];
344        }
345        if cfg.identity.file.starts_with(home_symbol) {
346            cfg.identity.file = home::home_dir()
347                .map(|h| h.as_path().display().to_string())
348                .expect("home dir for a user must be specified")
349                + &cfg.identity.file[1..];
350        }
351
352        Ok(cfg)
353    }
354}