1use std::{
2 collections::HashSet,
3 net::{IpAddr, SocketAddr},
4 str::FromStr,
5 time::Duration,
6};
7
8use hopr_lib::{
9 Address,
10 config::{HoprLibConfig, HostConfig, HostType},
11};
12use hoprd_api::config::{Api, Auth};
13use proc_macro_regex::regex;
14use serde::{Deserialize, Serialize};
15use serde_with::serde_as;
16use tracing::debug;
17use validator::{Validate, ValidationError};
18
19use crate::errors::HoprdError;
20
21pub const DEFAULT_HOST: &str = "0.0.0.0";
22pub const DEFAULT_PORT: u16 = 9091;
23
24fn validate_file_path(_s: &str) -> Result<(), ValidationError> {
30 Ok(())
31
32 }
40
41fn validate_password(s: &str) -> Result<(), ValidationError> {
42 if !s.is_empty() {
43 Ok(())
44 } else {
45 Err(ValidationError::new("No password could be found"))
46 }
47}
48
49regex!(is_private_key "^(0[xX])?[a-fA-F0-9]{128}$");
50
51pub(crate) fn validate_private_key(s: &str) -> Result<(), ValidationError> {
52 if is_private_key(s) {
53 Ok(())
54 } else {
55 Err(ValidationError::new("No valid private key could be found"))
56 }
57}
58
59fn validate_optional_private_key(s: &str) -> Result<(), ValidationError> {
60 validate_private_key(s)
61}
62
63#[derive(Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
64#[serde(deny_unknown_fields)]
65pub struct Identity {
66 #[validate(custom(function = "validate_file_path"))]
67 #[serde(default)]
68 pub file: String,
69 #[validate(custom(function = "validate_password"))]
70 #[serde(default)]
71 pub password: String,
72 #[validate(custom(function = "validate_optional_private_key"))]
73 #[serde(default)]
74 pub private_key: Option<String>,
75}
76
77impl std::fmt::Debug for Identity {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 let obfuscated: String = "<REDACTED>".into();
80
81 f.debug_struct("Identity")
82 .field("file", &self.file)
83 .field("password", &obfuscated)
84 .field("private_key", &obfuscated)
85 .finish()
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize, Validate)]
90#[serde(deny_unknown_fields)]
91pub struct Db {
92 #[serde(default)]
94 pub data: String,
95 #[serde(default = "just_true")]
96 #[default = true]
97 pub initialize: bool,
98 #[serde(default)]
99 pub force_initialize: bool,
100}
101
102#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq, smart_default::SmartDefault)]
110#[serde(deny_unknown_fields)]
111pub struct HoprdConfig {
112 #[validate(nested)]
114 #[serde(default)]
115 pub hopr: HoprLibConfig,
116 #[validate(nested)]
118 #[serde(default)]
119 pub identity: Identity,
120 #[validate(nested)]
122 #[serde(default)]
123 pub db: Db,
124 #[validate(nested)]
126 #[serde(default)]
127 pub api: Api,
128 #[validate(nested)]
130 #[serde(default)]
131 pub session_ip_forwarding: SessionIpForwardingConfig,
132 #[validate(url)]
134 pub provider: Option<String>,
135 #[validate(nested)]
140 #[serde(default = "hopr_strategy::hopr_default_strategies")]
141 #[default(hopr_strategy::hopr_default_strategies())]
142 pub strategy: hopr_strategy::StrategyConfig,
143}
144
145impl From<HoprdConfig> for HoprLibConfig {
146 fn from(val: HoprdConfig) -> HoprLibConfig {
147 val.hopr
148 }
149}
150
151impl HoprdConfig {
152 pub fn from_cli_args(cli_args: crate::cli::CliArgs, skip_validation: bool) -> crate::errors::Result<HoprdConfig> {
153 let mut cfg: HoprdConfig = if let Some(cfg_path) = cli_args.configuration_file_path {
154 debug!(cfg_path, "fetching configuration from file");
155 let yaml_configuration = std::fs::read_to_string(cfg_path.as_str())
156 .map_err(|e| crate::errors::HoprdError::ConfigError(e.to_string()))?;
157 serde_yaml::from_str(&yaml_configuration)
158 .map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))?
159 } else {
160 debug!("loading default configuration");
161 HoprdConfig::default()
162 };
163
164 if let Some(x) = cli_args.host {
166 cfg.hopr.host = x
167 };
168
169 if cli_args.test_announce_local_addresses > 0 {
171 cfg.hopr.transport.announce_local_addresses = true;
172 }
173 if cli_args.test_prefer_local_addresses > 0 {
174 cfg.hopr.transport.prefer_local_addresses = true;
175 }
176
177 if let Some(host) = cli_args.default_session_listen_host {
178 cfg.session_ip_forwarding.default_entry_listen_host = match host.address {
179 HostType::IPv4(addr) => IpAddr::from_str(&addr)
180 .map(|ip| std::net::SocketAddr::new(ip, host.port))
181 .map_err(|_| HoprdError::ConfigError("invalid default session listen IP address".into())),
182 HostType::Domain(_) => Err(HoprdError::ConfigError("default session listen must be an IP".into())),
183 }?;
184 }
185
186 if let Some(data) = cli_args.data {
188 cfg.db.data = data
189 }
190 if cli_args.init > 0 {
191 cfg.db.initialize = true;
192 }
193 if cli_args.force_init > 0 {
194 cfg.db.force_initialize = true;
195 }
196
197 if cli_args.api > 0 {
199 cfg.api.enable = true;
200 }
201 if cli_args.disable_api_authentication > 0 && cfg.api.auth != Auth::None {
202 cfg.api.auth = Auth::None;
203 };
204 if let Some(x) = cli_args.api_token {
205 cfg.api.auth = Auth::Token(x);
206 };
207 if let Some(x) = cli_args.api_host {
208 cfg.api.host =
209 HostConfig::from_str(format!("{}:{}", x.as_str(), hoprd_api::config::DEFAULT_API_PORT).as_str())
210 .map_err(crate::errors::HoprdError::ValidationError)?;
211 }
212 if let Some(x) = cli_args.api_port {
213 cfg.api.host.port = x
214 };
215
216 if let Some(x) = cli_args.probe_recheck_threshold {
218 cfg.hopr.probe.recheck_threshold = std::time::Duration::from_secs(x)
219 };
220
221 if let Some(x) = cli_args.network_quality_threshold {
223 cfg.hopr.network_options.quality_offline_threshold = x
224 };
225
226 if let Some(identity) = cli_args.identity {
228 cfg.identity.file = identity;
229 }
230 if let Some(x) = cli_args.password {
231 cfg.identity.password = x
232 };
233 if let Some(x) = cli_args.private_key {
234 cfg.identity.private_key = Some(x)
235 };
236
237 if cli_args.announce > 0 {
239 cfg.hopr.publish = true;
240 }
241
242 if let Some(x) = cli_args.provider {
243 cfg.provider = Some(x);
244 }
245
246 if let Some(x) = cli_args.safe_address {
247 cfg.hopr.safe_module.safe_address =
248 Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
249 };
250 if let Some(x) = cli_args.module_address {
251 cfg.hopr.safe_module.module_address =
252 Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
253 };
254
255 let home_symbol = '~';
257 if cfg.db.data.starts_with(home_symbol) {
258 cfg.db.data = home::home_dir()
259 .map(|h| h.as_path().display().to_string())
260 .expect("home dir for a user must be specified")
261 + &cfg.db.data[1..];
262 }
263 if cfg.identity.file.starts_with(home_symbol) {
264 cfg.identity.file = home::home_dir()
265 .map(|h| h.as_path().display().to_string())
266 .expect("home dir for a user must be specified")
267 + &cfg.identity.file[1..];
268 }
269
270 if skip_validation {
271 return Ok(cfg);
272 }
273
274 match cfg.validate() {
275 Ok(_) => Ok(cfg),
276 Err(e) => Err(crate::errors::HoprdError::ValidationError(e.to_string())),
277 }
278 }
279
280 pub fn as_redacted(&self) -> Self {
281 let mut ret = self.clone();
282 match ret.api.auth {
284 Auth::None => {}
285 Auth::Token(_) => ret.api.auth = Auth::Token("<REDACTED>".to_owned()),
286 }
287
288 if ret.identity.private_key.is_some() {
289 ret.identity.private_key = Some("<REDACTED>".to_owned());
290 }
291
292 "<REDACTED>".clone_into(&mut ret.identity.password);
293
294 ret
295 }
296
297 pub fn as_redacted_string(&self) -> crate::errors::Result<String> {
298 let redacted_cfg = self.as_redacted();
299 serde_json::to_string(&redacted_cfg).map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))
300 }
301}
302
303fn default_target_retry_delay() -> Duration {
304 Duration::from_secs(2)
305}
306
307fn default_entry_listen_host() -> SocketAddr {
308 "127.0.0.1:0".parse().unwrap()
309}
310
311fn default_max_tcp_target_retries() -> u32 {
312 10
313}
314
315fn just_true() -> bool {
316 true
317}
318
319#[serde_as]
322#[derive(
323 Clone, Debug, Eq, PartialEq, smart_default::SmartDefault, serde::Deserialize, serde::Serialize, validator::Validate,
324)]
325pub struct SessionIpForwardingConfig {
326 #[serde(default = "just_true")]
331 #[default(true)]
332 pub use_target_allow_list: bool,
333
334 #[serde(default)]
341 #[serde_as(as = "HashSet<serde_with::DisplayFromStr>")]
342 pub target_allow_list: HashSet<SocketAddr>,
343
344 #[serde(default = "default_target_retry_delay")]
348 #[default(default_target_retry_delay())]
349 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
350 pub tcp_target_retry_delay: Duration,
351
352 #[serde(default = "default_max_tcp_target_retries")]
356 #[default(default_max_tcp_target_retries())]
357 #[validate(range(min = 1))]
358 pub max_tcp_target_retries: u32,
359
360 #[serde(default = "default_entry_listen_host")]
363 #[default(default_entry_listen_host())]
364 #[serde_as(as = "serde_with::DisplayFromStr")]
365 pub default_entry_listen_host: SocketAddr,
366}
367
368#[cfg(test)]
369mod tests {
370 use std::io::{Read, Write};
371
372 use anyhow::Context;
373 use clap::{Args, Command, FromArgMatches};
374 use tempfile::NamedTempFile;
375
376 use super::*;
377
378 pub fn example_cfg() -> anyhow::Result<HoprdConfig> {
379 let safe_module = hopr_lib::config::SafeModule {
380 safe_transaction_service_provider: "https:://provider.com/".to_owned(),
381 safe_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
382 module_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
383 };
384
385 let identity = Identity {
386 file: "path/to/identity.file".to_string(),
387 password: "change_me".to_owned(),
388 private_key: None,
389 };
390
391 let host = HostConfig {
392 address: HostType::IPv4("1.2.3.4".into()),
393 port: 9091,
394 };
395
396 Ok(HoprdConfig {
397 hopr: HoprLibConfig {
398 host,
399 safe_module,
400 ..HoprLibConfig::default()
401 },
402 db: Db {
403 data: "/app/db".to_owned(),
404 ..Default::default()
405 },
406 identity,
407 ..HoprdConfig::default()
408 })
409 }
410
411 #[test]
412 fn test_config_should_be_serializable_into_string() -> Result<(), Box<dyn std::error::Error>> {
413 let cfg = example_cfg()?;
414
415 let from_yaml: HoprdConfig = serde_yaml::from_str(include_str!("../example_cfg.yaml"))?;
416
417 assert_eq!(cfg, from_yaml);
418
419 Ok(())
420 }
421
422 #[test]
423 fn test_config_should_be_deserializable_from_a_string_in_a_file() -> Result<(), Box<dyn std::error::Error>> {
424 let mut config_file = NamedTempFile::new()?;
425 let mut prepared_config_file = config_file.reopen()?;
426
427 let cfg = example_cfg()?;
428 let yaml = serde_yaml::to_string(&cfg)?;
429 config_file.write_all(yaml.as_bytes())?;
430
431 let mut buf = String::new();
432 prepared_config_file.read_to_string(&mut buf)?;
433 let deserialized_cfg: HoprdConfig = serde_yaml::from_str(&buf)?;
434
435 assert_eq!(deserialized_cfg, cfg);
436
437 Ok(())
438 }
439
440 #[test]
444 #[ignore]
445 fn test_config_is_extractable_from_the_cli_arguments() -> anyhow::Result<()> {
446 let pwnd = "rpc://pawned!";
447
448 let mut config_file = NamedTempFile::new()?;
449
450 let mut cfg = example_cfg()?;
451 cfg.provider = Some(pwnd.to_owned());
452
453 let yaml = serde_yaml::to_string(&cfg)?;
454 config_file.write_all(yaml.as_bytes())?;
455 let cfg_file_path = config_file
456 .path()
457 .to_str()
458 .context("file path should have a string representation")?
459 .to_string();
460
461 let cli_args = vec!["hoprd", "--configurationFilePath", cfg_file_path.as_str()];
462
463 let mut cmd = Command::new("hoprd").version("0.0.0");
464 cmd = crate::cli::CliArgs::augment_args(cmd);
465 let derived_matches = cmd.try_get_matches_from(cli_args)?;
466 let args = crate::cli::CliArgs::from_arg_matches(&derived_matches)?;
467
468 let cfg = HoprdConfig::from_cli_args(args, true);
470
471 assert!(cfg.is_ok());
472
473 let cfg = cfg?;
474
475 assert_eq!(cfg.provider, Some(pwnd.to_owned()));
476
477 Ok(())
478 }
479}