Skip to content

Commit

Permalink
feat: Delete message endpoint (#186)
Browse files Browse the repository at this point in the history
* Stub the DELETE /m/{message_id} route

* Extract the message ID data from the request path

* Implement DbClient::delete_message

* Implement the delete_notification_route handler

* Fix errors after rebase

Closes #175
  • Loading branch information
AzureMarker authored Jul 27, 2020
1 parent 6df3e36 commit 6a7fa49
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 21 deletions.
20 changes: 20 additions & 0 deletions autoendpoint/src/db/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,24 @@ impl DbClient {

Ok(())
}

/// Delete a notification
pub async fn delete_message(&self, uaid: Uuid, sort_key: String) -> DbResult<()> {
let input = DeleteItemInput {
table_name: self.message_table.clone(),
key: ddb_item! {
uaid: s => uaid.to_simple().to_string(),
chidmessageid: s => sort_key
},
..Default::default()
};

retry_policy()
.retry_if(
|| self.ddb.delete_item(input.clone()),
retryable_delete_error(self.metrics.clone()),
)
.await?;
Ok(())
}
}
9 changes: 7 additions & 2 deletions autoendpoint/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ pub enum ApiErrorKind {
#[error("Invalid router token")]
InvalidRouterToken,

#[error("Invalid message ID")]
InvalidMessageId,

#[error("{0}")]
Internal(String),
}
Expand All @@ -121,7 +124,8 @@ impl ApiErrorKind {
| ApiErrorKind::TokenHashValidation(_)
| ApiErrorKind::NoTTL
| ApiErrorKind::InvalidRouterType
| ApiErrorKind::InvalidRouterToken => StatusCode::BAD_REQUEST,
| ApiErrorKind::InvalidRouterToken
| ApiErrorKind::InvalidMessageId => StatusCode::BAD_REQUEST,

ApiErrorKind::NoUser | ApiErrorKind::NoSubscription => StatusCode::GONE,

Expand Down Expand Up @@ -176,7 +180,8 @@ impl ApiErrorKind {
| ApiErrorKind::PayloadError(_)
| ApiErrorKind::InvalidRouterToken
| ApiErrorKind::RegistrationSecretHash(_)
| ApiErrorKind::EndpointUrl(_) => None,
| ApiErrorKind::EndpointUrl(_)
| ApiErrorKind::InvalidMessageId => None,
}
}
}
Expand Down
128 changes: 128 additions & 0 deletions autoendpoint/src/extractors/message_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use crate::error::{ApiError, ApiErrorKind, ApiResult};
use crate::server::ServerState;
use actix_web::dev::{Payload, PayloadStream};
use actix_web::web::Data;
use actix_web::{FromRequest, HttpRequest};
use fernet::MultiFernet;
use futures::future;
use uuid::Uuid;

/// Holds information about a notification. The information is encoded and
/// encrypted into a "message ID" which is presented to the user. Later, the
/// user can send us the message ID to perform operations on the associated
/// notification (e.g. delete it).
#[derive(Debug)]
pub enum MessageId {
WithTopic {
uaid: Uuid,
channel_id: Uuid,
topic: String,
},
WithoutTopic {
uaid: Uuid,
channel_id: Uuid,
timestamp: u64,
},
}

impl FromRequest for MessageId {
type Error = ApiError;
type Future = future::Ready<Result<Self, Self::Error>>;
type Config = ();

fn from_request(req: &HttpRequest, _: &mut Payload<PayloadStream>) -> Self::Future {
let message_id_param = req
.match_info()
.get("message_id")
.expect("{message_id} must be part of the path");
let state: Data<ServerState> = Data::extract(req)
.into_inner()
.expect("No server state found");

future::ready(MessageId::decrypt(&state.fernet, message_id_param))
}
}

