diff --git a/services/tunnelbroker/src/notifs/apns/mod.rs b/services/tunnelbroker/src/notifs/apns/mod.rs index 84e5d8851..cf39d1710 100644 --- a/services/tunnelbroker/src/notifs/apns/mod.rs +++ b/services/tunnelbroker/src/notifs/apns/mod.rs @@ -1,88 +1,89 @@ use crate::notifs::apns::config::APNsConfig; use crate::notifs::apns::headers::{NotificationHeaders, PushType}; use crate::notifs::apns::token::APNsToken; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use std::time::Duration; pub mod config; pub mod error; mod headers; +mod response; pub mod token; #[derive(Clone)] pub struct APNsClient { http2_client: reqwest::Client, token: APNsToken, is_prod: bool, } impl APNsClient { pub fn new(config: &APNsConfig) -> Result { let token_ttl = Duration::from_secs(60 * 55); let token = APNsToken::new(config, token_ttl)?; let http2_client = reqwest::Client::builder() .http2_prior_knowledge() .http2_keep_alive_interval(Some(Duration::from_secs(5))) .http2_keep_alive_while_idle(true) .build()?; Ok(APNsClient { http2_client, token, is_prod: config.production, }) } async fn build_headers( &self, notif_headers: NotificationHeaders, ) -> Result { let mut headers = HeaderMap::new(); headers.insert( reqwest::header::CONTENT_TYPE, HeaderValue::from_static("application/json"), ); let bearer = self.token.get_bearer().await?; let token = format!("bearer {bearer}"); headers.insert(AUTHORIZATION, HeaderValue::from_str(&token)?); if let Some(apns_topic) = ¬if_headers.apns_topic { headers.insert("apns-topic", HeaderValue::from_str(apns_topic)?); } if let Some(apns_id) = ¬if_headers.apns_id { headers.insert("apns-id", HeaderValue::from_str(apns_id)?); } if let Some(push_type) = ¬if_headers.apns_push_type { let push_type_str = match push_type { PushType::Alert => "alert", PushType::Background => "background", PushType::Location => "location", PushType::Voip => "voip", PushType::Complication => "complication", PushType::FileProvider => "fileprovider", PushType::Mdm => "mdm", PushType::LiveActivity => "live", PushType::PushToTalk => "pushtotalk", }; headers.insert("apns-push-type", HeaderValue::from_static(push_type_str)); } if let Some(expiration) = notif_headers.apns_expiration { headers.insert("apns-expiration", HeaderValue::from(expiration)); } if let Some(priority) = notif_headers.apns_priority { headers.insert("apns-priority", HeaderValue::from(priority)); } if let Some(collapse_id) = ¬if_headers.apns_collapse_id { headers.insert("apns-collapse-id", HeaderValue::from_str(collapse_id)?); } Ok(headers) } } diff --git a/services/tunnelbroker/src/notifs/apns/response.rs b/services/tunnelbroker/src/notifs/apns/response.rs new file mode 100644 index 000000000..ed2a9557b --- /dev/null +++ b/services/tunnelbroker/src/notifs/apns/response.rs @@ -0,0 +1,116 @@ +use derive_more::Error; +use serde::Deserialize; + +/// The response body from APNs, only for errors. +/// Apple docs: https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns +#[derive(Deserialize, Debug, PartialEq, Eq, Error)] +pub struct ErrorBody { + /// The error code (specified as a string) indicating the reason for + /// the failure. + pub reason: ErrorReason, + + /// The time, represented in milliseconds since Epoch, at which APNs + /// confirmed the token was no longer valid for the topic. + pub timestamp: Option, +} + +impl std::fmt::Display for ErrorBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.reason) + } +} + +#[derive(Deserialize, Debug, PartialEq, Eq, derive_more::Display, Error)] +pub enum ErrorReason { + /// The collapse identifier exceeds the maximum allowed size. + BadCollapseId, + + /// The specified device token was bad. Verify that the request contains a + /// valid token and that the token matches the environment. + BadDeviceToken, + + /// The apns-expiration value is invalid. + BadExpirationDate, + + /// The apns-id value is invalid. + BadMessageId, + + /// The apns-priority value is invalid. + BadPriority, + + /// The apns-priority value is invalid. + BadTopic, + + /// The device token does not match the specified topic. + DeviceTokenNotForTopic, + + /// One or more headers were repeated. + DuplicateHeaders, + + /// Idle time out. + IdleTimeout, + + /// The apns-push-type value is invalid. + InvalidPushType, + + /// The device token is not specified in the payload. + MissingDeviceToken, + + /// The apns-topic header of the request isn’t specified and is required. + /// The apns-topic header is mandatory when the client is connected using + /// a certificate that supports multiple topics. + MissingTopic, + + /// The message payload was empty. + PayloadEmpty, + + /// Pushing to this topic is not allowed. + TopicDisallowed, + + /// The certificate was bad. + BadCertificate, + + /// The client certificate was for the wrong environment. + BadCertificateEnvironment, + + /// The provider token is stale and a new token should be generated. + ExpiredProviderToken, + + /// The specified action is not allowed. + Forbidden, + + /// The provider token is not valid or the token signature could + /// not be verified. + InvalidProviderToken, + + ///No provider certificate was used to connect to APNs, and the + /// authorization header is missing or no provider token is specified. + MissingProviderToken, + + /// The request path value is bad. + BadPath, + + /// The request method was not `POST`. + MethodNotAllowed, + + /// The device token is inactive for the specified topic. + Unregistered, + + /// The message payload was too large. + PayloadTooLarge, + + /// The provider token is being updated too often. + TooManyProviderTokenUpdates, + + /// Too many requests were made consecutively to the same device token. + TooManyRequests, + + /// An internal server error occurred. + InternalServerError, + + /// The service is unavailable. + ServiceUnavailable, + + /// The server is shutting down. + Shutdown, +}