use actix_http::header::HeaderMap;
use config::{Config, ConfigError, Environment, File};
use fernet::{Fernet, MultiFernet};
use serde::Deserialize;
use url::Url;
use crate::headers::vapid::VapidHeaderWithKey;
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 tracking_keys: 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(),
max_data_bytes: 5630,
crypto_keys: format!("[{}]", Fernet::generate_key()),
auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB="]"#.to_string(),
tracking_keys: r#"[]"#.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: Self = config.build()?.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
}
}
})?;
Ok(built)
}
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 tracking_keys(&self) -> Vec<String> {
let keys = &self.tracking_keys.replace(['"', ' '], "");
let result = Self::read_list_from_str(keys, "Invalid AUTOEND_TRACKING_KEYS")
.map(|v| v.to_owned().replace("=", ""))
.collect();
trace!("🔍 tracking_keys: {result:?}");
result
}
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")
}
}
#[derive(Clone, Debug)]
pub struct VapidTracker(pub Vec<String>);
impl VapidTracker {
pub fn is_trackable(&self, vapid: &VapidHeaderWithKey) -> bool {
let key = vapid.public_key.replace('=', "");
let result = self.0.contains(&key);
debug!("🔍 Checking {key} {}", {
if result {
"Match!"
} else {
"no match"
}
});
result
}
pub fn get_id(&self, headers: &HeaderMap) -> String {
headers
.get("X-MessageId")
.and_then(|v|
v.to_str().ok())
.map(|v| v.to_owned())
.unwrap_or_else(|| uuid::Uuid::new_v4().as_simple().to_string())
}
}
#[cfg(test)]
mod tests {
use actix_http::header::{HeaderMap, HeaderName, HeaderValue};
use super::{Settings, VapidTracker};
use crate::{
error::ApiResult,
headers::vapid::{VapidHeader, VapidHeaderWithKey},
};
#[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);
}
}
#[test]
fn test_tracking_keys() -> ApiResult<()> {
let settings = Settings{
tracking_keys: r#"["BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon-H_boHTzMtMoNHsAGDlDB6X7"]"#.to_owned(),
..Default::default()
};
let test_header = VapidHeaderWithKey {
vapid: VapidHeader {
scheme: "".to_owned(),
token: "".to_owned(),
version_data: crate::headers::vapid::VapidVersionData::Version1,
},
public_key: "BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon-H_boHTzMtMoNHsAGDlDB6X7==".to_owned()
};
let key_set = settings.tracking_keys();
assert!(!key_set.is_empty());
let reliability = VapidTracker(key_set);
assert!(reliability.is_trackable(&test_header));
Ok(())
}
#[test]
fn test_reliability_id() -> ApiResult<()> {
let mut headers = HeaderMap::new();
let keys = Vec::new();
let reliability = VapidTracker(keys);
let key = reliability.get_id(&headers);
assert!(!key.is_empty());
headers.insert(
HeaderName::from_lowercase(b"x-messageid").unwrap(),
HeaderValue::from_static("123foobar456"),
);
let key = reliability.get_id(&headers);
assert_eq!(key, "123foobar456".to_owned());
Ok(())
}
}