impl MessageId {
/// Encode and encrypt the message ID
pub fn encrypt(&self, fernet: &MultiFernet) -> String {
let id_str = match self {
MessageId::WithTopic {
uaid,
channel_id,
topic,
} => format!(
"01:{}:{}:{}",
uaid.to_simple_ref(),
channel_id.to_simple_ref(),
topic
),
MessageId::WithoutTopic {
uaid,
channel_id,
timestamp,
} => format!(
"02:{}:{}:{}",
uaid.to_simple_ref(),
channel_id.to_simple_ref(),
timestamp
),
};

fernet.encrypt(id_str.as_bytes())
}

/// Decrypt and decode the message ID
pub fn decrypt(fernet: &MultiFernet, message_id: &str) -> ApiResult<Self> {
let decrypted_bytes = fernet
.decrypt(message_id)
.map_err(|_| ApiErrorKind::InvalidMessageId)?;
let decrypted_str = String::from_utf8_lossy(&decrypted_bytes);
let segments: Vec<_> = decrypted_str.split(':').collect();

if segments.len() != 4 {
return Err(ApiErrorKind::InvalidMessageId.into());
}

let (version, uaid, chid, topic_or_timestamp) =
(segments[0], segments[1], segments[2], segments[3]);

match version {
"01" => Ok(MessageId::WithTopic {
uaid: Uuid::parse_str(uaid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
channel_id: Uuid::parse_str(chid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
topic: topic_or_timestamp.to_string(),
}),
"02" => Ok(MessageId::WithoutTopic {
uaid: Uuid::parse_str(uaid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
channel_id: Uuid::parse_str(chid).map_err(|_| ApiErrorKind::InvalidMessageId)?,
timestamp: topic_or_timestamp
.parse()
.map_err(|_| ApiErrorKind::InvalidMessageId)?,
}),
_ => Err(ApiErrorKind::InvalidMessageId.into()),
}
}

/// Get the UAID of the associated notification
pub fn uaid(&self) -> Uuid {
match self {
MessageId::WithTopic { uaid, .. } => *uaid,
MessageId::WithoutTopic { uaid, .. } => *uaid,
}
}

/// Get the sort-key for the associated notification
pub fn sort_key(&self) -> String {
match self {
MessageId::WithTopic {
channel_id, topic, ..
} => format!("01:{}:{}", channel_id.to_hyphenated(), topic),
MessageId::WithoutTopic {
channel_id,
timestamp,
..
} => format!("02:{}:{}", timestamp, channel_id.to_hyphenated()),
}
}
}
1 change: 1 addition & 0 deletions autoendpoint/src/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Actix extractors (`FromRequest`). These extractors transform and validate
//! the incoming request data.
pub mod message_id;
pub mod notification;
pub mod notification_headers;
pub mod registration_path_args;
Expand Down
33 changes: 16 additions & 17 deletions autoendpoint/src/extractors/notification.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::{ApiError, ApiErrorKind};
use crate::extractors::message_id::MessageId;
use crate::extractors::notification_headers::NotificationHeaders;
use crate::extractors::subscription::Subscription;
use crate::server::ServerState;
Expand Down Expand Up @@ -53,8 +54,8 @@ impl FromRequest for Notification {
let timestamp = sec_since_epoch();
let message_id = Self::generate_message_id(
&state.fernet,
&subscription.user.uaid,
&subscription.channel_id,
subscription.user.uaid,
subscription.channel_id,
headers.topic.as_deref(),
timestamp,
);
Expand Down Expand Up @@ -109,28 +110,26 @@ impl Notification {
/// Encrypted('02' : uaid.hex : channel_id.hex : timestamp)
fn generate_message_id(
fernet: &MultiFernet,
uaid: &Uuid,
channel_id: &Uuid,
uaid: Uuid,
channel_id: Uuid,
topic: Option<&str>,
timestamp: u64,
) -> String {
let message_id = if let Some(topic) = topic {
format!(
"01:{}:{}:{}",
uaid.to_simple_ref(),
channel_id.to_simple_ref(),
topic
)
MessageId::WithTopic {
uaid,
channel_id,
topic: topic.to_string(),
}
} else {
format!(
"02:{}:{}:{}",
uaid.to_simple_ref(),
channel_id.to_simple_ref(),
timestamp
)
MessageId::WithoutTopic {
uaid,
channel_id,
timestamp,
}
};

fernet.encrypt(message_id.as_bytes())
message_id.encrypt(fernet)
}

/// Serialize the notification for delivery to the connection server. Some
Expand Down
21 changes: 20 additions & 1 deletion autoendpoint/src/routes/webpush.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::error::ApiResult;
use crate::extractors::message_id::MessageId;
use crate::extractors::notification::Notification;
use crate::extractors::routers::Routers;
use crate::server::ServerState;
use actix_web::web::Data;
use actix_web::HttpResponse;

/// Handle the `/wpush/{api_version}/{token}` and `/wpush/{token}` routes
/// Handle the `POST /wpush/{api_version}/{token}` and `POST /wpush/{token}` routes
pub async fn webpush_route(
notification: Notification,
routers: Routers,
Expand All @@ -14,3 +17,19 @@ pub async fn webpush_route(

Ok(response.into())
}

/// Handle the `DELETE /m/{message_id}` route
pub async fn delete_notification_route(
message_id: MessageId,
state: Data<ServerState>,
) -> ApiResult<HttpResponse> {
let sort_key = message_id.sort_key();
debug!("Deleting notification with sort-key {}", sort_key);
trace!("message_id = {:?}", message_id);
state
.ddb
.delete_message(message_id.uaid(), sort_key)
.await?;

Ok(HttpResponse::NoContent().finish())
}
6 changes: 5 additions & 1 deletion autoendpoint/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::metrics;
use crate::routers::fcm::router::FcmRouter;
use crate::routes::health::{health_route, lb_heartbeat_route, status_route, version_route};
use crate::routes::registration::register_uaid_route;
use crate::routes::webpush::webpush_route;
use crate::routes::webpush::{delete_notification_route, webpush_route};
use crate::settings::Settings;
use actix_cors::Cors;
use actix_web::{
Expand Down Expand Up @@ -75,6 +75,10 @@ impl Server {
web::resource(["/wpush/{api_version}/{token}", "/wpush/{token}"])
.route(web::post().to(webpush_route)),
)
.service(
web::resource("/m/{message_id}")
.route(web::delete().to(delete_notification_route)),
)
.service(
web::resource("/v1/{router_type}/{app_id}/registration")
.route(web::post().to(register_uaid_route)),
Expand Down

0 comments on commit 6a7fa49

Please sign in to comment.