1use std::{collections::HashSet, net::SocketAddr, time::Duration};
2
3use hopr_lib::{
4 HoprBalance, HoprProtocolConfig, SafeModule, WinningProbability,
5 config::{
6 HoprLibConfig, HoprPacketPipelineConfig, HostConfig, HostType, ProbeConfig, SessionGlobalConfig,
7 TransportConfig,
8 },
9 exports::transport::config::HoprCodecConfig,
10};
11use hoprd_api::config::{Api, Auth};
12use proc_macro_regex::regex;
13use serde::{Deserialize, Serialize};
14use serde_with::serde_as;
15use validator::{Validate, ValidationError, ValidationErrors};
16
17pub const DEFAULT_HOST: &str = "0.0.0.0";
18pub const DEFAULT_PORT: u16 = 9091;
19
20fn validate_file_path(_s: &str) -> Result<(), ValidationError> {
26 Ok(())
27
28 }
36
37fn validate_password(s: &str) -> Result<(), ValidationError> {
38 if !s.is_empty() {
39 Ok(())
40 } else {
41 Err(ValidationError::new("No password could be found"))
42 }
43}
44
45regex!(is_private_key "^(0[xX])?[a-fA-F0-9]{128}$");
46
47pub(crate) fn validate_private_key(s: &str) -> Result<(), ValidationError> {
48 if is_private_key(s) {
49 Ok(())
50 } else {
51 Err(ValidationError::new("No valid private key could be found"))
52 }
53}
54
55fn validate_optional_private_key(s: &str) -> Result<(), ValidationError> {
56 validate_private_key(s)
57}
58
59#[derive(Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
60#[serde(deny_unknown_fields)]
61pub struct Identity {
62 #[validate(custom(function = "validate_file_path"))]
63 #[serde(default)]
64 pub file: String,
65 #[validate(custom(function = "validate_password"))]
66 #[serde(default)]
67 pub password: String,
68 #[validate(custom(function = "validate_optional_private_key"))]
69 #[serde(default)]
70 pub private_key: Option<String>,
71}
72
73impl std::fmt::Debug for Identity {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 let obfuscated: String = "<REDACTED>".into();
76
77 f.debug_struct("Identity")
78 .field("file", &self.file)
79 .field("password", &obfuscated)
80 .field("private_key", &obfuscated)
81 .finish()
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize, Validate)]
86#[serde(deny_unknown_fields)]
87pub struct Db {
88 #[serde(default)]
90 pub data: String,
91 #[serde(default = "just_true")]
93 #[default = true]
94 pub initialize: bool,
95 #[serde(default)]
97 pub force_initialize: bool,
98}
99
100fn default_session_idle_timeout() -> Duration {
101 HoprLibConfig::default().protocol.session.idle_timeout
102}
103
104fn default_max_sessions() -> usize {
105 HoprLibConfig::default().protocol.session.maximum_sessions as usize
106}
107
108fn default_session_establish_max_retries() -> usize {
109 HoprLibConfig::default().protocol.session.establish_max_retries as usize
110}
111
112fn default_probe_recheck_threshold() -> Duration {
113 HoprLibConfig::default().protocol.probe.recheck_threshold
114}
115
116fn default_probe_interval() -> Duration {
117 HoprLibConfig::default().protocol.probe.interval
118}
119
120fn default_outgoing_ticket_winning_prob() -> Option<f64> {
121 HoprLibConfig::default()
122 .protocol
123 .packet
124 .codec
125 .outgoing_win_prob
126 .map(|p| p.as_f64())
127}
128
129#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct UserHoprNetworkConfig {
133 #[default(default_session_idle_timeout())]
135 #[serde(default = "default_session_idle_timeout", with = "humantime_serde")]
136 pub session_idle_timeout: Duration,
137 #[default(default_max_sessions())]
139 #[serde(default = "default_max_sessions")]
140 pub maximum_sessions: usize,
141 #[default(default_session_establish_max_retries())]
143 #[serde(default = "default_session_establish_max_retries")]
144 pub session_establish_max_retries: usize,
145 #[default(default_probe_recheck_threshold())]
147 #[serde(default = "default_probe_recheck_threshold", with = "humantime_serde")]
148 pub probe_recheck_threshold: Duration,
149 #[default(default_probe_interval())]
151 #[serde(default = "default_probe_interval", with = "humantime_serde")]
152 pub probe_interval: Duration,
153 #[serde(default)]
155 pub announce_local_addresses: bool,
156 #[serde(default)]
158 pub prefer_local_addresses: bool,
159 #[default(default_outgoing_ticket_winning_prob())]
161 #[serde(default = "default_outgoing_ticket_winning_prob")]
162 pub outgoing_ticket_winning_prob: Option<f64>,
163 #[serde(default)]
168 pub min_incoming_ticket_price: Option<HoprBalance>,
169}
170
171#[derive(Debug, Clone, PartialEq, smart_default::SmartDefault, Serialize, Deserialize)]
173#[serde(deny_unknown_fields)]
174pub struct UserHoprLibConfig {
175 #[default(just_true())]
177 #[serde(default = "just_true")]
178 pub announce: bool,
179 #[default(default_host())]
181 #[serde(default = "default_host")]
182 pub host: HostConfig,
183 #[serde(default)]
185 pub safe_module: SafeModule,
186 #[serde(default)]
188 pub network: UserHoprNetworkConfig,
189}
190
191#[inline]
194fn default_host() -> HostConfig {
195 HostConfig {
196 address: HostType::IPv4(hopr_lib::config::DEFAULT_HOST.to_owned()),
197 port: hopr_lib::config::DEFAULT_PORT,
198 }
199}
200
201impl From<UserHoprLibConfig> for HoprLibConfig {
202 fn from(value: UserHoprLibConfig) -> Self {
203 HoprLibConfig {
204 host: value.host,
205 publish: value.announce,
206 safe_module: value.safe_module,
207 protocol: HoprProtocolConfig {
208 transport: TransportConfig {
209 announce_local_addresses: value.network.announce_local_addresses,
210 prefer_local_addresses: value.network.prefer_local_addresses,
211 },
212 packet: HoprPacketPipelineConfig {
213 codec: HoprCodecConfig {
214 outgoing_win_prob: value
215 .network
216 .outgoing_ticket_winning_prob
217 .and_then(|v| WinningProbability::try_from_f64(v).ok()),
218 min_incoming_ticket_price: value.network.min_incoming_ticket_price,
219 ..Default::default()
220 },
221 ..Default::default()
222 },
223 probe: ProbeConfig {
224 interval: value.network.probe_interval,
225 recheck_threshold: value.network.probe_recheck_threshold,
226 ..Default::default()
227 },
228 session: SessionGlobalConfig {
229 idle_timeout: value.network.session_idle_timeout,
230 maximum_sessions: value.network.maximum_sessions as u32,
231 establish_max_retries: value.network.session_establish_max_retries as u32,
232 ..Default::default()
233 },
234 },
235 }
236 }
237}
238
239impl Validate for UserHoprLibConfig {
240 fn validate(&self) -> Result<(), ValidationErrors> {
241 HoprLibConfig::from(self.clone()).validate()
242 }
243}
244
245#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq, smart_default::SmartDefault)]
253#[serde(deny_unknown_fields)]
254pub struct HoprdConfig {
255 #[validate(nested)]
257 #[serde(default)]
258 pub hopr: UserHoprLibConfig,
259 #[validate(nested)]
261 #[serde(default)]
262 pub identity: Identity,
263 #[validate(nested)]
265 #[serde(default)]
266 pub db: Db,
267 #[validate(nested)]
269 #[serde(default)]
270 pub api: Api,
271 #[validate(nested)]
273 #[serde(default)]
274 pub session_ip_forwarding: SessionIpForwardingConfig,
275 #[validate(url)]
277 pub blokli_url: Option<String>,
278 #[validate(nested)]
283 #[serde(default = "hopr_strategy::hopr_default_strategies")]
284 #[default(hopr_strategy::hopr_default_strategies())]
285 pub strategy: hopr_strategy::StrategyConfig,
286}
287
288impl HoprdConfig {
289 pub fn as_redacted(&self) -> Self {
290 let mut ret = self.clone();
291 match ret.api.auth {
293 Auth::None => {}
294 Auth::Token(_) => ret.api.auth = Auth::Token("<REDACTED>".to_owned()),
295 }
296
297 if ret.identity.private_key.is_some() {
298 ret.identity.private_key = Some("<REDACTED>".to_owned());
299 }
300
301 "<REDACTED>".clone_into(&mut ret.identity.password);
302
303 ret
304 }
305
306 pub fn as_redacted_string(&self) -> crate::errors::Result<String> {
307 let redacted_cfg = self.as_redacted();
308 serde_json::to_string(&redacted_cfg).map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))
309 }
310}
311
312fn default_target_retry_delay() -> Duration {
313 Duration::from_secs(2)
314}
315
316fn default_entry_listen_host() -> SocketAddr {
317 "127.0.0.1:0".parse().unwrap()
318}
319
320fn default_max_tcp_target_retries() -> u32 {
321 10
322}
323
324fn just_true() -> bool {
325 true
326}
327
328#[serde_as]
331#[derive(
332 Clone, Debug, Eq, PartialEq, smart_default::SmartDefault, serde::Deserialize, serde::Serialize, validator::Validate,
333)]
334pub struct SessionIpForwardingConfig {
335 #[serde(default = "just_true")]
340 #[default(true)]
341 pub use_target_allow_list: bool,
342
343 #[serde(default)]
350 #[serde_as(as = "HashSet<serde_with::DisplayFromStr>")]
351 pub target_allow_list: HashSet<SocketAddr>,
352
353 #[serde(default = "default_target_retry_delay")]
357 #[default(default_target_retry_delay())]
358 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
359 pub tcp_target_retry_delay: Duration,
360
361 #[serde(default = "default_max_tcp_target_retries")]
365 #[default(default_max_tcp_target_retries())]
366 #[validate(range(min = 1))]
367 pub max_tcp_target_retries: u32,
368
369 #[serde(default = "default_entry_listen_host")]
372 #[default(default_entry_listen_host())]
373 #[serde_as(as = "serde_with::DisplayFromStr")]
374 pub default_entry_listen_host: SocketAddr,
375}
376
377#[cfg(test)]
378mod tests {
379 use std::{
380 io::{Read, Write},
381 str::FromStr,
382 };
383
384 use anyhow::Context;
385 use clap::{Args, Command, FromArgMatches};
386 use hopr_lib::Address;
387 use tempfile::NamedTempFile;
388
389 use super::*;
390
391 pub fn example_cfg() -> anyhow::Result<HoprdConfig> {
392 let safe_module = hopr_lib::config::SafeModule {
393 safe_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
394 module_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
395 };
396
397 let identity = Identity {
398 file: "path/to/identity.file".to_string(),
399 password: "change_me".to_owned(),
400 private_key: None,
401 };
402
403 let host = HostConfig {
404 address: HostType::IPv4("1.2.3.4".into()),
405 port: 9091,
406 };
407
408 Ok(HoprdConfig {
409 hopr: UserHoprLibConfig {
410 host,
411 safe_module,
412 ..Default::default()
413 },
414 db: Db {
415 data: "/app/db".to_owned(),
416 ..Default::default()
417 },
418 identity,
419 ..HoprdConfig::default()
420 })
421 }
422
423 #[test]
424 fn test_config_should_be_serializable_into_string() -> anyhow::Result<()> {
425 let cfg = example_cfg()?;
426
427 let from_yaml: HoprdConfig = serde_saphyr::from_str(include_str!("../example_cfg.yaml"))?;
428 assert_eq!(cfg, from_yaml);
429
430 Ok(())
431 }
432
433 #[test]
434 fn test_config_should_be_deserializable_from_a_string_in_a_file() -> anyhow::Result<()> {
435 let mut config_file = NamedTempFile::new()?;
436 let mut prepared_config_file = config_file.reopen()?;
437
438 let cfg = example_cfg()?;
439 let yaml = serde_saphyr::to_string(&cfg)?;
440 config_file.write_all(yaml.as_bytes())?;
441
442 let mut buf = String::new();
443 prepared_config_file.read_to_string(&mut buf)?;
444 let deserialized_cfg: HoprdConfig = serde_saphyr::from_str(&buf)?;
445
446 assert_eq!(deserialized_cfg, cfg);
447
448 Ok(())
449 }
450
451 #[test]
455 #[ignore]
456 fn test_config_is_extractable_from_the_cli_arguments() -> anyhow::Result<()> {
457 let pwnd = "rpc://pawned!";
458
459 let mut config_file = NamedTempFile::new()?;
460
461 let mut cfg = example_cfg()?;
462 cfg.blokli_url = Some(pwnd.to_owned());
463
464 let yaml = serde_saphyr::to_string(&cfg)?;
465 config_file.write_all(yaml.as_bytes())?;
466 let cfg_file_path = config_file
467 .path()
468 .to_str()
469 .context("file path should have a string representation")?
470 .to_string();
471
472 let cli_args = vec!["hoprd", "--configurationFilePath", cfg_file_path.as_str()];
473
474 let mut cmd = Command::new("hoprd").version("0.0.0");
475 cmd = crate::cli::CliArgs::augment_args(cmd);
476 let derived_matches = cmd.try_get_matches_from(cli_args)?;
477 let args = crate::cli::CliArgs::from_arg_matches(&derived_matches)?;
478
479 let cfg = HoprdConfig::try_from(args)?;
481
482 assert_eq!(cfg.blokli_url, Some(pwnd.to_owned()));
483
484 Ok(())
485 }
486}