Skip to content

Commit

Permalink
feat: Reject Legacy GCM endpoints (#459)
Browse files Browse the repository at this point in the history
* feat: Reject Legacy GCM endpoints

This will force GCM endpoints to return 410, and remove the subscription
from the user's record.

Closes: SYNC-3946
  • Loading branch information
jrconlin authored Oct 4, 2023
1 parent 2a97e67 commit aaa4713
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 103 deletions.
19 changes: 19 additions & 0 deletions autoendpoint/src/extractors/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::error::{ApiErrorKind, ApiResult};
use crate::extractors::routers::RouterType;
use crate::server::AppState;
use actix_http::StatusCode;
use autopush_common::db::{client::DbClient, User};
use cadence::{CountedExt, StatsdClient};
use uuid::Uuid;
Expand All @@ -27,6 +28,24 @@ pub async fn validate_user(
}
};

// Legacy GCM support was discontinued by Google in Sept 2023.
// Since we do not have access to the account that originally created the GCM project
// and credentials, we cannot move those users to modern FCM implementations, so we
// must drop them.
if router_type == RouterType::GCM {
debug!("Encountered GCM record, dropping user"; "user" => ?user);
// record the bridge error for accounting reasons.
app_state
.metrics
.incr_with_tags("notification.bridge.error")
.with_tag("platform", "gcm")
.with_tag("reason", "gcm_kill")
.with_tag("error", &StatusCode::GONE.to_string())
.send();
drop_user(user.uaid, app_state.db.as_ref(), &app_state.metrics).await?;
return Err(ApiErrorKind::Router(crate::routers::RouterError::NotFound).into());
}

