diff --git a/lib/types/tunnelbroker/notif-types.js b/lib/types/tunnelbroker/notif-types.js --- a/lib/types/tunnelbroker/notif-types.js +++ b/lib/types/tunnelbroker/notif-types.js @@ -23,7 +23,15 @@ +payload: string, }; +export type TunnelbrokerWNSNotif = { + +type: 'WNSNotif', + +clientMessageID: string, + +deviceID: string, + +payload: string, +}; + export type TunnelbrokerNotif = | TunnelbrokerAPNsNotif | TunnelbrokerFCMNotif - | TunnelbrokerWebPushNotif; + | TunnelbrokerWebPushNotif + | TunnelbrokerWNSNotif; diff --git a/services/tunnelbroker/src/notifs/mod.rs b/services/tunnelbroker/src/notifs/mod.rs --- a/services/tunnelbroker/src/notifs/mod.rs +++ b/services/tunnelbroker/src/notifs/mod.rs @@ -14,7 +14,7 @@ APNs, FCM, WebPush, - WNs, + WNS, } impl NotifClientType { @@ -25,7 +25,7 @@ } NotifClientType::FCM => platform == Platform::Android, NotifClientType::WebPush => platform == Platform::Web, - NotifClientType::WNs => platform == Platform::Windows, + NotifClientType::WNS => platform == Platform::Windows, } } } diff --git a/services/tunnelbroker/src/notifs/wns/error.rs b/services/tunnelbroker/src/notifs/wns/error.rs --- a/services/tunnelbroker/src/notifs/wns/error.rs +++ b/services/tunnelbroker/src/notifs/wns/error.rs @@ -1,15 +1,29 @@ use derive_more::{Display, Error, From}; +use super::response::WNSErrorResponse; + #[derive(Debug, From, Display, Error)] pub enum Error { Reqwest(reqwest::Error), SerdeJson(serde_json::Error), - #[display(fmt = "Token not found in response")] - TokenNotFound, - #[display(fmt = "Expiry time not found in response")] - ExpiryNotFound, + #[display(fmt = "WNS Token Error: {}", _0)] + WNSToken(WNSTokenError), #[display(fmt = "Failed to acquire read lock")] ReadLock, #[display(fmt = "Failed to acquire write lock")] WriteLock, + #[display(fmt = "WNS Notification Error: {}", _0)] + WNSNotification(WNSErrorResponse), +} + +#[derive(Debug, From, Display)] +pub enum WNSTokenError { + #[display(fmt = "Token not found in response")] + TokenNotFound, + #[display(fmt = "Expiry time not found in response")] + ExpiryNotFound, + #[display(fmt = "Unknown Error: {}", _0)] + Unknown(String), } + +impl std::error::Error for WNSTokenError {} diff --git a/services/tunnelbroker/src/notifs/wns/mod.rs b/services/tunnelbroker/src/notifs/wns/mod.rs --- a/services/tunnelbroker/src/notifs/wns/mod.rs +++ b/services/tunnelbroker/src/notifs/wns/mod.rs @@ -1,4 +1,7 @@ use crate::notifs::wns::config::WNSConfig; +use error::WNSTokenError; +use reqwest::StatusCode; +use response::WNSErrorResponse; use std::{ sync::{Arc, RwLock}, time::{Duration, SystemTime}, @@ -6,6 +9,7 @@ pub mod config; mod error; +mod response; #[derive(Debug, Clone)] pub struct WNSAccessToken { @@ -13,6 +17,12 @@ expires: SystemTime, } +#[derive(Debug, Clone)] +pub struct WNSNotif { + pub device_token: String, + pub payload: String, +} + #[derive(Clone)] pub struct WNSClient { http_client: reqwest::Client, @@ -30,9 +40,45 @@ }) } - pub async fn get_wns_token( - &mut self, - ) -> Result, error::Error> { + pub async fn send(&self, notif: WNSNotif) -> Result<(), error::Error> { + let wns_access_token = self.get_wns_token().await?; + + let url = notif.device_token; + + // Send the notification + let response = self + .http_client + .post(&url) + .header("Content-Type", "application/octet-stream") + .header("X-WNS-Type", "wns/raw") + .bearer_auth(wns_access_token) + .body(notif.payload) + .send() + .await?; + + match response.status() { + StatusCode::OK => { + tracing::debug!("Successfully sent WNS notif to {}", &url); + Ok(()) + } + error_status => { + let body = response + .text() + .await + .unwrap_or_else(|error| format!("Error occurred: {}", error)); + tracing::error!( + "Failed sending WNS notification to: {}. Status: {}. Body: {}", + &url, + error_status, + body + ); + let fcm_error = WNSErrorResponse::from_status(error_status, body); + Err(error::Error::WNSNotification(fcm_error)) + } + } + } + + pub async fn get_wns_token(&self) -> Result { const EXPIRY_WINDOW: Duration = Duration::from_secs(10); { @@ -42,7 +88,7 @@ .map_err(|_| error::Error::ReadLock)?; if let Some(ref token) = *read_guard { if token.expires >= SystemTime::now() - EXPIRY_WINDOW { - return Ok(Some(token.token.clone())); + return Ok(token.token.clone()); } } } @@ -68,17 +114,17 @@ .await .unwrap_or_else(|_| String::from("")); tracing::error!(status, "Failure when getting the WNS token: {}", body); - return Ok(None); + return Err(error::Error::WNSToken(WNSTokenError::Unknown(status))); } let response_json: serde_json::Value = response.json().await?; let token = response_json["access_token"] .as_str() - .ok_or(error::Error::TokenNotFound)? + .ok_or(error::Error::WNSToken(WNSTokenError::TokenNotFound))? .to_string(); let expires_in = response_json["expires_in"] .as_u64() - .ok_or(error::Error::ExpiryNotFound)?; + .ok_or(error::Error::WNSToken(WNSTokenError::ExpiryNotFound))?; let expires = SystemTime::now() + Duration::from_secs(expires_in); @@ -92,6 +138,6 @@ expires, }); } - Ok(Some(token)) + Ok(token) } } diff --git a/services/tunnelbroker/src/notifs/wns/response.rs b/services/tunnelbroker/src/notifs/wns/response.rs new file mode 100644 --- /dev/null +++ b/services/tunnelbroker/src/notifs/wns/response.rs @@ -0,0 +1,75 @@ +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 WNSErrorResponse { + /// No more information is available about this error. + UnspecifiedError, + + /// HTTP error code = 400. + /// One or more headers were specified incorrectly or conflict with another header. + BadRequest(InvalidArgumentError), + + /// HTTP error code = 401. + /// The cloud service did not present a valid authentication ticket. + Unauthorized, + + /// HTTP error code = 403. + /// The cloud service is not authorized to send a notification to this URI. + Forbidden, + + /// HTTP error code = 404. + /// The channel URI is not valid or is not recognized by WNS. + NotFound, + + /// HTTP error code = 405. + /// Invalid method (GET, CREATE); only POST (Windows or Windows Phone) or DELETE (Windows Phone only) is allowed. + MethodNotAllowed, + + /// HTTP error code = 406. + /// The cloud service exceeded its throttle limit. + NotAcceptable, + + /// HTTP error code = 410. + /// The channel expired. + Gone, + + /// HTTP error code = 413. + /// The notification payload exceeds the 5000 byte size limit. + RequestEntityTooLarge, + + /// HTTP error code = 500. + /// An internal failure caused notification delivery to fail. + InternalServerError, + + /// HTTP error code = 503. + /// The server is currently unavailable. + ServiceUnavailable, +} + +impl WNSErrorResponse { + pub fn from_status(status: StatusCode, body: String) -> Self { + match status { + StatusCode::BAD_REQUEST => { + WNSErrorResponse::BadRequest(InvalidArgumentError { details: body }) + } + StatusCode::UNAUTHORIZED => WNSErrorResponse::Unauthorized, + StatusCode::FORBIDDEN => WNSErrorResponse::Forbidden, + StatusCode::NOT_FOUND => WNSErrorResponse::NotFound, + StatusCode::METHOD_NOT_ALLOWED => WNSErrorResponse::MethodNotAllowed, + StatusCode::NOT_ACCEPTABLE => WNSErrorResponse::NotAcceptable, + StatusCode::GONE => WNSErrorResponse::Gone, + StatusCode::PAYLOAD_TOO_LARGE => WNSErrorResponse::RequestEntityTooLarge, + StatusCode::INTERNAL_SERVER_ERROR => { + WNSErrorResponse::InternalServerError + } + StatusCode::SERVICE_UNAVAILABLE => WNSErrorResponse::ServiceUnavailable, + _ => WNSErrorResponse::UnspecifiedError, + } + } +} diff --git a/services/tunnelbroker/src/websockets/session.rs b/services/tunnelbroker/src/websockets/session.rs --- a/services/tunnelbroker/src/websockets/session.rs +++ b/services/tunnelbroker/src/websockets/session.rs @@ -40,6 +40,7 @@ AndroidConfig, AndroidMessagePriority, FCMMessage, }; use crate::notifs::web_push::WebPushNotif; +use crate::notifs::wns::WNSNotif; use crate::notifs::{apns, NotifClient, NotifClientType}; use crate::{identity, notifs}; @@ -77,6 +78,7 @@ MissingAPNsClient, MissingFCMClient, MissingWebPushClient, + MissingWNSClient, MissingDeviceToken, InvalidDeviceToken, InvalidNotifProvider, @@ -581,6 +583,46 @@ self.get_message_to_device_status(¬if.client_message_id, result), ) } + DeviceToTunnelbrokerMessage::WNSNotif(notif) => { + if !self.device_info.is_authenticated { + debug!( + "Unauthenticated device {} tried to send WNS notif. Aborting.", + self.device_info.device_id + ); + return Some(MessageSentStatus::Unauthenticated); + } + debug!("Received WNS notif for {}", notif.device_id); + + let Some(wns_client) = self.notif_client.wns.clone() else { + return Some(self.get_message_to_device_status( + ¬if.client_message_id, + Err(SessionError::MissingWNSClient), + )); + }; + + let device_token = match self + .get_device_token(notif.device_id, NotifClientType::WNS) + .await + { + Ok(token) => token, + Err(e) => { + return Some( + self + .get_message_to_device_status(¬if.client_message_id, Err(e)), + ) + } + }; + + let wns_notif = WNSNotif { + device_token, + payload: notif.payload, + }; + + let result = wns_client.send(wns_notif).await; + Some( + self.get_message_to_device_status(¬if.client_message_id, result), + ) + } _ => { error!("Client sent invalid message type"); Some(MessageSentStatus::InvalidRequest) diff --git a/shared/tunnelbroker_messages/src/messages/mod.rs b/shared/tunnelbroker_messages/src/messages/mod.rs --- a/shared/tunnelbroker_messages/src/messages/mod.rs +++ b/shared/tunnelbroker_messages/src/messages/mod.rs @@ -46,6 +46,7 @@ APNsNotif(APNsNotif), FCMNotif(FCMNotif), WebPushNotif(WebPushNotif), + WNSNotif(WNSNotif), MessageToDeviceRequest(MessageToDeviceRequest), MessageReceiveConfirmation(MessageReceiveConfirmation), MessageToTunnelbrokerRequest(MessageToTunnelbrokerRequest), diff --git a/shared/tunnelbroker_messages/src/messages/notif.rs b/shared/tunnelbroker_messages/src/messages/notif.rs --- a/shared/tunnelbroker_messages/src/messages/notif.rs +++ b/shared/tunnelbroker_messages/src/messages/notif.rs @@ -35,3 +35,14 @@ pub device_id: String, pub payload: String, } + +/// WNS notif built on client. +#[derive(Serialize, Deserialize, TagAwareDeserialize, PartialEq, Debug)] +#[serde(tag = "type", remote = "Self", rename_all = "camelCase")] +pub struct WNSNotif { + #[serde(rename = "clientMessageID")] + pub client_message_id: String, + #[serde(rename = "deviceID")] + pub device_id: String, + pub payload: String, +}