use config::{Config, ConfigError, Environment, File};
use http::Uri;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr, DurationMilliSeconds, DurationSeconds};
#[cfg(feature = "ipfs")]
use std::net::Ipv4Addr;
use std::{
env,
net::{IpAddr, Ipv6Addr},
path::PathBuf,
time::Duration,
};
mod libp2p_config;
mod pubkey_config;
pub(crate) use libp2p_config::{Dht, Libp2p, Pubsub};
pub(crate) use pubkey_config::PubkeyConfig;
#[cfg(target_os = "windows")]
const HOME_VAR: &str = "USERPROFILE";
#[cfg(not(target_os = "windows"))]
const HOME_VAR: &str = "HOME";
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Settings {
#[serde(default)]
pub(crate) node: Node,
}
impl Settings {
pub fn node(&self) -> &Node {
&self.node
}
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct Node {
#[serde(default)]
pub(crate) monitoring: Monitoring,
#[serde(default)]
pub(crate) network: Network,
#[serde(default)]
pub(crate) db: Database,
#[serde_as(as = "DurationSeconds<u64>")]
pub(crate) gc_interval: Duration,
#[serde_as(as = "DurationSeconds<u64>")]
pub(crate) shutdown_timeout: Duration,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub(crate) struct Database {
#[serde_as(as = "Option<DisplayFromStr>")]
pub(crate) url: Option<String>,
pub(crate) max_pool_size: u32,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct Monitoring {
pub console_subscriber_port: u16,
#[cfg(feature = "monitoring")]
#[cfg_attr(docsrs, doc(cfg(feature = "monitoring")))]
#[serde_as(as = "DurationMilliSeconds<u64>")]
pub process_collector_interval: Duration,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub struct Network {
pub(crate) libp2p: Libp2p,
pub(crate) metrics: Metrics,
pub(crate) events_buffer_len: usize,
pub(crate) rpc: Rpc,
pub(crate) keypair_config: PubkeyConfig,
#[serde_as(as = "DurationMilliSeconds<u64>")]
pub(crate) poll_cache_interval: Duration,
#[cfg(feature = "ipfs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
pub(crate) ipfs: Ipfs,
pub(crate) webserver: Webserver,
}
#[cfg(feature = "ipfs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub(crate) struct Ipfs {
pub(crate) host: String,
pub(crate) port: u16,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub(crate) struct Metrics {
pub(crate) port: u16,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub(crate) struct Rpc {
#[serde_as(as = "DisplayFromStr")]
pub(crate) host: IpAddr,
pub(crate) max_connections: usize,
pub(crate) port: u16,
#[serde_as(as = "DurationSeconds<u64>")]
pub(crate) server_timeout: Duration,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(default)]
pub(crate) struct Webserver {
#[serde(with = "http_serde::uri")]
pub(crate) host: Uri,
pub(crate) port: u16,
#[serde_as(as = "DurationSeconds<u64>")]
pub(crate) timeout: Duration,
pub(crate) websocket_capacity: usize,
#[serde_as(as = "DurationMilliSeconds<u64>")]
pub(crate) websocket_sender_timeout: Duration,
}
impl Default for Node {
fn default() -> Self {
Self {
gc_interval: Duration::from_secs(1800),
shutdown_timeout: Duration::from_secs(20),
monitoring: Default::default(),
network: Default::default(),
db: Default::default(),
}
}
}
impl Node {
pub fn monitoring(&self) -> &Monitoring {
&self.monitoring
}
pub fn network(&self) -> &Network {
&self.network
}
pub fn shutdown_timeout(&self) -> Duration {
self.shutdown_timeout
}
}
impl Default for Database {
fn default() -> Self {
Self {
max_pool_size: 100,
url: None,
}
}
}
#[cfg(feature = "monitoring")]
#[cfg_attr(docsrs, doc(cfg(feature = "monitoring")))]
impl Default for Monitoring {
fn default() -> Self {
Self {
process_collector_interval: Duration::from_millis(5000),
console_subscriber_port: 6669,
}
}
}
#[cfg(not(feature = "monitoring"))]
impl Default for Monitoring {
fn default() -> Self {
Self {
console_subscriber_port: 6669,
}
}
}
impl Default for Network {
fn default() -> Self {
Self {
libp2p: Libp2p::default(),
metrics: Metrics::default(),
events_buffer_len: 1024,
rpc: Rpc::default(),
keypair_config: PubkeyConfig::Random,
poll_cache_interval: Duration::from_millis(1000),
#[cfg(feature = "ipfs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
ipfs: Default::default(),
webserver: Webserver::default(),
}
}
}
impl Network {
#[cfg(feature = "ipfs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
pub(crate) fn ipfs(&self) -> &Ipfs {
&self.ipfs
}
pub(crate) fn libp2p(&self) -> &Libp2p {
&self.libp2p
}
pub(crate) fn webserver(&self) -> &Webserver {
&self.webserver
}
}
#[cfg(feature = "ipfs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ipfs")))]
impl Default for Ipfs {
fn default() -> Self {
Self {
host: Ipv4Addr::LOCALHOST.to_string(),
port: 5001,
}
}
}
impl Default for Metrics {
fn default() -> Self {
Self { port: 4000 }
}
}
impl Default for Rpc {
fn default() -> Self {
Self {
host: IpAddr::V6(Ipv6Addr::LOCALHOST),
max_connections: 10,
port: 3030,
server_timeout: Duration::new(120, 0),
}
}
}
impl Default for Webserver {
fn default() -> Self {
Self {
host: Uri::from_static("127.0.0.1"),
port: 1337,
timeout: Duration::new(120, 0),
websocket_capacity: 2048,
websocket_sender_timeout: Duration::from_millis(30_000),
}
}
}
impl Settings {
pub fn load() -> Result<Self, ConfigError> {
#[cfg(test)]
{
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config/settings.toml");
Self::build(Some(path))
}
#[cfg(not(test))]
Self::build(None)
}
pub fn load_from_file(file: PathBuf) -> Result<Self, ConfigError> {
Self::build(Some(file))
}
fn build(path: Option<PathBuf>) -> Result<Self, ConfigError> {
let builder = if let Some(p) = path {
Config::builder().add_source(File::with_name(
&p.canonicalize()
.map_err(|e| ConfigError::NotFound(e.to_string()))?
.as_path()
.display()
.to_string(),
))
} else {
Config::builder()
};
let s = builder
.add_source(Environment::with_prefix("HOMESTAR").separator("__"))
.build()?;
s.try_deserialize()
}
}
#[allow(dead_code)]
fn config_dir() -> PathBuf {
let config_dir =
env::var("XDG_CONFIG_HOME").map_or_else(|_| home_dir().join(".config"), PathBuf::from);
config_dir.join("homestar")
}
#[allow(dead_code)]
fn home_dir() -> PathBuf {
let home = env::var(HOME_VAR).unwrap_or_else(|_| panic!("{} not found", HOME_VAR));
PathBuf::from(home)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn defaults() {
let settings = Settings::load().unwrap();
let node_settings = settings.node;
let default_settings = Node {
gc_interval: Duration::from_secs(1800),
shutdown_timeout: Duration::from_secs(20),
..Default::default()
};
assert_eq!(node_settings, default_settings);
}
#[test]
fn defaults_with_modification() {
let settings = Settings::build(Some("fixtures/settings.toml".into())).unwrap();
let mut default_modded_settings = Node::default();
default_modded_settings.network.events_buffer_len = 1000;
default_modded_settings.network.webserver.port = 9999;
default_modded_settings.gc_interval = Duration::from_secs(1800);
default_modded_settings.shutdown_timeout = Duration::from_secs(20);
default_modded_settings.network.libp2p.node_addresses =
vec!["/ip4/127.0.0.1/tcp/9998/ws".to_string().try_into().unwrap()];
assert_eq!(settings.node(), &default_modded_settings);
}
#[test]
#[serial_test::parallel]
fn default_config() {
let settings = Settings::load().unwrap();
let default_config = Settings::build(Some("config/defaults.toml".into()))
.expect("default settings file in test fixtures");
assert_eq!(settings, default_config);
}
#[test]
#[serial_test::file_serial]
fn overriding_env_serial() {
std::env::set_var("HOMESTAR__NODE__NETWORK__RPC__PORT", "2046");
std::env::set_var("HOMESTAR__NODE__DB__MAX_POOL_SIZE", "1");
let settings = Settings::build(Some("fixtures/settings.toml".into())).unwrap();
assert_eq!(settings.node.network.rpc.port, 2046);
assert_eq!(settings.node.db.max_pool_size, 1);
}
#[test]
fn import_existing_key() {
let settings = Settings::build(Some("fixtures/settings-import-ed25519.toml".into()))
.expect("setting file in test fixtures");
let msg = b"foo bar";
let signature = libp2p::identity::Keypair::ed25519_from_bytes([0; 32])
.unwrap()
.sign(msg)
.unwrap();
assert!(settings
.node
.network
.keypair_config
.keypair()
.expect("import ed25519 key")
.public()
.verify(msg, &signature));
}
#[test]
fn import_secp256k1_key() {
let settings = Settings::build(Some("fixtures/settings-import-secp256k1.toml".into()))
.expect("setting file in test fixtures");
settings
.node
.network
.keypair_config
.keypair()
.expect("import secp256k1 key");
}
#[test]
fn seeded_secp256k1_key() {
let settings = Settings::build(Some("fixtures/settings-random-secp256k1.toml".into()))
.expect("setting file in test fixtures");
settings
.node
.network
.keypair_config
.keypair()
.expect("generate a seeded secp256k1 key");
}
#[test]
#[serial_test::file_serial]
fn test_config_dir_xdg() {
env::remove_var("HOME");
env::set_var("XDG_CONFIG_HOME", "/home/user/custom_config");
assert_eq!(
config_dir(),
PathBuf::from("/home/user/custom_config/homestar")
);
env::remove_var("XDG_CONFIG_HOME");
}
#[cfg(not(target_os = "windows"))]
#[test]
#[serial_test::file_serial]
fn test_config_dir() {
env::set_var("HOME", "/home/user");
env::remove_var("XDG_CONFIG_HOME");
assert_eq!(config_dir(), PathBuf::from("/home/user/.config/homestar"));
env::remove_var("HOME");
}
#[cfg(target_os = "windows")]
#[test]
#[serial_test::file_serial]
fn test_config_dir() {
env::remove_var("XDG_CONFIG_HOME");
assert_eq!(
config_dir(),
PathBuf::from(format!(r"{}\.config\homestar", env!("USERPROFILE")))
);
}
}