1use std::{
2 collections::HashSet,
3 net::{IpAddr, SocketAddr},
4 str::FromStr,
5 time::Duration,
6};
7
8use hopr_lib::{Address, HostConfig, HostType, ProtocolsConfig, config::HoprLibConfig};
9use hopr_platform::file::native::read_to_string;
10use hoprd_api::config::{Api, Auth};
11use proc_macro_regex::regex;
12use serde::{Deserialize, Serialize};
13use serde_with::serde_as;
14use tracing::debug;
15use validator::{Validate, ValidationError};
16
17use crate::errors::HoprdError;
18
19pub const DEFAULT_HOST: &str = "0.0.0.0";
20pub const DEFAULT_PORT: u16 = 9091;
21
22pub const DEFAULT_SAFE_TRANSACTION_SERVICE_PROVIDER: &str = "https://safe-transaction.prod.hoprtech.net/";
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, Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
97#[serde(deny_unknown_fields)]
98pub struct HoprdConfig {
99 #[validate(nested)]
101 #[serde(default)]
102 pub hopr: HoprLibConfig,
103 #[validate(nested)]
105 #[serde(default)]
106 pub identity: Identity,
107 #[validate(nested)]
109 #[serde(default)]
110 pub api: Api,
111 #[validate(nested)]
113 #[serde(default)]
114 pub session_ip_forwarding: SessionIpForwardingConfig,
115}
116
117impl From<HoprdConfig> for HoprLibConfig {
118 fn from(val: HoprdConfig) -> HoprLibConfig {
119 val.hopr
120 }
121}
122
123impl HoprdConfig {
124 pub fn from_cli_args(cli_args: crate::cli::CliArgs, skip_validation: bool) -> crate::errors::Result<HoprdConfig> {
125 let mut cfg: HoprdConfig = if let Some(cfg_path) = cli_args.configuration_file_path {
126 debug!(cfg_path, "fetching configuration from file");
127 let yaml_configuration =
128 read_to_string(cfg_path.as_str()).map_err(|e| crate::errors::HoprdError::ConfigError(e.to_string()))?;
129 serde_yaml::from_str(&yaml_configuration)
130 .map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))?
131 } else {
132 debug!("loading default configuration");
133 HoprdConfig::default()
134 };
135
136 if let Some(x) = cli_args.host {
138 cfg.hopr.host = x
139 };
140
141 if cli_args.test_announce_local_addresses > 0 {
143 cfg.hopr.transport.announce_local_addresses = true;
144 }
145 if cli_args.test_prefer_local_addresses > 0 {
146 cfg.hopr.transport.prefer_local_addresses = true;
147 }
148
149 if let Some(host) = cli_args.default_session_listen_host {
150 cfg.session_ip_forwarding.default_entry_listen_host = match host.address {
151 HostType::IPv4(addr) => IpAddr::from_str(&addr)
152 .map(|ip| std::net::SocketAddr::new(ip, host.port))
153 .map_err(|_| HoprdError::ConfigError("invalid default session listen IP address".into())),
154 HostType::Domain(_) => Err(HoprdError::ConfigError("default session listen must be an IP".into())),
155 }?;
156 }
157
158 if let Some(data) = cli_args.data {
160 cfg.hopr.db.data = data
161 }
162 if cli_args.init > 0 {
163 cfg.hopr.db.initialize = true;
164 }
165 if cli_args.force_init > 0 {
166 cfg.hopr.db.force_initialize = true;
167 }
168
169 if cli_args.api > 0 {
171 cfg.api.enable = true;
172 }
173 if cli_args.disable_api_authentication > 0 && cfg.api.auth != Auth::None {
174 cfg.api.auth = Auth::None;
175 };
176 if let Some(x) = cli_args.api_token {
177 cfg.api.auth = Auth::Token(x);
178 };
179 if let Some(x) = cli_args.api_host {
180 cfg.api.host =
181 HostConfig::from_str(format!("{}:{}", x.as_str(), hoprd_api::config::DEFAULT_API_PORT).as_str())
182 .map_err(crate::errors::HoprdError::ValidationError)?;
183 }
184 if let Some(x) = cli_args.api_port {
185 cfg.api.host.port = x
186 };
187
188 if let Some(x) = cli_args.probe_recheck_threshold {
190 cfg.hopr.probe.recheck_threshold = std::time::Duration::from_secs(x)
191 };
192
193 if let Some(x) = cli_args.network_quality_threshold {
195 cfg.hopr.network_options.quality_offline_threshold = x
196 };
197
198 if let Some(identity) = cli_args.identity {
200 cfg.identity.file = identity;
201 }
202 if let Some(x) = cli_args.password {
203 cfg.identity.password = x
204 };
205 if let Some(x) = cli_args.private_key {
206 cfg.identity.private_key = Some(x)
207 };
208
209 if cli_args.announce > 0 {
211 cfg.hopr.chain.announce = true;
212 }
213 if let Some(network) = cli_args.network {
214 cfg.hopr.chain.network = network;
215 }
216
217 if let Some(protocol_config) = cli_args.protocol_config_path {
218 cfg.hopr.chain.protocols = ProtocolsConfig::from_str(
219 &hopr_platform::file::native::read_to_string(&protocol_config)
220 .map_err(|e| crate::errors::HoprdError::ConfigError(e.to_string()))?,
221 )
222 .map_err(crate::errors::HoprdError::ConfigError)?;
223 }
224
225 if let Some(x) = cli_args.provider {
227 cfg.hopr.chain.provider = Some(x);
228 }
229
230 if let Some(x) = cli_args.max_rpc_requests_per_sec {
231 cfg.hopr.chain.max_rpc_requests_per_sec = Some(x);
232 }
233
234 if let Some(x) = cli_args.max_block_range {
235 for (_, n) in cfg.hopr.chain.protocols.networks.iter_mut() {
237 n.max_block_range = x;
238 }
239 }
240
241 cfg.hopr.chain.fast_sync = cli_args.no_fast_sync == 0;
246 cfg.hopr.chain.keep_logs = cli_args.no_keep_logs == 0;
247
248 if let Some(x) = cli_args.safe_transaction_service_provider {
250 cfg.hopr.safe_module.safe_transaction_service_provider = x
251 };
252 if let Some(x) = cli_args.safe_address {
253 cfg.hopr.safe_module.safe_address =
254 Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
255 };
256 if let Some(x) = cli_args.module_address {
257 cfg.hopr.safe_module.module_address =
258 Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
259 };
260
261 let home_symbol = '~';
263 if cfg.hopr.db.data.starts_with(home_symbol) {
264 cfg.hopr.db.data = home::home_dir()
265 .map(|h| h.as_path().display().to_string())
266 .expect("home dir for a user must be specified")
267 + &cfg.hopr.db.data[1..];
268 }
269 if cfg.identity.file.starts_with(home_symbol) {
270 cfg.identity.file = home::home_dir()
271 .map(|h| h.as_path().display().to_string())
272 .expect("home dir for a user must be specified")
273 + &cfg.identity.file[1..];
274 }
275
276 if skip_validation {
277 Ok(cfg)
278 } else {
279 if !cfg
280 .hopr
281 .chain
282 .protocols
283 .supported_networks(hopr_lib::constants::APP_VERSION_COERCED)
284 .iter()
285 .any(|network| network == &cfg.hopr.chain.network)
286 {
287 return Err(crate::errors::HoprdError::ValidationError(format!(
288 "The specified network '{}' is not listed as supported ({:?})",
289 cfg.hopr.chain.network,
290 cfg.hopr
291 .chain
292 .protocols
293 .supported_networks(hopr_lib::constants::APP_VERSION_COERCED)
294 )));
295 }
296
297 match cfg.validate() {
298 Ok(_) => Ok(cfg),
299 Err(e) => Err(crate::errors::HoprdError::ValidationError(e.to_string())),
300 }
301 }
302 }
303
304 pub fn as_redacted(&self) -> Self {
305 let mut ret = self.clone();
306 match ret.api.auth {
308 Auth::None => {}
309 Auth::Token(_) => ret.api.auth = Auth::Token("<REDACTED>".to_owned()),
310 }
311
312 if ret.identity.private_key.is_some() {
313 ret.identity.private_key = Some("<REDACTED>".to_owned());
314 }
315
316 "<REDACTED>".clone_into(&mut ret.identity.password);
317
318 ret
319 }
320
321 pub fn as_redacted_string(&self) -> crate::errors::Result<String> {
322 let redacted_cfg = self.as_redacted();
323 serde_json::to_string(&redacted_cfg).map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))
324 }
325}
326
327fn default_target_retry_delay() -> Duration {
328 Duration::from_secs(2)
329}
330
331fn default_entry_listen_host() -> SocketAddr {
332 "127.0.0.1:0".parse().unwrap()
333}
334
335fn default_max_tcp_target_retries() -> u32 {
336 10
337}
338
339fn just_true() -> bool {
340 true
341}
342
343#[serde_as]
345#[derive(
346 Clone, Debug, Eq, PartialEq, smart_default::SmartDefault, serde::Deserialize, serde::Serialize, validator::Validate,
347)]
348pub struct SessionIpForwardingConfig {
349 #[serde(default = "just_true")]
354 #[default(true)]
355 pub use_target_allow_list: bool,
356
357 #[serde(default)]
364 #[serde_as(as = "HashSet<serde_with::DisplayFromStr>")]
365 pub target_allow_list: HashSet<SocketAddr>,
366
367 #[serde(default = "default_target_retry_delay")]
371 #[default(default_target_retry_delay())]
372 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
373 pub tcp_target_retry_delay: Duration,
374
375 #[serde(default = "default_max_tcp_target_retries")]
379 #[default(default_max_tcp_target_retries())]
380 #[validate(range(min = 1))]
381 pub max_tcp_target_retries: u32,
382
383 #[serde(default = "default_entry_listen_host")]
386 #[default(default_entry_listen_host())]
387 #[serde_as(as = "serde_with::DisplayFromStr")]
388 pub default_entry_listen_host: SocketAddr,
389}
390
391#[cfg(test)]
392mod tests {
393 use std::io::{Read, Write};
394
395 use anyhow::Context;
396 use clap::{Args, Command, FromArgMatches};
397 use hopr_lib::HostType;
398 use tempfile::NamedTempFile;
399
400 use super::*;
401
402 pub fn example_cfg() -> anyhow::Result<HoprdConfig> {
403 let chain = hopr_lib::config::Chain {
404 protocols: hopr_lib::ProtocolsConfig::from_str(
405 r#"
406 {
407 "networks": {
408 "anvil-localhost": {
409 "chain": "anvil",
410 "environment_type": "local",
411 "version_range": "*",
412 "indexer_start_block_number": 5,
413 "addresses": {
414 "network_registry": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
415 "network_registry_proxy": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
416 "channels": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
417 "token": "0x9A676e781A523b5d0C0e43731313A708CB607508",
418 "module_implementation": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
419 "node_safe_registry": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
420 "ticket_price_oracle": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
421 "winning_probability_oracle": "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
422 "announcements": "0xc5a5C42992dECbae36851359345FE25997F5C42d",
423 "node_stake_v2_factory": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
424 },
425 "confirmations": 2,
426 "tags": [],
427 "tx_polling_interval": 1000,
428 "max_block_range": 200
429 }
430 },
431 "chains": {
432 "anvil": {
433 "description": "Local Ethereum node, akin to Ganache, Hardhat chain",
434 "chain_id": 31337,
435 "live": false,
436 "max_fee_per_gas": "1 gwei",
437 "max_priority_fee_per_gas": "0.2 gwei",
438 "default_provider": "http://127.0.0.1:8545/",
439 "native_token_name": "ETH",
440 "hopr_token_name": "wxHOPR",
441 "block_time": 5000,
442 "max_rpc_requests_per_sec": 100,
443 "tags": [],
444 "etherscan_api_url": null
445 }
446 }
447 }
448 "#,
449 )
450 .map_err(|e| anyhow::anyhow!(e))?,
451 ..hopr_lib::config::Chain::default()
452 };
453
454 let db = hopr_lib::config::Db {
455 data: "/app/db".to_owned(),
456 ..hopr_lib::config::Db::default()
457 };
458
459 let safe_module = hopr_lib::config::SafeModule {
460 safe_transaction_service_provider: "https:://provider.com/".to_owned(),
461 safe_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
462 module_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
463 };
464
465 let identity = Identity {
466 file: "path/to/identity.file".to_string(),
467 password: "change_me".to_owned(),
468 private_key: None,
469 };
470
471 let host = HostConfig {
472 address: HostType::IPv4("1.2.3.4".into()),
473 port: 9091,
474 };
475
476 Ok(HoprdConfig {
477 hopr: HoprLibConfig {
478 host,
479 db,
480 chain,
481 safe_module,
482 ..HoprLibConfig::default()
483 },
484 identity,
485 ..HoprdConfig::default()
486 })
487 }
488
489 #[test]
490 fn test_config_should_be_serializable_into_string() -> Result<(), Box<dyn std::error::Error>> {
491 let cfg = example_cfg()?;
492
493 let from_yaml: HoprdConfig = serde_yaml::from_str(include_str!("../example_cfg.yaml"))?;
494
495 assert_eq!(cfg, from_yaml);
496
497 Ok(())
498 }
499
500 #[test]
501 fn test_config_should_be_deserializable_from_a_string_in_a_file() -> Result<(), Box<dyn std::error::Error>> {
502 let mut config_file = NamedTempFile::new()?;
503 let mut prepared_config_file = config_file.reopen()?;
504
505 let cfg = example_cfg()?;
506 let yaml = serde_yaml::to_string(&cfg)?;
507 config_file.write_all(yaml.as_bytes())?;
508
509 let mut buf = String::new();
510 prepared_config_file.read_to_string(&mut buf)?;
511 let deserialized_cfg: HoprdConfig = serde_yaml::from_str(&buf)?;
512
513 assert_eq!(deserialized_cfg, cfg);
514
515 Ok(())
516 }
517
518 #[test]
522 #[ignore]
523 fn test_config_is_extractable_from_the_cli_arguments() -> anyhow::Result<()> {
524 let pwnd = "rpc://pawned!";
525
526 let mut config_file = NamedTempFile::new()?;
527
528 let mut cfg = example_cfg()?;
529 cfg.hopr.chain.provider = Some(pwnd.to_owned());
530
531 let yaml = serde_yaml::to_string(&cfg)?;
532 config_file.write_all(yaml.as_bytes())?;
533 let cfg_file_path = config_file
534 .path()
535 .to_str()
536 .context("file path should have a string representation")?
537 .to_string();
538
539 let cli_args = vec!["hoprd", "--configurationFilePath", cfg_file_path.as_str()];
540
541 let mut cmd = Command::new("hoprd").version("0.0.0");
542 cmd = crate::cli::CliArgs::augment_args(cmd);
543 let derived_matches = cmd.try_get_matches_from(cli_args)?;
544 let args = crate::cli::CliArgs::from_arg_matches(&derived_matches)?;
545
546 let cfg = HoprdConfig::from_cli_args(args, true);
548
549 assert!(cfg.is_ok());
550
551 let cfg = cfg?;
552
553 assert_eq!(cfg.hopr.chain.provider, Some(pwnd.to_owned()));
554
555 Ok(())
556 }
557}