1use std::time::Duration;
3
4use actix_http::header::HeaderMap;
5use config::{Config, ConfigError, Environment, File};
6use fernet::{Fernet, MultiFernet};
7use serde::Deserialize;
8use serde_with::serde_as;
9use url::Url;
10
11use autopush_common::{MAX_NOTIFICATION_TTL_SECS, util};
12
13use crate::headers::vapid::VapidHeaderWithKey;
14use crate::routers::apns::settings::ApnsSettings;
15use crate::routers::fcm::settings::FcmSettings;
16#[cfg(feature = "stub")]
17use crate::routers::stub::settings::StubSettings;
18
19pub const ENV_PREFIX: &str = "autoend";
20
21#[serde_as]
22#[derive(Clone, Debug, Deserialize)]
23#[serde(default)]
24pub struct Settings {
25 pub scheme: String,
27 pub host: String,
29 pub port: u16,
31 pub endpoint_url: String,
33
34 pub db_dsn: Option<String>,
36 pub db_settings: String,
38
39 pub router_table_name: String,
41 pub message_table_name: String,
43
44 pub tracking_keys: String,
51
52 pub max_data_bytes: usize,
54 pub crypto_keys: String,
57 pub auth_keys: String,
59 pub human_logs: bool,
61
62 pub connection_timeout_millis: u64,
64 pub request_timeout_millis: u64,
66 pub pool_max_idle_per_host: usize,
68 pub pool_idle_timeout_secs: u64,
70
71 pub statsd_host: Option<String>,
73 pub statsd_port: u16,
75 pub statsd_label: String,
77
78 pub disable_sentry: bool,
80
81 pub fcm: FcmSettings,
83 pub apns: ApnsSettings,
85 #[cfg(feature = "stub")]
86 pub stub: StubSettings,
89 #[cfg(feature = "reliable_report")]
90 pub reliability_dsn: Option<String>,
94 #[cfg(feature = "reliable_report")]
95 pub reliability_retry_count: usize,
97 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
99 pub max_notification_ttl: Duration,
100 pub kubernetes_memory_path: String,
102}
103impl Default for Settings {
106 fn default() -> Settings {
107 Settings {
108 scheme: "http".to_string(),
109 host: "127.0.0.1".to_string(),
110 endpoint_url: "".to_string(),
111 port: 8000,
112 db_dsn: None,
113 db_settings: "".to_owned(),
114 router_table_name: "router".to_string(),
115 message_table_name: "message".to_string(),
116 max_data_bytes: 5630,
121 crypto_keys: format!("[{}]", Fernet::generate_key()),
122 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB="]"#.to_string(),
123 tracking_keys: r#"[]"#.to_string(),
124 human_logs: false,
125 connection_timeout_millis: 1000,
126 request_timeout_millis: 3000,
127 pool_max_idle_per_host: 10,
128 pool_idle_timeout_secs: 30,
129 statsd_host: None,
130 statsd_port: 8125,
131 statsd_label: "autoendpoint".to_string(),
132 fcm: FcmSettings::default(),
133 apns: ApnsSettings::default(),
134 #[cfg(feature = "stub")]
135 stub: StubSettings::default(),
136 #[cfg(feature = "reliable_report")]
137 reliability_dsn: None,
138 #[cfg(feature = "reliable_report")]
139 reliability_retry_count: autopush_common::redis_util::MAX_TRANSACTION_LOOP,
140 max_notification_ttl: Duration::from_secs(MAX_NOTIFICATION_TTL_SECS),
141 disable_sentry: false,
142 kubernetes_memory_path: "/sys/fs/cgroup".to_string(),
146 }
147 }
148}
149
150impl Settings {
151 pub fn with_env_and_config_file(filename: &Option<String>) -> Result<Self, ConfigError> {
153 let mut config = Config::builder();
154
155 if let Some(config_filename) = filename {
157 config = config.add_source(File::with_name(config_filename));
158 }
159
160 config = config.add_source(Environment::with_prefix(ENV_PREFIX).separator("__"));
164
165 let built: Self = config.build()?.try_deserialize::<Self>().map_err(|error| {
166 match error {
167 ConfigError::Message(error_msg) => {
170 println!("Bad configuration: {:?}", &error_msg);
171 println!("Please set in config file or use environment variable.");
172 println!(
173 "For example to set `database_url` use env var `{}_DATABASE_URL`\n",
174 ENV_PREFIX.to_uppercase()
175 );
176 error!("Configuration error: Value undefined {:?}", &error_msg);
177 ConfigError::NotFound(error_msg)
178 }
179 _ => {
180 error!("Configuration error: Other: {:?}", &error);
181 error
182 }
183 }
184 })?;
185
186 Ok(built)
187 }
188
189 fn read_list_from_str<'list>(
192 list_str: &'list str,
193 panic_msg: &'static str,
194 ) -> impl Iterator<Item = &'list str> {
195 if !(list_str.starts_with('[') && list_str.ends_with(']')) {
196 panic!("{}", panic_msg);
197 }
198
199 let items = &list_str[1..list_str.len() - 1];
200 items.split(',')
201 }
202
203 pub fn make_fernet(&self) -> MultiFernet {
205 let keys = &self.crypto_keys.replace(['"', ' '], "");
206 let fernets = Self::read_list_from_str(keys, "Invalid AUTOEND_CRYPTO_KEYS")
207 .map(|key| {
208 debug!("🔐 Fernet keys: {:?}", &key);
209 Fernet::new(key).expect("Invalid AUTOEND_CRYPTO_KEYS")
210 })
211 .collect();
212 MultiFernet::new(fernets)
213 }
214
215 pub fn auth_keys(&self) -> Vec<String> {
217 let keys = &self.auth_keys.replace(['"', ' '], "");
218 Self::read_list_from_str(keys, "Invalid AUTOEND_AUTH_KEYS")
219 .map(|v| v.to_owned())
220 .collect()
221 }
222
223 pub fn tracking_keys(&self) -> Result<Vec<Vec<u8>>, ConfigError> {
228 let keys = &self.tracking_keys.replace(['"', ' '], "");
229 let mut result = Vec::new();
231 for v in Self::read_list_from_str(keys, "Invalid AUTOEND_TRACKING_KEYS") {
232 result.push(
233 util::b64_decode(v)
234 .map_err(|e| ConfigError::Message(format!("Invalid tracking key: {e:?}")))?,
235 );
236 }
237 trace!("🔍 tracking_keys: {result:?}");
238 Ok(result)
239 }
240
241 pub fn endpoint_url(&self) -> Url {
243 let endpoint = if self.endpoint_url.is_empty() {
244 format!("{}://{}:{}", self.scheme, self.host, self.port)
245 } else {
246 self.endpoint_url.clone()
247 };
248 Url::parse(&endpoint).expect("Invalid endpoint URL")
249 }
250}
251
252#[derive(Clone, Debug)]
253pub struct VapidTracker(pub Vec<Vec<u8>>);
254impl VapidTracker {
255 pub fn is_trackable(&self, vapid: &VapidHeaderWithKey) -> bool {
258 let key = match util::b64_decode(&vapid.public_key) {
262 Ok(v) => v,
263 Err(e) => {
264 warn!("🔍 VAPID: tracker failure {e}");
267 return false;
268 }
269 };
270 let result = self.0.contains(&key);
271
272 debug!("🔍 Checking {:?} {}", &vapid.public_key, {
273 if result { "Match!" } else { "no match" }
274 });
275 result
276 }
277
278 pub fn get_id(&self, headers: &HeaderMap) -> String {
280 headers
281 .get("X-MessageId")
282 .and_then(|v|
283 v.to_str().ok())
289 .map(|v| v.to_owned())
290 .unwrap_or_else(|| uuid::Uuid::new_v4().as_simple().to_string())
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use actix_http::header::{HeaderMap, HeaderName, HeaderValue};
297
298 use super::{Settings, VapidTracker};
299 use crate::{
300 error::ApiResult,
301 headers::vapid::{VapidHeader, VapidHeaderWithKey},
302 };
303
304 #[test]
305 fn test_auth_keys() -> ApiResult<()> {
306 let success: Vec<String> = vec![
307 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=".to_owned(),
308 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=".to_owned(),
309 ];
310 let settings = Settings{
312 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC="]"#.to_owned(),
313 ..Default::default()
314 };
315 let result = settings.auth_keys();
316 assert_eq!(result, success);
317
318 let settings = Settings{
320 auth_keys: r#"[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=]"#.to_owned(),
321 ..Default::default()
322 };
323 let result = settings.auth_keys();
324 assert_eq!(result, success);
325 Ok(())
326 }
327
328 #[test]
329 fn test_endpoint_url() -> ApiResult<()> {
330 let example = "https://example.org/";
331 let settings = Settings {
332 endpoint_url: example.to_owned(),
333 ..Default::default()
334 };
335
336 assert_eq!(settings.endpoint_url(), url::Url::parse(example).unwrap());
337 let settings = Settings {
338 ..Default::default()
339 };
340
341 assert_eq!(
342 settings.endpoint_url(),
343 url::Url::parse(&format!(
344 "{}://{}:{}",
345 settings.scheme, settings.host, settings.port
346 ))
347 .unwrap()
348 );
349 Ok(())
350 }
351
352 #[test]
394 fn test_tracking_keys() -> ApiResult<()> {
395 let settings = Settings{
397 tracking_keys: r#"["BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon+H/boHTzMtMoNHsAGDlDB6X"]"#.to_owned(),
398 ..Default::default()
399 };
400
401 let test_header = VapidHeaderWithKey {
402 vapid: VapidHeader {
403 scheme: "".to_owned(),
404 token: "".to_owned(),
405 version_data: crate::headers::vapid::VapidVersionData::Version1,
406 },
407 public_key: "BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon-H_boHTzMtMoNHsAGDlDB6X==".to_owned()
408 };
409
410 let key_set = settings.tracking_keys().unwrap();
411 assert!(!key_set.is_empty());
412
413 let reliability = VapidTracker(key_set);
414 assert!(reliability.is_trackable(&test_header));
415
416 Ok(())
417 }
418
419 #[test]
420 fn test_multi_tracking_keys() -> ApiResult<()> {
421 let settings = Settings{
423 tracking_keys: r#"[BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg,BHDgfiL1hz4oIBFaxxS9jkzyAVing-W9jjt_7WUeFjWS5Invalid5EjC8TQKddJNP3iow7UW6u8JE3t7u_y3Plc]"#.to_owned(),
424 ..Default::default()
425 };
426
427 let test_header = VapidHeaderWithKey {
428 vapid: VapidHeader {
429 scheme: "".to_owned(),
430 token: "".to_owned(),
431 version_data: crate::headers::vapid::VapidVersionData::Version1,
432 },
433 public_key: "BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg".to_owned()
434 };
435
436 let key_set = settings.tracking_keys().unwrap();
437 assert!(!key_set.is_empty());
438
439 let reliability = VapidTracker(key_set);
440 assert!(reliability.is_trackable(&test_header));
441
442 Ok(())
443 }
444
445 #[test]
446 fn test_reliability_id() -> ApiResult<()> {
447 let mut headers = HeaderMap::new();
448 let keys = Vec::new();
449 let reliability = VapidTracker(keys);
450
451 let key = reliability.get_id(&headers);
452 assert!(!key.is_empty());
453
454 headers.insert(
455 HeaderName::from_lowercase(b"x-messageid").unwrap(),
456 HeaderValue::from_static("123foobar456"),
457 );
458
459 let key = reliability.get_id(&headers);
460 assert_eq!(key, "123foobar456".to_owned());
461
462 Ok(())
463 }
464}