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#[derive(Serialize, Deserialize, Clone, Parser)]
52#[command(author, version, about, long_about = None)]
53pub struct CliArgs {
54 #[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 #[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 if let Some(x) = value.host {
252 cfg.hopr.host = x
253 };
254
255 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 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 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 if let Some(x) = value.probe_recheck_threshold {
304 cfg.hopr.network.probe_recheck_threshold = std::time::Duration::from_secs(x)
305 };
306
307 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 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 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}