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 cfg.hopr.chain.enable_logs_snapshot = cli_args.enable_logs_snapshot != 0;
248
249 if let Some(x) = cli_args.logs_snapshot_url {
250 cfg.hopr.chain.logs_snapshot_url = Some(x);
251 }
252
253 if let Some(x) = cli_args.safe_transaction_service_provider {
255 cfg.hopr.safe_module.safe_transaction_service_provider = x
256 };
257 if let Some(x) = cli_args.safe_address {
258 cfg.hopr.safe_module.safe_address =
259 Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
260 };
261 if let Some(x) = cli_args.module_address {
262 cfg.hopr.safe_module.module_address =
263 Address::from_str(&x).map_err(|e| HoprdError::ValidationError(e.to_string()))?
264 };
265
266 let home_symbol = '~';
268 if cfg.hopr.db.data.starts_with(home_symbol) {
269 cfg.hopr.db.data = home::home_dir()
270 .map(|h| h.as_path().display().to_string())
271 .expect("home dir for a user must be specified")
272 + &cfg.hopr.db.data[1..];
273 }
274 if cfg.identity.file.starts_with(home_symbol) {
275 cfg.identity.file = home::home_dir()
276 .map(|h| h.as_path().display().to_string())
277 .expect("home dir for a user must be specified")
278 + &cfg.identity.file[1..];
279 }
280
281 if skip_validation {
282 Ok(cfg)
283 } else {
284 if !cfg
285 .hopr
286 .chain
287 .protocols
288 .supported_networks(hopr_lib::constants::APP_VERSION_COERCED)
289 .iter()
290 .any(|network| network == &cfg.hopr.chain.network)
291 {
292 return Err(crate::errors::HoprdError::ValidationError(format!(
293 "The specified network '{}' is not listed as supported ({:?})",
294 cfg.hopr.chain.network,
295 cfg.hopr
296 .chain
297 .protocols
298 .supported_networks(hopr_lib::constants::APP_VERSION_COERCED)
299 )));
300 }
301
302 match cfg.validate() {
303 Ok(_) => Ok(cfg),
304 Err(e) => Err(crate::errors::HoprdError::ValidationError(e.to_string())),
305 }
306 }
307 }
308
309 pub fn as_redacted(&self) -> Self {
310 let mut ret = self.clone();
311 match ret.api.auth {
313 Auth::None => {}
314 Auth::Token(_) => ret.api.auth = Auth::Token("<REDACTED>".to_owned()),
315 }
316
317 if ret.identity.private_key.is_some() {
318 ret.identity.private_key = Some("<REDACTED>".to_owned());
319 }
320
321 "<REDACTED>".clone_into(&mut ret.identity.password);
322
323 ret
324 }
325
326 pub fn as_redacted_string(&self) -> crate::errors::Result<String> {
327 let redacted_cfg = self.as_redacted();
328 serde_json::to_string(&redacted_cfg).map_err(|e| crate::errors::HoprdError::SerializationError(e.to_string()))
329 }
330}
331
332fn default_target_retry_delay() -> Duration {
333 Duration::from_secs(2)
334}
335
336fn default_entry_listen_host() -> SocketAddr {
337 "127.0.0.1:0".parse().unwrap()
338}
339
340fn default_max_tcp_target_retries() -> u32 {
341 10
342}
343
344fn just_true() -> bool {
345 true
346}
347
348#[serde_as]
351#[derive(
352 Clone, Debug, Eq, PartialEq, smart_default::SmartDefault, serde::Deserialize, serde::Serialize, validator::Validate,
353)]
354pub struct SessionIpForwardingConfig {
355 #[serde(default = "just_true")]
360 #[default(true)]
361 pub use_target_allow_list: bool,
362
363 #[serde(default)]
370 #[serde_as(as = "HashSet<serde_with::DisplayFromStr>")]
371 pub target_allow_list: HashSet<SocketAddr>,
372
373 #[serde(default = "default_target_retry_delay")]
377 #[default(default_target_retry_delay())]
378 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
379 pub tcp_target_retry_delay: Duration,
380
381 #[serde(default = "default_max_tcp_target_retries")]
385 #[default(default_max_tcp_target_retries())]
386 #[validate(range(min = 1))]
387 pub max_tcp_target_retries: u32,
388
389 #[serde(default = "default_entry_listen_host")]
392 #[default(default_entry_listen_host())]
393 #[serde_as(as = "serde_with::DisplayFromStr")]
394 pub default_entry_listen_host: SocketAddr,
395}
396
397#[cfg(test)]
398mod tests {
399 use std::io::{Read, Write};
400
401 use anyhow::Context;
402 use clap::{Args, Command, FromArgMatches};
403 use hopr_lib::HostType;
404 use tempfile::NamedTempFile;
405
406 use super::*;
407
408 pub fn example_cfg() -> anyhow::Result<HoprdConfig> {
409 let chain = hopr_lib::config::Chain {
410 protocols: hopr_lib::ProtocolsConfig::from_str(
411 r#"
412 {
413 "networks": {
414 "anvil-localhost": {
415 "chain": "anvil",
416 "environment_type": "local",
417 "version_range": "*",
418 "indexer_start_block_number": 5,
419 "addresses": {
420 "network_registry": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
421 "network_registry_proxy": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
422 "channels": "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
423 "token": "0x9A676e781A523b5d0C0e43731313A708CB607508",
424 "module_implementation": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
425 "node_safe_registry": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
426 "ticket_price_oracle": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
427 "winning_probability_oracle": "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
428 "announcements": "0xc5a5C42992dECbae36851359345FE25997F5C42d",
429 "node_stake_v2_factory": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
430 },
431 "confirmations": 2,
432 "tags": [],
433 "tx_polling_interval": 1000,
434 "max_block_range": 200
435 }
436 },
437 "chains": {
438 "anvil": {
439 "description": "Local Ethereum node, akin to Ganache, Hardhat chain",
440 "chain_id": 31337,
441 "live": false,
442 "max_fee_per_gas": "1 gwei",
443 "max_priority_fee_per_gas": "0.2 gwei",
444 "default_provider": "http://127.0.0.1:8545/",
445 "native_token_name": "ETH",
446 "hopr_token_name": "wxHOPR",
447 "block_time": 5000,
448 "max_rpc_requests_per_sec": 100,
449 "tags": [],
450 "etherscan_api_url": null
451 }
452 }
453 }
454 "#,
455 )
456 .map_err(|e| anyhow::anyhow!(e))?,
457 ..hopr_lib::config::Chain::default()
458 };
459
460 let db = hopr_lib::config::Db {
461 data: "/app/db".to_owned(),
462 ..hopr_lib::config::Db::default()
463 };
464
465 let safe_module = hopr_lib::config::SafeModule {
466 safe_transaction_service_provider: "https:://provider.com/".to_owned(),
467 safe_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
468 module_address: Address::from_str("0x0000000000000000000000000000000000000000")?,
469 };
470
471 let identity = Identity {
472 file: "path/to/identity.file".to_string(),
473 password: "change_me".to_owned(),
474 private_key: None,
475 };
476
477 let host = HostConfig {
478 address: HostType::IPv4("1.2.3.4".into()),
479 port: 9091,
480 };
481
482 Ok(HoprdConfig {
483 hopr: HoprLibConfig {
484 host,
485 db,
486 chain,
487 safe_module,
488 ..HoprLibConfig::default()
489 },
490 identity,
491 ..HoprdConfig::default()
492 })
493 }
494
495 #[test]
496 fn test_config_should_be_serializable_into_string() -> Result<(), Box<dyn std::error::Error>> {
497 let cfg = example_cfg()?;
498
499 let from_yaml: HoprdConfig = serde_yaml::from_str(include_str!("../example_cfg.yaml"))?;
500
501 assert_eq!(cfg, from_yaml);
502
503 Ok(())
504 }
505
506 #[test]
507 fn test_config_should_be_deserializable_from_a_string_in_a_file() -> Result<(), Box<dyn std::error::Error>> {
508 let mut config_file = NamedTempFile::new()?;
509 let mut prepared_config_file = config_file.reopen()?;
510
511 let cfg = example_cfg()?;
512 let yaml = serde_yaml::to_string(&cfg)?;
513 config_file.write_all(yaml.as_bytes())?;
514
515 let mut buf = String::new();
516 prepared_config_file.read_to_string(&mut buf)?;
517 let deserialized_cfg: HoprdConfig = serde_yaml::from_str(&buf)?;
518
519 assert_eq!(deserialized_cfg, cfg);
520
521 Ok(())
522 }
523
524 #[test]
528 #[ignore]
529 fn test_config_is_extractable_from_the_cli_arguments() -> anyhow::Result<()> {
530 let pwnd = "rpc://pawned!";
531
532 let mut config_file = NamedTempFile::new()?;
533
534 let mut cfg = example_cfg()?;
535 cfg.hopr.chain.provider = Some(pwnd.to_owned());
536
537 let yaml = serde_yaml::to_string(&cfg)?;
538 config_file.write_all(yaml.as_bytes())?;
539 let cfg_file_path = config_file
540 .path()
541 .to_str()
542 .context("file path should have a string representation")?
543 .to_string();
544
545 let cli_args = vec!["hoprd", "--configurationFilePath", cfg_file_path.as_str()];
546
547 let mut cmd = Command::new("hoprd").version("0.0.0");
548 cmd = crate::cli::CliArgs::augment_args(cmd);
549 let derived_matches = cmd.try_get_matches_from(cli_args)?;
550 let args = crate::cli::CliArgs::from_arg_matches(&derived_matches)?;
551
552 let cfg = HoprdConfig::from_cli_args(args, true);
554
555 assert!(cfg.is_ok());
556
557 let cfg = cfg?;
558
559 assert_eq!(cfg.hopr.chain.provider, Some(pwnd.to_owned()));
560
561 Ok(())
562 }
563}