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 pub log_chan_size: usize,
66
67 pub connection_timeout_millis: u64,
69 pub request_timeout_millis: u64,
71 pub pool_max_idle_per_host: usize,
73 pub pool_idle_timeout_secs: u64,
75
76 pub statsd_host: Option<String>,
78 pub statsd_port: u16,
80 pub statsd_label: String,
82
83 pub disable_sentry: bool,
85
86 pub fcm: FcmSettings,
88 pub apns: ApnsSettings,
90 #[cfg(feature = "stub")]
91 pub stub: StubSettings,
94 #[cfg(feature = "reliable_report")]
95 pub reliability_dsn: Option<String>,
99 #[cfg(feature = "reliable_report")]
100 pub reliability_retry_count: usize,
102 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
104 pub max_notification_ttl: Duration,
105 pub kubernetes_memory_path: Option<String>,
107}
108impl Default for Settings {
111 fn default() -> Settings {
112 Settings {
113 scheme: "http".to_string(),
114 host: "127.0.0.1".to_string(),
115 endpoint_url: "".to_string(),
116 port: 8000,
117 db_dsn: None,
118 db_settings: "".to_owned(),
119 router_table_name: "router".to_string(),
120 message_table_name: "message".to_string(),
121 max_data_bytes: 5630,
126 crypto_keys: format!("[{}]", Fernet::generate_key()),
127 auth_keys: r#"[]"#.to_string(),
128 tracking_keys: r#"[]"#.to_string(),
129 human_logs: false,
130 log_chan_size: autopush_common::logging::DEFAULT_LOG_CHAN_SIZE,
131 connection_timeout_millis: 1000,
132 request_timeout_millis: 3000,
133 pool_max_idle_per_host: 10,
134 pool_idle_timeout_secs: 30,
135 statsd_host: None,
136 statsd_port: 8125,
137 statsd_label: "autoendpoint".to_string(),
138 fcm: FcmSettings::default(),
139 apns: ApnsSettings::default(),
140 #[cfg(feature = "stub")]
141 stub: StubSettings::default(),
142 #[cfg(feature = "reliable_report")]
143 reliability_dsn: None,
144 #[cfg(feature = "reliable_report")]
145 reliability_retry_count: autopush_common::redis_util::MAX_TRANSACTION_LOOP,
146 max_notification_ttl: Duration::from_secs(MAX_NOTIFICATION_TTL_SECS),
147 disable_sentry: false,
148 kubernetes_memory_path: None,
152 }
153 }
154}
155
156impl Settings {
157 pub fn with_env_and_config_file(filename: &Option<String>) -> Result<Self, ConfigError> {
159 let mut config = Config::builder();
160
161 if let Some(config_filename) = filename {
163 config = config.add_source(File::with_name(config_filename));
164 }
165
166 config = config.add_source(Environment::with_prefix(ENV_PREFIX).separator("__"));
170
171 let built: Self = config.build()?.try_deserialize::<Self>().map_err(|error| {
172 match error {
173 ConfigError::Message(error_msg) => {
176 println!("Bad configuration: {:?}", &error_msg);
177 println!("Please set in config file or use environment variable.");
178 println!(
179 "For example to set `database_url` use env var `{}_DATABASE_URL`\n",
180 ENV_PREFIX.to_uppercase()
181 );
182 error!("Configuration error: Value undefined {:?}", &error_msg);
183 ConfigError::NotFound(error_msg)
184 }
185 _ => {
186 error!("Configuration error: Other: {:?}", &error);
187 error
188 }
189 }
190 })?;
191 built.validate_auth_keys()?;
194 Ok(built)
195 }
196
197 fn read_list_from_str<'list>(
200 list_str: &'list str,
201 panic_msg: &'static str,
202 ) -> impl Iterator<Item = &'list str> {
203 if !(list_str.starts_with('[') && list_str.ends_with(']')) {
204 panic!("{}", panic_msg);
205 }
206
207 let items = &list_str[1..list_str.len() - 1];
208 items.split(',')
209 }
210
211 pub fn make_fernet(&self) -> MultiFernet {
213 let keys = &self.crypto_keys.replace(['"', ' '], "");
214 let fernets = Self::read_list_from_str(keys, "Invalid AUTOEND_CRYPTO_KEYS")
215 .map(|key| {
216 debug!("🔐 Fernet keys: {:?}", &key);
217 Fernet::new(key).expect("Invalid AUTOEND_CRYPTO_KEYS")
218 })
219 .collect();
220 MultiFernet::new(fernets)
221 }
222
223 pub fn auth_keys(&self) -> Vec<String> {
225 let keys = &self.auth_keys.replace(['"', ' '], "");
226 Self::read_list_from_str(keys, "Invalid AUTOEND_AUTH_KEYS")
227 .map(|v| v.to_owned())
228 .collect()
229 }
230 fn validate_auth_keys(&self) -> Result<(), ConfigError> {
232 if self.auth_keys().iter().all(|key| key.is_empty()) {
233 return Err(ConfigError::Message(
234 "AUTOEND__AUTH_KEYS must contain at least one non-empty key".to_owned(),
235 ));
236 }
237 Ok(())
238 }
239
240 pub fn tracking_keys(&self) -> Result<Vec<Vec<u8>>, ConfigError> {
245 let keys = &self.tracking_keys.replace(['"', ' '], "");
246 let mut result = Vec::new();
248 for v in Self::read_list_from_str(keys, "Invalid AUTOEND_TRACKING_KEYS") {
249 result.push(
250 util::b64_decode(v)
251 .map_err(|e| ConfigError::Message(format!("Invalid tracking key: {e:?}")))?,
252 );
253 }
254 trace!("🔍 tracking_keys: {result:?}");
255 Ok(result)
256 }
257
258 pub fn endpoint_url(&self) -> Url {
260 let endpoint = if self.endpoint_url.is_empty() {
261 format!("{}://{}:{}", self.scheme, self.host, self.port)
262 } else {
263 self.endpoint_url.clone()
264 };
265 Url::parse(&endpoint).expect("Invalid endpoint URL")
266 }
267}
268
269#[derive(Clone, Debug)]
270pub struct VapidTracker(pub Vec<Vec<u8>>);
271impl VapidTracker {
272 pub fn is_trackable(&self, vapid: &VapidHeaderWithKey) -> bool {
275 let key = match util::b64_decode(&vapid.public_key) {
279 Ok(v) => v,
280 Err(e) => {
281 warn!("🔍 VAPID: tracker failure {e}");
284 return false;
285 }
286 };
287 let result = self.0.contains(&key);
288
289 debug!("🔍 Checking {:?} {}", &vapid.public_key, {
290 if result { "Match!" } else { "no match" }
291 });
292 result
293 }
294
295 pub fn get_id(&self, headers: &HeaderMap) -> String {
297 headers
298 .get("X-MessageId")
299 .and_then(|v|
300 v.to_str().ok())
306 .map(|v| v.to_owned())
307 .unwrap_or_else(|| uuid::Uuid::new_v4().as_simple().to_string())
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use actix_http::header::{HeaderMap, HeaderName, HeaderValue};
314
315 use super::{Settings, VapidTracker};
316 use crate::{
317 error::ApiResult,
318 headers::vapid::{VapidHeader, VapidHeaderWithKey},
319 };
320
321 #[test]
322 fn test_auth_keys() -> ApiResult<()> {
323 let success: Vec<String> = vec![
324 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=".to_owned(),
325 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=".to_owned(),
326 ];
327 let settings = Settings{
329 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC="]"#.to_owned(),
330 ..Default::default()
331 };
332 let result = settings.auth_keys();
333 assert_eq!(result, success);
334
335 let settings = Settings{
337 auth_keys: r#"[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=]"#.to_owned(),
338 ..Default::default()
339 };
340 let result = settings.auth_keys();
341 assert_eq!(result, success);
342 Ok(())
343 }
344 #[test]
345 fn test_auth_keys_rejects() -> ApiResult<()> {
346 assert!(Settings::default().validate_auth_keys().is_err());
348 let settings = Settings {
350 auth_keys: r#"[]"#.to_owned(),
351 ..Default::default()
352 };
353 assert!(settings.validate_auth_keys().is_err());
354 let settings = Settings {
356 auth_keys: r#"["", ""]"#.to_owned(),
357 ..Default::default()
358 };
359 assert!(settings.validate_auth_keys().is_err());
360 let settings = Settings {
362 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB="]"#.to_owned(),
363 ..Default::default()
364 };
365 assert!(settings.validate_auth_keys().is_ok());
366 Ok(())
367 }
368
369 #[test]
370 fn test_endpoint_url() -> ApiResult<()> {
371 let example = "https://example.org/";
372 let settings = Settings {
373 endpoint_url: example.to_owned(),
374 ..Default::default()
375 };
376
377 assert_eq!(settings.endpoint_url(), url::Url::parse(example).unwrap());
378 let settings = Settings {
379 ..Default::default()
380 };
381
382 assert_eq!(
383 settings.endpoint_url(),
384 url::Url::parse(&format!(
385 "{}://{}:{}",
386 settings.scheme, settings.host, settings.port
387 ))
388 .unwrap()
389 );
390 Ok(())
391 }
392
393 #[test]
435 fn test_tracking_keys() -> ApiResult<()> {
436 let settings = Settings{
438 tracking_keys: r#"["BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon+H/boHTzMtMoNHsAGDlDB6X"]"#.to_owned(),
439 ..Default::default()
440 };
441
442 let test_header = VapidHeaderWithKey {
443 vapid: VapidHeader {
444 scheme: "".to_owned(),
445 token: "".to_owned(),
446 version_data: crate::headers::vapid::VapidVersionData::Version1,
447 },
448 public_key: "BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon-H_boHTzMtMoNHsAGDlDB6X==".to_owned()
449 };
450
451 let key_set = settings.tracking_keys().unwrap();
452 assert!(!key_set.is_empty());
453
454 let reliability = VapidTracker(key_set);
455 assert!(reliability.is_trackable(&test_header));
456
457 Ok(())
458 }
459
460 #[test]
461 fn test_multi_tracking_keys() -> ApiResult<()> {
462 let settings = Settings{
464 tracking_keys: r#"[BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg,BHDgfiL1hz4oIBFaxxS9jkzyAVing-W9jjt_7WUeFjWS5Invalid5EjC8TQKddJNP3iow7UW6u8JE3t7u_y3Plc]"#.to_owned(),
465 ..Default::default()
466 };
467
468 let test_header = VapidHeaderWithKey {
469 vapid: VapidHeader {
470 scheme: "".to_owned(),
471 token: "".to_owned(),
472 version_data: crate::headers::vapid::VapidVersionData::Version1,
473 },
474 public_key: "BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg".to_owned()
475 };
476
477 let key_set = settings.tracking_keys().unwrap();
478 assert!(!key_set.is_empty());
479
480 let reliability = VapidTracker(key_set);
481 assert!(reliability.is_trackable(&test_header));
482
483 Ok(())
484 }
485
486 #[test]
487 fn test_reliability_id() -> ApiResult<()> {
488 let mut headers = HeaderMap::new();
489 let keys = Vec::new();
490 let reliability = VapidTracker(keys);
491
492 let key = reliability.get_id(&headers);
493 assert!(!key.is_empty());
494
495 headers.insert(
496 HeaderName::from_lowercase(b"x-messageid").unwrap(),
497 HeaderValue::from_static("123foobar456"),
498 );
499
500 let key = reliability.get_id(&headers);
501 assert_eq!(key, "123foobar456".to_owned());
502
503 Ok(())
504 }
505}