homestar_runtime/
settings.rs

1//! General runtime settings / configuration.
2
3use 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/// Application settings.
28#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
29pub struct Settings {
30    /// Node settings
31    #[builder(default)]
32    #[serde(default)]
33    pub(crate) node: Node,
34}
35
36impl Settings {
37    /// Node settings getter.
38    pub fn node(&self) -> &Node {
39        &self.node
40    }
41}
42
43/// Server settings.
44#[serde_as]
45#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
46#[builder(default)]
47#[serde(default)]
48pub struct Node {
49    /// Monitoring settings.
50    #[serde(default)]
51    pub(crate) monitoring: Monitoring,
52    /// Network settings.
53    #[serde(default)]
54    pub(crate) network: Network,
55    /// Database settings.
56    #[serde(default)]
57    pub(crate) db: Database,
58    /// Garbage collection interval.
59    #[serde_as(as = "DurationSeconds<u64>")]
60    pub(crate) gc_interval: Duration,
61    /// Shutdown timeout.
62    #[serde_as(as = "DurationSeconds<u64>")]
63    pub(crate) shutdown_timeout: Duration,
64}
65
66/// Database-related settings for a homestar node.
67#[serde_as]
68#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
69#[builder(default)]
70#[serde(default)]
71pub struct Database {
72    /// Database Url provided within the configuration file.
73    ///
74    /// Note: This is not used if the `DATABASE_URL` environment variable
75    /// is set.
76    #[serde_as(as = "Option<DisplayFromStr>")]
77    pub(crate) url: Option<String>,
78    /// Maximum number of connections managed by the pool.
79    ///
80    /// [pool]: crate::db::Pool
81    pub(crate) max_pool_size: u32,
82}
83
84/// Monitoring settings.
85#[serde_as]
86#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
87#[builder(default)]
88#[serde(default)]
89pub struct Monitoring {
90    /// Tokio console port.
91    pub console_subscriber_port: u16,
92    /// Monitoring collection interval in milliseconds.
93    #[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/// Network settings for a homestar node.
100#[serde_as]
101#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
102#[builder(default)]
103#[serde(default)]
104pub struct Network {
105    /// libp2p Settings.
106    pub(crate) libp2p: Libp2p,
107    /// Metrics Settings.
108    pub(crate) metrics: Metrics,
109    /// Buffer-length for event(s) / command(s) channels.
110    pub(crate) events_buffer_len: usize,
111    /// RPC server settings.
112    pub(crate) rpc: Rpc,
113    /// Pubkey setup configuration.
114    pub(crate) keypair_config: PubkeyConfig,
115    /// Event handler poll cache interval in milliseconds.
116    #[serde_as(as = "DurationMilliSeconds<u64>")]
117    pub(crate) poll_cache_interval: Duration,
118    /// IPFS settings.
119    #[cfg(feature = "ipfs")]
120    #[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
121    pub(crate) ipfs: Ipfs,
122    /// Webserver settings
123    pub(crate) webserver: Webserver,
124}
125
126/// IPFS Settings
127#[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    /// The host where Homestar expects IPFS.
135    pub(crate) host: String,
136    /// The port where Homestar expects IPFS.
137    pub(crate) port: u16,
138}
139
140/// Metrics settings.
141#[serde_as]
142#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
143#[builder(default)]
144#[serde(default)]
145pub struct Metrics {
146    /// Metrics port for prometheus scraping.
147    pub(crate) port: u16,
148}
149
150/// RPC server settings.
151#[serde_as]
152#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
153#[builder(default)]
154#[serde(default)]
155pub struct Rpc {
156    /// RPC-server port.
157    #[serde_as(as = "DisplayFromStr")]
158    pub(crate) host: IpAddr,
159    /// RPC-server max-concurrent connections.
160    pub(crate) max_connections: usize,
161    /// RPC-server port.
162    pub(crate) port: u16,
163    #[serde_as(as = "DurationSeconds<u64>")]
164    /// RPC-server timeout.
165    pub(crate) server_timeout: Duration,
166}
167
168/// Webserver settings
169#[serde_as]
170#[derive(Builder, Clone, Debug, Serialize, Deserialize, PartialEq)]
171#[builder(default)]
172#[serde(default)]
173pub struct Webserver {
174    /// V4 Webserver host address.
175    #[serde(with = "http_serde::uri")]
176    pub(crate) v4_host: Uri,
177    /// V6 (fallback) Webserver host address.
178    #[serde(with = "http_serde::uri")]
179    pub(crate) v6_host: Uri,
180    /// Webserver-server port.
181    pub(crate) port: u16,
182    /// Webserver timeout.
183    #[serde_as(as = "DurationSeconds<u64>")]
184    pub(crate) timeout: Duration,
185    /// Message capacity for the websocket-server.
186    pub(crate) websocket_capacity: usize,
187    /// Websocket-server send timeout.
188    #[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    /// Monitoring settings getter.
206    pub fn monitoring(&self) -> &Monitoring {
207        &self.monitoring
208    }
209
210    /// Network settings.
211    pub fn network(&self) -> &Network {
212        &self.network
213    }
214
215    /// Node shutdown timeout.
216    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    /// IPFS settings.
269    #[cfg(feature = "ipfs")]
270    #[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
271    pub(crate) fn ipfs(&self) -> &Ipfs {
272        &self.ipfs
273    }
274
275    /// libp2p settings.
276    pub(crate) fn libp2p(&self) -> &Libp2p {
277        &self.libp2p
278    }
279
280    /// Webserver settings.
281    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    /// Settings file path.
329    pub fn path() -> PathBuf {
330        config_file_with_extension("toml")
331    }
332
333    /// Load settings.
334    ///
335    /// Inject environment variables naming them properly on the settings,
336    /// e.g. HOMESTAR__NODE__DB__MAX_POOL_SIZE=10.
337    ///
338    /// Use two underscores as defined by the separator below
339    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    /// Load settings from file string that must conform to a [PathBuf].
350    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        // Test using a key not containing curve parameters
451        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        // round-about way of testing since there is no Eq derive for keypairs
462        assert!(settings
463            .node
464            .network
465            .keypair_config
466            .keypair()
467            .expect("import ed25519 key")
468            .public()
469            .verify(msg, &signature));
470
471        // Test using a key containing curve parameters
472        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        // round-about way of testing since there is no Eq derive for keypairs
488        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}