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