use config::{Config, ConfigError, Environment, File};
use fernet::{Fernet, MultiFernet};
use serde::Deserialize;
use url::Url;
use crate::routers::apns::settings::ApnsSettings;
use crate::routers::fcm::settings::FcmSettings;
#[cfg(feature = "stub")]
use crate::routers::stub::settings::StubSettings;
pub const ENV_PREFIX: &str = "autoend";
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Settings {
pub scheme: String,
pub host: String,
pub port: u16,
pub endpoint_url: String,
pub db_dsn: Option<String>,
pub db_settings: String,
pub router_table_name: String,
pub message_table_name: String,
pub vapid_aud: Vec<String>,
pub max_data_bytes: usize,
pub crypto_keys: String,
pub auth_keys: String,
pub human_logs: bool,
pub connection_timeout_millis: u64,
pub request_timeout_millis: u64,
pub statsd_host: Option<String>,
pub statsd_port: u16,
pub statsd_label: String,
pub fcm: FcmSettings,
pub apns: ApnsSettings,
#[cfg(feature = "stub")]
pub stub: StubSettings,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
scheme: "http".to_string(),
host: "127.0.0.1".to_string(),
endpoint_url: "".to_string(),
port: 8000,
db_dsn: None,
db_settings: "".to_owned(),
router_table_name: "router".to_string(),
message_table_name: "message".to_string(),
vapid_aud: vec![
"https://push.services.mozilla.org".to_string(),
"http://127.0.0.1:9160".to_string(),
],
max_data_bytes: 5630,
crypto_keys: format!("[{}]", Fernet::generate_key()),
auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB="]"#.to_string(),
human_logs: false,
connection_timeout_millis: 1000,
request_timeout_millis: 3000,
statsd_host: None,
statsd_port: 8125,
statsd_label: "autoendpoint".to_string(),
fcm: FcmSettings::default(),
apns: ApnsSettings::default(),
#[cfg(feature = "stub")]
stub: StubSettings::default(),
}
}
}
impl Settings {
pub fn with_env_and_config_file(filename: &Option<String>) -> Result<Self, ConfigError> {
let mut config = Config::builder();
if let Some(config_filename) = filename {
config = config.add_source(File::with_name(config_filename));
}
config = config.add_source(Environment::with_prefix(ENV_PREFIX).separator("__"));
let built = config.build()?;
built.try_deserialize::<Self>().map_err(|error| {
match error {
ConfigError::Message(error_msg) => {
println!("Bad configuration: {:?}", &error_msg);
println!("Please set in config file or use environment variable.");
println!(
"For example to set `database_url` use env var `{}_DATABASE_URL`\n",
ENV_PREFIX.to_uppercase()
);
error!("Configuration error: Value undefined {:?}", &error_msg);
ConfigError::NotFound(error_msg)
}
_ => {
error!("Configuration error: Other: {:?}", &error);
error
}
}
})
}
fn read_list_from_str<'list>(
list_str: &'list str,
panic_msg: &'static str,
) -> impl Iterator<Item = &'list str> {
if !(list_str.starts_with('[') && list_str.ends_with(']')) {
panic!("{}", panic_msg);
}
let items = &list_str[1..list_str.len() - 1];
items.split(',')
}
pub fn make_fernet(&self) -> MultiFernet {
let keys = &self.crypto_keys.replace(['"', ' '], "");
let fernets = Self::read_list_from_str(keys, "Invalid AUTOEND_CRYPTO_KEYS")
.map(|key| {
debug!("🔐 Fernet keys: {:?}", &key);
Fernet::new(key).expect("Invalid AUTOEND_CRYPTO_KEYS")
})
.collect();
MultiFernet::new(fernets)
}
pub fn auth_keys(&self) -> Vec<String> {
let keys = &self.auth_keys.replace(['"', ' '], "");
Self::read_list_from_str(keys, "Invalid AUTOEND_AUTH_KEYS")
.map(|v| v.to_owned())
.collect()
}
pub fn endpoint_url(&self) -> Url {
let endpoint = if self.endpoint_url.is_empty() {
format!("{}://{}:{}", self.scheme, self.host, self.port)
} else {
self.endpoint_url.clone()
};
Url::parse(&endpoint).expect("Invalid endpoint URL")
}
}
#[cfg(test)]
mod tests {
use super::Settings;
use crate::error::ApiResult;
#[test]
fn test_auth_keys() -> ApiResult<()> {
let success: Vec<String> = vec![
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=".to_owned(),
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=".to_owned(),
];
let settings = Settings{
auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC="]"#.to_owned(),
..Default::default()
};
let result = settings.auth_keys();
assert_eq!(result, success);
let settings = Settings{
auth_keys: r#"[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=]"#.to_owned(),
..Default::default()
};
let result = settings.auth_keys();
assert_eq!(result, success);
Ok(())
}
#[test]
fn test_endpoint_url() -> ApiResult<()> {
let example = "https://example.org/";
let settings = Settings {
endpoint_url: example.to_owned(),
..Default::default()
};
assert_eq!(settings.endpoint_url(), url::Url::parse(example).unwrap());
let settings = Settings {
..Default::default()
};
assert_eq!(
settings.endpoint_url(),
url::Url::parse(&format!(
"{}://{}:{}",
settings.scheme, settings.host, settings.port
))
.unwrap()
);
Ok(())
}
#[test]
fn test_default_settings() {
let port = format!("{}__PORT", super::ENV_PREFIX).to_uppercase();
let timeout = format!("{}__FCM__TIMEOUT", super::ENV_PREFIX).to_uppercase();
use std::env;
let v1 = env::var(&port);
let v2 = env::var(&timeout);
env::set_var(&port, "9123");
env::set_var(&timeout, "123");
let settings = Settings::with_env_and_config_file(&None).unwrap();
assert_eq!(&settings.port, &9123);
assert_eq!(&settings.fcm.timeout, &123);
assert_eq!(settings.host, "127.0.0.1".to_owned());
if let Ok(p) = v1 {
trace!("Resetting {}", &port);
env::set_var(&port, p);
} else {
env::remove_var(&port);
}
if let Ok(p) = v2 {
trace!("Resetting {}", &timeout);
env::set_var(&timeout, p);
} else {
env::remove_var(&timeout);
}
}
}