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,
26 pub host: String,
27 pub port: u16,
28 pub endpoint_url: String,
29
30 pub db_dsn: Option<String>,
32 pub db_settings: String,
34
35 pub router_table_name: String,
36 pub message_table_name: String,
37
38 pub tracking_keys: String,
45
46 pub max_data_bytes: usize,
47 pub crypto_keys: String,
48 pub auth_keys: String,
49 pub human_logs: bool,
50
51 pub connection_timeout_millis: u64,
52 pub request_timeout_millis: u64,
53
54 pub statsd_host: Option<String>,
55 pub statsd_port: u16,
56 pub statsd_label: String,
57
58 pub fcm: FcmSettings,
59 pub apns: ApnsSettings,
60 #[cfg(feature = "stub")]
61 pub stub: StubSettings,
64 #[cfg(feature = "reliable_report")]
65 pub reliability_dsn: Option<String>,
69 #[cfg(feature = "reliable_report")]
70 pub reliability_retry_count: usize,
72 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
74 pub max_notification_ttl: Duration,
75}
76
77impl Default for Settings {
78 fn default() -> Settings {
79 Settings {
80 scheme: "http".to_string(),
81 host: "127.0.0.1".to_string(),
82 endpoint_url: "".to_string(),
83 port: 8000,
84 db_dsn: None,
85 db_settings: "".to_owned(),
86 router_table_name: "router".to_string(),
87 message_table_name: "message".to_string(),
88 max_data_bytes: 5630,
93 crypto_keys: format!("[{}]", Fernet::generate_key()),
94 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB="]"#.to_string(),
95 tracking_keys: r#"[]"#.to_string(),
96 human_logs: false,
97 connection_timeout_millis: 1000,
98 request_timeout_millis: 3000,
99 statsd_host: None,
100 statsd_port: 8125,
101 statsd_label: "autoendpoint".to_string(),
102 fcm: FcmSettings::default(),
103 apns: ApnsSettings::default(),
104 #[cfg(feature = "stub")]
105 stub: StubSettings::default(),
106 #[cfg(feature = "reliable_report")]
107 reliability_dsn: None,
108 #[cfg(feature = "reliable_report")]
109 reliability_retry_count: autopush_common::redis_util::MAX_TRANSACTION_LOOP,
110 max_notification_ttl: Duration::from_secs(MAX_NOTIFICATION_TTL_SECS),
111 }
112 }
113}
114
115impl Settings {
116 pub fn with_env_and_config_file(filename: &Option<String>) -> Result<Self, ConfigError> {
118 let mut config = Config::builder();
119
120 if let Some(config_filename) = filename {
122 config = config.add_source(File::with_name(config_filename));
123 }
124
125 config = config.add_source(Environment::with_prefix(ENV_PREFIX).separator("__"));
129
130 let built: Self = config.build()?.try_deserialize::<Self>().map_err(|error| {
131 match error {
132 ConfigError::Message(error_msg) => {
135 println!("Bad configuration: {:?}", &error_msg);
136 println!("Please set in config file or use environment variable.");
137 println!(
138 "For example to set `database_url` use env var `{}_DATABASE_URL`\n",
139 ENV_PREFIX.to_uppercase()
140 );
141 error!("Configuration error: Value undefined {:?}", &error_msg);
142 ConfigError::NotFound(error_msg)
143 }
144 _ => {
145 error!("Configuration error: Other: {:?}", &error);
146 error
147 }
148 }
149 })?;
150
151 Ok(built)
152 }
153
154 fn read_list_from_str<'list>(
157 list_str: &'list str,
158 panic_msg: &'static str,
159 ) -> impl Iterator<Item = &'list str> {
160 if !(list_str.starts_with('[') && list_str.ends_with(']')) {
161 panic!("{}", panic_msg);
162 }
163
164 let items = &list_str[1..list_str.len() - 1];
165 items.split(',')
166 }
167
168 pub fn make_fernet(&self) -> MultiFernet {
170 let keys = &self.crypto_keys.replace(['"', ' '], "");
171 let fernets = Self::read_list_from_str(keys, "Invalid AUTOEND_CRYPTO_KEYS")
172 .map(|key| {
173 debug!("🔐 Fernet keys: {:?}", &key);
174 Fernet::new(key).expect("Invalid AUTOEND_CRYPTO_KEYS")
175 })
176 .collect();
177 MultiFernet::new(fernets)
178 }
179
180 pub fn auth_keys(&self) -> Vec<String> {
182 let keys = &self.auth_keys.replace(['"', ' '], "");
183 Self::read_list_from_str(keys, "Invalid AUTOEND_AUTH_KEYS")
184 .map(|v| v.to_owned())
185 .collect()
186 }
187
188 pub fn tracking_keys(&self) -> Result<Vec<Vec<u8>>, ConfigError> {
193 let keys = &self.tracking_keys.replace(['"', ' '], "");
194 let mut result = Vec::new();
196 for v in Self::read_list_from_str(keys, "Invalid AUTOEND_TRACKING_KEYS") {
197 result.push(
198 util::b64_decode(v)
199 .map_err(|e| ConfigError::Message(format!("Invalid tracking key: {e:?}")))?,
200 );
201 }
202 trace!("🔍 tracking_keys: {result:?}");
203 Ok(result)
204 }
205
206 pub fn endpoint_url(&self) -> Url {
208 let endpoint = if self.endpoint_url.is_empty() {
209 format!("{}://{}:{}", self.scheme, self.host, self.port)
210 } else {
211 self.endpoint_url.clone()
212 };
213 Url::parse(&endpoint).expect("Invalid endpoint URL")
214 }
215}
216
217#[derive(Clone, Debug)]
218pub struct VapidTracker(pub Vec<Vec<u8>>);
219impl VapidTracker {
220 pub fn is_trackable(&self, vapid: &VapidHeaderWithKey) -> bool {
223 let key = match util::b64_decode(&vapid.public_key) {
227 Ok(v) => v,
228 Err(e) => {
229 warn!("🔍 VAPID: tracker failure {e}");
232 return false;
233 }
234 };
235 let result = self.0.contains(&key);
236
237 debug!("🔍 Checking {:?} {}", &vapid.public_key, {
238 if result {
239 "Match!"
240 } else {
241 "no match"
242 }
243 });
244 result
245 }
246
247 pub fn get_id(&self, headers: &HeaderMap) -> String {
249 headers
250 .get("X-MessageId")
251 .and_then(|v|
252 v.to_str().ok())
258 .map(|v| v.to_owned())
259 .unwrap_or_else(|| uuid::Uuid::new_v4().as_simple().to_string())
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use actix_http::header::{HeaderMap, HeaderName, HeaderValue};
266
267 use super::{Settings, VapidTracker};
268 use crate::{
269 error::ApiResult,
270 headers::vapid::{VapidHeader, VapidHeaderWithKey},
271 };
272
273 #[test]
274 fn test_auth_keys() -> ApiResult<()> {
275 let success: Vec<String> = vec![
276 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=".to_owned(),
277 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=".to_owned(),
278 ];
279 let settings = Settings{
281 auth_keys: r#"["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC="]"#.to_owned(),
282 ..Default::default()
283 };
284 let result = settings.auth_keys();
285 assert_eq!(result, success);
286
287 let settings = Settings{
289 auth_keys: r#"[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=]"#.to_owned(),
290 ..Default::default()
291 };
292 let result = settings.auth_keys();
293 assert_eq!(result, success);
294 Ok(())
295 }
296
297 #[test]
298 fn test_endpoint_url() -> ApiResult<()> {
299 let example = "https://example.org/";
300 let settings = Settings {
301 endpoint_url: example.to_owned(),
302 ..Default::default()
303 };
304
305 assert_eq!(settings.endpoint_url(), url::Url::parse(example).unwrap());
306 let settings = Settings {
307 ..Default::default()
308 };
309
310 assert_eq!(
311 settings.endpoint_url(),
312 url::Url::parse(&format!(
313 "{}://{}:{}",
314 settings.scheme, settings.host, settings.port
315 ))
316 .unwrap()
317 );
318 Ok(())
319 }
320
321 #[test]
363 fn test_tracking_keys() -> ApiResult<()> {
364 let settings = Settings{
366 tracking_keys: r#"["BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon+H/boHTzMtMoNHsAGDlDB6X"]"#.to_owned(),
367 ..Default::default()
368 };
369
370 let test_header = VapidHeaderWithKey {
371 vapid: VapidHeader {
372 scheme: "".to_owned(),
373 token: "".to_owned(),
374 version_data: crate::headers::vapid::VapidVersionData::Version1,
375 },
376 public_key: "BLMymkOqvT6OZ1o9etCqV4jGPkvOXNz5FdBjsAR9zR5oeCV1x5CBKuSLTlHon-H_boHTzMtMoNHsAGDlDB6X==".to_owned()
377 };
378
379 let key_set = settings.tracking_keys().unwrap();
380 assert!(!key_set.is_empty());
381
382 let reliability = VapidTracker(key_set);
383 assert!(reliability.is_trackable(&test_header));
384
385 Ok(())
386 }
387
388 #[test]
389 fn test_multi_tracking_keys() -> ApiResult<()> {
390 let settings = Settings{
392 tracking_keys: r#"[BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg,BHDgfiL1hz4oIBFaxxS9jkzyAVing-W9jjt_7WUeFjWS5Invalid5EjC8TQKddJNP3iow7UW6u8JE3t7u_y3Plc]"#.to_owned(),
393 ..Default::default()
394 };
395
396 let test_header = VapidHeaderWithKey {
397 vapid: VapidHeader {
398 scheme: "".to_owned(),
399 token: "".to_owned(),
400 version_data: crate::headers::vapid::VapidVersionData::Version1,
401 },
402 public_key: "BLbZTvXsQr0rdvLQr73ETRcseSpoof5xV83NiPK9U-Qi00DjNJct1N6EZtTBMD0uh-nNjtLAxik1XP9CZXrKtTg".to_owned()
403 };
404
405 let key_set = settings.tracking_keys().unwrap();
406 assert!(!key_set.is_empty());
407
408 let reliability = VapidTracker(key_set);
409 assert!(reliability.is_trackable(&test_header));
410
411 Ok(())
412 }
413
414 #[test]
415 fn test_reliability_id() -> ApiResult<()> {
416 let mut headers = HeaderMap::new();
417 let keys = Vec::new();
418 let reliability = VapidTracker(keys);
419
420 let key = reliability.get_id(&headers);
421 assert!(!key.is_empty());
422
423 headers.insert(
424 HeaderName::from_lowercase(b"x-messageid").unwrap(),
425 HeaderValue::from_static("123foobar456"),
426 );
427
428 let key = reliability.get_id(&headers);
429 assert_eq!(key, "123foobar456".to_owned());
430
431 Ok(())
432 }
433}