diff --git a/services/tunnelbroker/src/notifs/fcm/error.rs b/services/tunnelbroker/src/notifs/fcm/error.rs index 69f5a5956..25123a2bb 100644 --- a/services/tunnelbroker/src/notifs/fcm/error.rs +++ b/services/tunnelbroker/src/notifs/fcm/error.rs @@ -1,18 +1,18 @@ -use crate::notifs::fcm::response::FCMError; +use crate::notifs::fcm::response::FCMErrorResponse; use derive_more::{Display, Error, From}; #[derive(Debug, From, Display, Error)] pub enum Error { JWTError, ReqwestError(reqwest::Error), InvalidHeaderValue(reqwest::header::InvalidHeaderValue), SerdeJson(serde_json::Error), FCMTokenNotInitialized, - FCMError(FCMError), + FCMError(FCMErrorResponse), } impl From for Error { fn from(_: jsonwebtoken::errors::Error) -> Self { Self::JWTError } } diff --git a/services/tunnelbroker/src/notifs/fcm/mod.rs b/services/tunnelbroker/src/notifs/fcm/mod.rs index 6666e916b..2d0631d8f 100644 --- a/services/tunnelbroker/src/notifs/fcm/mod.rs +++ b/services/tunnelbroker/src/notifs/fcm/mod.rs @@ -1,33 +1,88 @@ use crate::notifs::fcm::config::FCMConfig; +use crate::notifs::fcm::error::Error::FCMError; +use crate::notifs::fcm::firebase_message::{FCMMessage, FCMMessageWrapper}; +use crate::notifs::fcm::response::FCMErrorResponse; use crate::notifs::fcm::token::FCMToken; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::StatusCode; use std::time::Duration; +use tracing::{debug, error}; pub mod config; mod error; mod firebase_message; mod response; mod token; #[derive(Clone)] pub struct FCMClient { http_client: reqwest::Client, config: FCMConfig, token: FCMToken, } impl FCMClient { pub fn new(config: &FCMConfig) -> Result { let http_client = reqwest::Client::builder().build()?; // Token must be a short-lived token (60 minutes) and in a reasonable // timeframe. let token_ttl = Duration::from_secs(60 * 55); let token = FCMToken::new(&config.clone(), token_ttl)?; Ok(FCMClient { http_client, config: config.clone(), token, }) } + + pub async fn send(&self, message: FCMMessage) -> Result<(), error::Error> { + let token = message.token.clone(); + debug!("Sending FCM notif to {}", token); + + let mut headers = HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + + let bearer = self.token.get_auth_bearer().await?; + headers.insert(AUTHORIZATION, HeaderValue::from_str(&bearer)?); + + let url = format!( + "https://fcm.googleapis.com/v1/projects/{}/messages:send", + self.config.project_id + ); + + let msg_wrapper = FCMMessageWrapper { message }; + let payload = serde_json::to_string(&msg_wrapper).unwrap(); + + let response = self + .http_client + .post(&url) + .headers(headers) + .body(payload) + .send() + .await?; + + match response.status() { + StatusCode::OK => { + debug!("Successfully sent FCM notif to {}", token); + Ok(()) + } + error_status => { + let body = response + .text() + .await + .unwrap_or_else(|error| format!("Error occurred: {}", error)); + error!( + "Failed sending FCM notification to: {}. Status: {}. Body: {}", + token, error_status, body + ); + let fcm_error = FCMErrorResponse::from_status(error_status, body); + Err(FCMError(fcm_error)) + } + } + } } diff --git a/services/tunnelbroker/src/notifs/fcm/response.rs b/services/tunnelbroker/src/notifs/fcm/response.rs index 190d3e93f..0cfedeb0e 100644 --- a/services/tunnelbroker/src/notifs/fcm/response.rs +++ b/services/tunnelbroker/src/notifs/fcm/response.rs @@ -1,44 +1,63 @@ use derive_more::{Display, Error}; +use reqwest::StatusCode; #[derive(PartialEq, Debug, Clone, Display, Error)] pub struct InvalidArgumentError { pub details: String, } #[derive(PartialEq, Debug, Display, Error)] -pub enum FCMError { +pub enum FCMErrorResponse { /// No more information is available about this error. UnspecifiedError, /// HTTP error code = 400. /// Request parameters were invalid. /// Potential causes include invalid registration, invalid package name, /// message too big, invalid data key, invalid TTL, or other invalid /// parameters. InvalidArgument(InvalidArgumentError), /// HTTP error code = 404. /// App instance was unregistered from FCM. This usually means that /// the token used is no longer valid and a new one must be used. Unregistered, /// HTTP error code = 403. /// The authenticated sender ID is different from the sender ID for /// the registration token. SenderIdMismatch, /// HTTP error code = 429. /// Sending limit exceeded for the message target. QuotaExceeded, /// HTTP error code = 503. /// The server is overloaded. Unavailable, /// HTTP error code = 500. /// An unknown internal error occurred. Internal, /// HTTP error code = 401. /// APNs certificate or web push auth key was invalid or missing. ThirdPartyAuthError, } +impl FCMErrorResponse { + pub fn from_status(status: StatusCode, body: String) -> Self { + match status { + StatusCode::BAD_REQUEST => { + FCMErrorResponse::InvalidArgument(InvalidArgumentError { + details: body, + }) + } + StatusCode::NOT_FOUND => FCMErrorResponse::Unregistered, + StatusCode::FORBIDDEN => FCMErrorResponse::SenderIdMismatch, + StatusCode::TOO_MANY_REQUESTS => FCMErrorResponse::QuotaExceeded, + StatusCode::SERVICE_UNAVAILABLE => FCMErrorResponse::Unavailable, + StatusCode::INTERNAL_SERVER_ERROR => FCMErrorResponse::Internal, + StatusCode::UNAUTHORIZED => FCMErrorResponse::ThirdPartyAuthError, + _ => FCMErrorResponse::UnspecifiedError, + } + } +}