Skip to main content

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