1use config::{Config, ConfigError, Environment, File};
4use derive_builder::Builder;
5use http::Uri;
6use serde::{Deserialize, Serialize};
7use serde_with::{serde_as, DisplayFromStr, DurationMilliSeconds, DurationSeconds};
8#[cfg(feature = "ipfs")]
9use std::net::Ipv4Addr;
10use std::{
11 env,
12 net::{IpAddr, Ipv6Addr},
13 path::PathBuf,
14 time::Duration,
15};
16
17mod libp2p_config;
18mod pubkey_config;
19pub use libp2p_config::{Dht, Libp2p, Mdns, Pubsub, Rendezvous};
20pub use pubkey_config::{ExistingKeyPath, KeyType, PubkeyConfig, RNGSeed};
21
22#[cfg(target_os = "windows")]
23const HOME_VAR: &str = "USERPROFILE";
24#[cfg(not(target_os = "windows"))]
25const HOME_VAR: &str = "HOME";
26
27#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
29pub struct Settings {
30 #[builder(default)]
32 #[serde(default)]
33 pub(crate) node: Node,
34}
35
36impl Settings {
37 pub fn node(&self) -> &Node {
39 &self.node
40 }
41}
42
43#[serde_as]
45#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
46#[builder(default)]
47#[serde(default)]
48pub struct Node {
49 #[serde(default)]
51 pub(crate) monitoring: Monitoring,
52 #[serde(default)]
54 pub(crate) network: Network,
55 #[serde(default)]
57 pub(crate) db: Database,
58 #[serde_as(as = "DurationSeconds<u64>")]
60 pub(crate) gc_interval: Duration,
61 #[serde_as(as = "DurationSeconds<u64>")]
63 pub(crate) shutdown_timeout: Duration,
64}
65
66#[serde_as]
68#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
69#[builder(default)]
70#[serde(default)]
71pub struct Database {
72 #[serde_as(as = "Option<DisplayFromStr>")]
77 pub(crate) url: Option<String>,
78 pub(crate) max_pool_size: u32,
82}
83
84#[serde_as]
86#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
87#[builder(default)]
88#[serde(default)]
89pub struct Monitoring {
90 pub console_subscriber_port: u16,
92 #[cfg(feature = "monitoring")]
94 #[cfg_attr(docsrs, doc(cfg(feature = "monitoring")))]
95 #[serde_as(as = "DurationMilliSeconds<u64>")]
96 pub process_collector_interval: Duration,
97}
98
99#[serde_as]
101#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
102#[builder(default)]
103#[serde(default)]
104pub struct Network {
105 pub(crate) libp2p: Libp2p,
107 pub(crate) metrics: Metrics,
109 pub(crate) events_buffer_len: usize,
111 pub(crate) rpc: Rpc,
113 pub(crate) keypair_config: PubkeyConfig,
115 #[serde_as(as = "DurationMilliSeconds<u64>")]
117 pub(crate) poll_cache_interval: Duration,
118 #[cfg(feature = "ipfs")]
120 #[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
121 pub(crate) ipfs: Ipfs,
122 pub(crate) webserver: Webserver,
124}
125
126#[cfg(feature = "ipfs")]
128#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
129#[serde_as]
130#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
131#[builder(default)]
132#[serde(default)]
133pub struct Ipfs {
134 pub(crate) host: String,
136 pub(crate) port: u16,
138}
139
140#[serde_as]
142#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
143#[builder(default)]
144#[serde(default)]
145pub struct Metrics {
146 pub(crate) port: u16,
148}
149
150#[serde_as]
152#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
153#[builder(default)]
154#[serde(default)]
155pub struct Rpc {
156 #[serde_as(as = "DisplayFromStr")]
158 pub(crate) host: IpAddr,
159 pub(crate) max_connections: usize,
161 pub(crate) port: u16,
163 #[serde_as(as = "DurationSeconds<u64>")]
164 pub(crate) server_timeout: Duration,
166}
167
168#[serde_as]
170#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
171#[builder(default)]
172#[serde(default)]
173pub struct Webserver {
174 #[serde(with = "http_serde::uri")]
176 pub(crate) v4_host: Uri,
177 #[serde(with = "http_serde::uri")]
179 pub(crate) v6_host: Uri,
180 pub(crate) port: u16,
182 #[serde_as(as = "DurationSeconds<u64>")]
184 pub(crate) timeout: Duration,
185 pub(crate) websocket_capacity: usize,
187 #[serde_as(as = "DurationMilliSeconds<u64>")]
189 pub(crate) websocket_sender_timeout: Duration,
190}
191
192impl Default for Node {
193 fn default() -> Self {
194 Self {
195 gc_interval: Duration::from_secs(1800),
196 shutdown_timeout: Duration::from_secs(20),
197 monitoring: Default::default(),
198 network: Default::default(),
199 db: Default::default(),
200 }
201 }
202}
203
204impl Node {
205 pub fn monitoring(&self) -> &Monitoring {
207 &self.monitoring
208 }
209
210 pub fn network(&self) -> &Network {
212 &self.network
213 }
214
215 pub fn shutdown_timeout(&self) -> Duration {
217 self.shutdown_timeout
218 }
219}
220
221impl Default for Database {
222 fn default() -> Self {
223 Self {
224 max_pool_size: 100,
225 url: None,
226 }
227 }
228}
229
230#[cfg(feature = "monitoring")]
231#[cfg_attr(docsrs, doc(cfg(feature = "monitoring")))]
232impl Default for Monitoring {
233 fn default() -> Self {
234 Self {
235 process_collector_interval: Duration::from_millis(5000),
236 console_subscriber_port: 6669,
237 }
238 }
239}
240
241#[cfg(not(feature = "monitoring"))]
242impl Default for Monitoring {
243 fn default() -> Self {
244 Self {
245 console_subscriber_port: 6669,
246 }
247 }
248}
249
250impl Default for Network {
251 fn default() -> Self {
252 Self {
253 libp2p: Libp2p::default(),
254 metrics: Metrics::default(),
255 events_buffer_len: 1024,
256 rpc: Rpc::default(),
257 keypair_config: PubkeyConfig::Random,
258 poll_cache_interval: Duration::from_millis(1000),
259 #[cfg(feature = "ipfs")]
260 #[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
261 ipfs: Default::default(),
262 webserver: Webserver::default(),
263 }
264 }
265}
266
267impl Network {
268 #[cfg(feature = "ipfs")]
270 #[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
271 pub(crate) fn ipfs(&self) -> &Ipfs {
272 &self.ipfs
273 }
274
275 pub(crate) fn libp2p(&self) -> &Libp2p {
277 &self.libp2p
278 }
279
280 pub(crate) fn webserver(&self) -> &Webserver {
282 &self.webserver
283 }
284}
285
286#[cfg(feature = "ipfs")]
287#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
288impl Default for Ipfs {
289 fn default() -> Self {
290 Self {
291 host: Ipv4Addr::LOCALHOST.to_string(),
292 port: 5001,
293 }
294 }
295}
296
297impl Default for Metrics {
298 fn default() -> Self {
299 Self { port: 4000 }
300 }
301}
302
303impl Default for Rpc {
304 fn default() -> Self {
305 Self {
306 host: IpAddr::V6(Ipv6Addr::LOCALHOST),
307 max_connections: 10,
308 port: 3030,
309 server_timeout: Duration::new(120, 0),
310 }
311 }
312}
313
314impl Default for Webserver {
315 fn default() -> Self {
316 Self {
317 v4_host: Uri::from_static("127.0.0.1"),
318 v6_host: Uri::from_static("[::1]"),
319 port: 1337,
320 timeout: Duration::new(120, 0),
321 websocket_capacity: 2048,
322 websocket_sender_timeout: Duration::from_millis(30_000),
323 }
324 }
325}
326
327impl Settings {
328 pub fn path() -> PathBuf {
330 config_file_with_extension("toml")
331 }
332
333 pub fn load() -> Result<Self, ConfigError> {
340 #[cfg(test)]
341 {
342 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config/settings.toml");
343 Self::build(Some(path))
344 }
345 #[cfg(not(test))]
346 Self::build(None)
347 }
348
349 pub fn load_from_file(file: PathBuf) -> Result<Self, ConfigError> {
351 Self::build(Some(file))
352 }
353
354 fn build(path: Option<PathBuf>) -> Result<Self, ConfigError> {
355 let builder = Config::builder();
356
357 #[cfg(not(test))]
358 let builder = builder.add_source(File::from(config_file()).required(false));
359
360 let builder = if let Some(p) = path {
361 builder.add_source(File::with_name(
362 &p.canonicalize()
363 .map_err(|e| ConfigError::NotFound(e.to_string()))?
364 .as_path()
365 .display()
366 .to_string(),
367 ))
368 } else {
369 builder
370 };
371
372 let s = builder
373 .add_source(Environment::with_prefix("HOMESTAR").separator("__"))
374 .build()?;
375 s.try_deserialize()
376 }
377}
378
379fn config_file() -> PathBuf {
380 config_dir().join("settings")
381}
382
383fn config_file_with_extension(ext: &str) -> PathBuf {
384 config_file().with_extension(ext)
385}
386
387fn config_dir() -> PathBuf {
388 let config_dir =
389 env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
390 config_dir.join("homestar")
391}
392
393fn home_dir() -> PathBuf {
394 let home = env::var(HOME_VAR).unwrap_or_else(|_| panic!("{} not found", HOME_VAR));
395 PathBuf::from(home)
396}
397
398#[cfg(test)]
399mod test {
400 use super::*;
401
402 #[test]
403 fn defaults() {
404 let settings = Settings::load().unwrap();
405 let node_settings = settings.node;
406
407 let default_settings = Node {
408 gc_interval: Duration::from_secs(1800),
409 shutdown_timeout: Duration::from_secs(20),
410 ..Default::default()
411 };
412
413 assert_eq!(node_settings, default_settings);
414 }
415
416 #[test]
417 fn defaults_with_modification() {
418 let settings = Settings::build(Some("fixtures/settings.toml".into())).unwrap();
419
420 let mut default_modded_settings = Node::default();
421 default_modded_settings.network.events_buffer_len = 1000;
422 default_modded_settings.network.webserver.port = 9999;
423 default_modded_settings.gc_interval = Duration::from_secs(1800);
424 default_modded_settings.shutdown_timeout = Duration::from_secs(20);
425 default_modded_settings.network.libp2p.node_addresses =
426 vec!["/ip4/127.0.0.1/tcp/9998/ws".to_string().try_into().unwrap()];
427 assert_eq!(settings.node(), &default_modded_settings);
428 }
429
430 #[test]
431 #[serial_test::parallel]
432 fn default_config() {
433 let settings = Settings::load().unwrap();
434 let default_config = Settings::default();
435 assert_eq!(settings, default_config);
436 }
437
438 #[test]
439 #[serial_test::file_serial]
440 fn overriding_env_serial() {
441 std::env::set_var("HOMESTAR__NODE__NETWORK__RPC__PORT", "2046");
442 std::env::set_var("HOMESTAR__NODE__DB__MAX_POOL_SIZE", "1");
443 let settings = Settings::build(Some("fixtures/settings.toml".into())).unwrap();
444 assert_eq!(settings.node.network.rpc.port, 2046);
445 assert_eq!(settings.node.db.max_pool_size, 1);
446 }
447
448 #[test]
449 fn import_existing_key() {
450 let settings = Settings::build(Some("fixtures/settings-import-ed25519.toml".into()))
452 .expect("setting file in test fixtures");
453
454 let msg = b"foo bar";
455 let key = [0; 32];
456 let signature = libp2p::identity::Keypair::ed25519_from_bytes(key)
457 .unwrap()
458 .sign(msg)
459 .unwrap();
460
461 assert!(settings
463 .node
464 .network
465 .keypair_config
466 .keypair()
467 .expect("import ed25519 key")
468 .public()
469 .verify(msg, &signature));
470
471 let settings = Settings::build(Some(
473 "fixtures/settings-import-ed25519-with-params.toml".into(),
474 ))
475 .expect("setting file in test fixtures");
476
477 let msg = b"foo bar";
478 let key = [
479 255, 211, 202, 168, 61, 181, 166, 62, 247, 234, 100, 3, 193, 51, 5, 251, 20, 1, 62,
480 135, 139, 231, 142, 86, 225, 243, 163, 90, 161, 31, 155, 129,
481 ];
482 let signature = libp2p::identity::Keypair::ed25519_from_bytes(key)
483 .unwrap()
484 .sign(msg)
485 .unwrap();
486
487 assert!(settings
489 .node
490 .network
491 .keypair_config
492 .keypair()
493 .expect("import ed25519 key")
494 .public()
495 .verify(msg, &signature));
496 }
497
498 #[test]
499 fn import_secp256k1_key() {
500 let settings = Settings::build(Some("fixtures/settings-import-secp256k1.toml".into()))
501 .expect("setting file in test fixtures");
502
503 settings
504 .node
505 .network
506 .keypair_config
507 .keypair()
508 .expect("import secp256k1 key");
509 }
510
511 #[test]
512 fn seeded_secp256k1_key() {
513 let settings = Settings::build(Some("fixtures/settings-random-secp256k1.toml".into()))
514 .expect("setting file in test fixtures");
515
516 settings
517 .node
518 .network
519 .keypair_config
520 .keypair()
521 .expect("generate a seeded secp256k1 key");
522 }
523
524 #[test]
525 #[serial_test::file_serial]
526 fn test_config_dir_xdg() {
527 env::remove_var("HOME");
528 env::set_var("XDG_CONFIG_HOME", "/home/user/custom_config");
529 assert_eq!(
530 config_dir(),
531 PathBuf::from("/home/user/custom_config/homestar")
532 );
533 env::remove_var("XDG_CONFIG_HOME");
534 }
535
536 #[cfg(not(target_os = "windows"))]
537 #[test]
538 #[serial_test::file_serial]
539 fn test_config_dir() {
540 env::set_var("HOME", "/home/user");
541 env::remove_var("XDG_CONFIG_HOME");
542 assert_eq!(config_dir(), PathBuf::from("/home/user/.config/homestar"));
543 env::remove_var("HOME");
544 }
545
546 #[cfg(target_os = "windows")]
547 #[test]
548 #[serial_test::file_serial]
549 fn test_config_dir() {
550 env::remove_var("XDG_CONFIG_HOME");
551 assert_eq!(
552 config_dir(),
553 PathBuf::from(format!(r"{}\.config\homestar", env!("USERPROFILE")))
554 );
555 }
556}