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::{util, MAX_NOTIFICATION_TTL_SECS};
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}
101impl Default for Settings {
104 fn default() -> Settings {
105 Settings {
106 scheme: "http".to_string(),
107 host: "127.0.0.1".to_string(),
108 endpoint_url: "".to_string(),
109 port: 8000,
110 db_dsn: None,
111 db_settings: "".to_owned(),
112 router_table_name: "router".to_string(),
113 message_table_name: "message".to_string(),
114 max_data_bytes: 5630,
119 crypto_keys: format!("[{}]", Fernet::generate_key()),
120 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB="]"#.to_string(),
121 tracking_keys: r#"[]"#.to_string(),
122 human_logs: false,
123 connection_timeout_millis: 1000,
124 request_timeout_millis: 3000,
125 pool_max_idle_per_host: 10,
126 pool_idle_timeout_secs: 30,
127 statsd_host: None,
128 statsd_port: 8125,
129 statsd_label: "autoendpoint".to_string(),
130 fcm: FcmSettings::default(),
131 apns: ApnsSettings::default(),
132 #[cfg(feature = "stub")]
133 stub: StubSettings::default(),
134 #[cfg(feature = "reliable_report")]
135 reliability_dsn: None,
136 #[cfg(feature = "reliable_report")]
137 reliability_retry_count: autopush_common::redis_util::MAX_TRANSACTION_LOOP,
138 max_notification_ttl: Duration::from_secs(MAX_NOTIFICATION_TTL_SECS),
139 disable_sentry: false,
140 }
141 }
142}
143
144impl Settings {
145 pub fn with_env_and_config_file(filename: &Option<String>) -> Result<Self, ConfigError> {
147 let mut config = Config::builder();
148
149 if let Some(config_filename) = filename {
151 config = config.add_source(File::with_name(config_filename));
152 }
153
154 config = config.add_source(Environment::with_prefix(ENV_PREFIX).separator("__"));
158
159 let built: Self = config.build()?.try_deserialize::<Self>().map_err(|error| {
160 match error {
161 ConfigError::Message(error_msg) => {
164 println!("Bad configuration: {:?}", &error_msg);
165 println!("Please set in config file or use environment variable.");
166 println!(
167 "For example to set `database_url` use env var `{}_DATABASE_URL`\n",
168 ENV_PREFIX.to_uppercase()
169 );
170 error!("Configuration error: Value undefined {:?}", &error_msg);
171 ConfigError::NotFound(error_msg)
172 }
173 _ => {
174 error!("Configuration error: Other: {:?}", &error);
175 error
176 }
177 }
178 })?;
179
180 Ok(built)
181 }
182
183 fn read_list_from_str<'list>(
186 list_str: &'list str,
187 panic_msg: &'static str,
188 ) -> impl Iterator<Item = &'list str> {
189 if !(list_str.starts_with('[') && list_str.ends_with(']')) {
190 panic!("{}", panic_msg);
191 }
192
193 let items = &list_str[1..list_str.len() - 1];
194 items.split(',')
195 }
196
197 pub fn make_fernet(&self) -> MultiFernet {
199 let keys = &self.crypto_keys.replace(['"', ' '], "");
200 let fernets = Self::read_list_from_str(keys, "Invalid AUTOEND_CRYPTO_KEYS")
201 .map(|key| {
202 debug!("🔐 Fernet keys: {:?}", &key);
203 Fernet::new(key).expect("Invalid AUTOEND_CRYPTO_KEYS")
204 })
205 .collect();
206 MultiFernet::new(fernets)
207 }
208
209 pub fn auth_keys(&self) -> Vec<String> {
211 let keys = &self.auth_keys.replace(['"', ' '], "");
212 Self::read_list_from_str(keys, "Invalid AUTOEND_AUTH_KEYS")
213 .map(|v| v.to_owned())
214 .collect()
215 }
216
217 pub fn tracking_keys(&self) -> Result<Vec<Vec<u8>>, ConfigError> {
222 let keys = &self.tracking_keys.replace(['"', ' '], "");
223 let mut result = Vec::new();
225 for v in Self::read_list_from_str(keys, "Invalid AUTOEND_TRACKING_KEYS") {
226 result.push(
227 util::b64_decode(v)
228 .map_err(|e| ConfigError::Message(format!("Invalid tracking key: {e:?}")))?,
229 );
230 }
231 trace!("🔍 tracking_keys: {result:?}");
232 Ok(result)
233 }
234
235 pub fn endpoint_url(&self) -> Url {
237 let endpoint = if self.endpoint_url.is_empty() {
238 format!("{}://{}:{}", self.scheme, self.host, self.port)
239 } else {
240 self.endpoint_url.clone()
241 };
242 Url::parse(&endpoint).expect("Invalid endpoint URL")
243 }
244}
245
246#[derive(Clone, Debug)]
247pub struct VapidTracker(pub Vec<Vec<u8>>);
248impl VapidTracker {
249 pub fn is_trackable(&self, vapid: &VapidHeaderWithKey) -> bool {
252 let key = match util::b64_decode(&vapid.public_key) {
256 Ok(v) => v,
257 Err(e) => {
258 warn!("🔍 VAPID: tracker failure {e}");
261 return false;
262 }
263 };
264 let result = self.0.contains(&key);
265
266 debug!("🔍 Checking {:?} {}", &vapid.public_key, {
267 if result {
268 "Match!"
269 } else {
270 "no match"
271 }
272 });
273 result
274 }
275
276 pub fn get_id(&self, headers: &HeaderMap) -> String {
278 headers
279 .get("X-MessageId")
280 .and_then(|v|
281 v.to_str().ok())
287 .map(|v| v.to_owned())
288 .unwrap_or_else(|| uuid::Uuid::new_v4().as_simple().to_string())
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use actix_http::header::{HeaderMap, HeaderName, HeaderValue};
295
296 use super::{Settings, VapidTracker};
297 use crate::{
298 error::ApiResult,
299 headers::vapid::{VapidHeader, VapidHeaderWithKey},
300 };
301
302 #[test]
303 fn test_auth_keys() -> ApiResult<()> {
304 let success: Vec<String> = vec![
305 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=".to_owned(),
306 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=".to_owned(),
307 ];
308 let settings = Settings{
310 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC="]"#.to_owned(),
311 ..Default::default()
312 };
313 let result = settings.auth_keys();
314 assert_eq!(result, success);
315
316 let settings = Settings{
318 auth_keys: r#"[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=]"#.to_owned(),
319 ..Default::default()
320 };
321 let result = settings.auth_keys();
322 assert_eq!(result, success);
323 Ok(())
324 }
325
326 #[test]
327 fn test_endpoint_url() -> ApiResult<()> {
328 let example = "https://example.org/";
329 let settings = Settings {
330 endpoint_url: example.to_owned(),
331 ..Default::default()
332 };
333
334 assert_eq!(settings.endpoint_url(), url::Url::parse(example).unwrap());
335 let settings = Settings {
336 ..Default::default()
337 };
338
339 assert_eq!(
340 settings.endpoint_url(),
341 url::Url::parse(&format!(
342 "{}://{}:{}",
343 settings.scheme, settings.host, settings.port
344 ))
345 .unwrap()
346 );
347 Ok(())
348 }
349
350 #[test]
392 fn test_tracking_keys() -> ApiResult<()> {
393 let settings = Settings{
395 tracking_keys: r#"["BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon+H/boHTzMtMoNHsAGDlDB6X"]"#.to_owned(),
396 ..Default::default()
397 };
398
399 let test_header = VapidHeaderWithKey {
400 vapid: VapidHeader {
401 scheme: "".to_owned(),
402 token: "".to_owned(),
403 version_data: crate::headers::vapid::VapidVersionData::Version1,
404 },
405 public_key: "BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon-H_boHTzMtMoNHsAGDlDB6X==".to_owned()
406 };
407
408 let key_set = settings.tracking_keys().unwrap();
409 assert!(!key_set.is_empty());
410
411 let reliability = VapidTracker(key_set);
412 assert!(reliability.is_trackable(&test_header));
413
414 Ok(())
415 }
416
417 #[test]
418 fn test_multi_tracking_keys() -> ApiResult<()> {
419 let settings = Settings{
421 tracking_keys: r#"[BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg,BHDgfiL1hz4oIBFaxxS9jkzyAVing-W9jjt_7WUeFjWS5Invalid5EjC8TQKddJNP3iow7UW6u8JE3t7u_y3Plc]"#.to_owned(),
422 ..Default::default()
423 };
424
425 let test_header = VapidHeaderWithKey {
426 vapid: VapidHeader {
427 scheme: "".to_owned(),
428 token: "".to_owned(),
429 version_data: crate::headers::vapid::VapidVersionData::Version1,
430 },
431 public_key: "BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg".to_owned()
432 };
433
434 let key_set = settings.tracking_keys().unwrap();
435 assert!(!key_set.is_empty());
436
437 let reliability = VapidTracker(key_set);
438 assert!(reliability.is_trackable(&test_header));
439
440 Ok(())
441 }
442
443 #[test]
444 fn test_reliability_id() -> ApiResult<()> {
445 let mut headers = HeaderMap::new();
446 let keys = Vec::new();
447 let reliability = VapidTracker(keys);
448
449 let key = reliability.get_id(&headers);
450 assert!(!key.is_empty());
451
452 headers.insert(
453 HeaderName::from_lowercase(b"x-messageid").unwrap(),
454 HeaderValue::from_static("123foobar456"),
455 );
456
457 let key = reliability.get_id(&headers);
458 assert_eq!(key, "123foobar456".to_owned());
459
460 Ok(())
461 }
462}