if router_type == RouterType::WebPush {
validate_webpush_user(user, channel_id, app_state.db.as_ref(), &app_state.metrics).await?;
}
Expand Down
23 changes: 3 additions & 20 deletions autoendpoint/src/routers/fcm/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ impl FcmClient {
.map_err(FcmError::DeserializeResponse)?;
if raw_data.is_empty() {
warn!("Empty FCM response [{status}]");
return Err(FcmError::EmptyResponse(status, self.is_gcm).into());
return Err(FcmError::EmptyResponse(status).into());
}
let data: FcmResponse = serde_json::from_slice(&raw_data).map_err(|e| {
let s = String::from_utf8(raw_data.to_vec()).unwrap_or_else(|e| e.to_string());
Expand All @@ -150,25 +150,8 @@ impl FcmClient {

// we only ever send one.
return Err(match (status, data.error) {
(StatusCode::UNAUTHORIZED, _) => {
if self.is_gcm {
RouterError::GCMAuthentication
} else {
RouterError::Authentication
}
}
(StatusCode::NOT_FOUND, _) => {
// GCM ERROR
if self.is_gcm {
warn!("Converting GCM NOT FOUND to FCM SERVICE_UNAVAILABLE");
RouterError::Upstream {
status: StatusCode::SERVICE_UNAVAILABLE.to_string(),
message: "FCM did not find GCM user".to_owned(),
}
} else {
RouterError::NotFound
}
}
(StatusCode::UNAUTHORIZED, _) => RouterError::Authentication,
(StatusCode::NOT_FOUND, _) => RouterError::NotFound,
(_, Some(error)) => RouterError::Upstream {
status: error.status,
message: error.message,
Expand Down
13 changes: 5 additions & 8 deletions autoendpoint/src/routers/fcm/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub enum FcmError {
InvalidResponse(#[source] serde_json::Error, String, StatusCode),

#[error("Empty response from FCM")]
EmptyResponse(StatusCode, bool),
EmptyResponse(StatusCode),

#[error("No OAuth token was present")]
NoOAuthToken,
Expand Down Expand Up @@ -50,7 +50,7 @@ impl FcmError {
| FcmError::NoOAuthToken => StatusCode::INTERNAL_SERVER_ERROR,

FcmError::DeserializeResponse(_)
| FcmError::EmptyResponse(_, _)
| FcmError::EmptyResponse(_)
| FcmError::InvalidResponse(_, _, _) => StatusCode::BAD_GATEWAY,
}
}
Expand All @@ -66,19 +66,16 @@ impl FcmError {
| FcmError::OAuthClientBuild(_)
| FcmError::OAuthToken(_)
| FcmError::DeserializeResponse(_)
| FcmError::EmptyResponse(_, _)
| FcmError::EmptyResponse(_)
| FcmError::InvalidResponse(_, _, _)
| FcmError::NoOAuthToken => None,
}
}

pub fn extras(&self) -> Vec<(&str, String)> {
match self {
FcmError::EmptyResponse(status, is_gcm) => {
vec![
("status", status.to_string()),
("is_gcm", is_gcm.to_string()),
]
FcmError::EmptyResponse(status) => {
vec![("status", status.to_string())]
}
FcmError::InvalidResponse(_, body, status) => {
vec![("status", status.to_string()), ("body", body.to_owned())]
Expand Down
79 changes: 4 additions & 75 deletions autoendpoint/src/routers/fcm/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,45 +154,18 @@ impl Router for FcmRouter {
.as_ref()
.ok_or(FcmError::NoRegistrationToken)?;

// Older "GCM" set the router data as "senderID" : "auth"
// Newer "FCM" set the router data as "app_id": "token"
// The first element is the project identifier, which is
// matched against the values specified in the settings to
// get the authentication method. The second is the client
// provided routing token. This is a proprietary identifier
// that is sent by the client at registration.
//
// Try reading as FCM and fall back to GCM.
let (routing_token, app_id) =
self.routing_info(router_data, &notification.subscription.user.uaid)?;
let ttl = MAX_TTL.min(self.settings.min_ttl.max(notification.headers.ttl as usize));
let message_data = build_message_data(notification)?;

// Send the notification to FCM
// (Sigh, errors do not have tags support. )
let client = self
.clients
.get(&app_id)
.ok_or_else(|| FcmError::InvalidAppId(app_id.clone()))?;

// GCM is the older message format for android, and it's not possible to generate
// new test keys. As of 2023-09, GCM should use FCM OAuth2 in order to authenticate.
// FCM operates by using the more complex token as part of an OAuth2
// transaction. According to the documentation, GCM and FCM are interoperable,
// meaning that the client provided tokens should match up, and that the structure
// of the data should also not make a huge difference.
//
// In Oct of 2023, "legacy" GCM support was deprecated. After some
// research, it was hypothesised that the current FCM project may be able
// to accept GCM routing identifiers as FCM message tokens. The current code presumes
// that this is a fragile relationship and so GCM errors do not immediately drop the
// remote user's endpoint (potentially causing a UA reset.)
//
let platform = if client.is_gcm {
"gcm_as_fcmv1"
} else {
"fcmv1"
};
let message_data = build_message_data(notification)?;
let platform = "fcmv1";
trace!("Sending message to {platform}: [{:?}]", &app_id);
if let Err(e) = client.send(message_data, routing_token, ttl).await {
return Err(handle_error(
Expand Down Expand Up @@ -261,7 +234,8 @@ mod tests {
},
GCM_PROJECT_ID: {
"project_id": GCM_PROJECT_ID,
"credential": gcm_credential
"credential": gcm_credential,
"is_gcm": true,
}
})
.to_string(),
Expand Down Expand Up @@ -322,51 +296,6 @@ mod tests {
fcm_mock.assert();
}

/// A notification with no data is sent to GCM if the subscription specifies it.
/*
#[tokio::test]
async fn successful_gcm_fallback() {
let auth_key = "AIzaSyB0ecSrqnEDXQ7yjLXqVc0CUGOeSlq9BsM"; // this is a nonce value used only for testing.
let registration_id = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
// let project_id = GCM_PROJECT_ID;
let db = MockDbClient::new().into_boxed_arc();
let router = make_router(make_service_key(), auth_key.to_owned(), db).await;
assert!(router.active());
// body must match the composed body in `gcm_send` exactly (order, values, etc.)
let body = serde_json::json!({
"registration_ids": [registration_id],
"time_to_live": 60_i32,
"delay_while_idle": false,
"data": {
"chid": CHANNEL_ID
},
})
.to_string();
let _token_mock = mock_token_endpoint();
let fcm_mock = mock_gcm_endpoint_builder()
.match_header("Authorization", format!("key={}", &auth_key).as_str())
.match_header("Content-Type", "application/json")
.with_body(
r#"{ "multicast_id": 216,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"1:02"}]}"#,
)
.match_body(body.as_str())
.create();
let notification = make_notification(
gcm_router_data(registration_id.to_owned()),
None,
RouterType::GCM,
);
let result = router.route_notification(&notification).await;
assert!(result.is_ok(), "result = {result:?}");
assert_eq!(
result.unwrap(),
RouterResponse::success("http://localhost:8080/m/test-message-id".to_string(), 0)
);
fcm_mock.assert();
}
*/

/// A notification with data is sent to FCM
#[tokio::test]
async fn successful_routing_with_data() {
Expand Down

0 comments on commit aaa4713

Please sign in to comment.