summaryrefslogtreecommitdiffstats
path: root/third_party/rust/fxa-client/src/push.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/fxa-client/src/push.rs')
-rw-r--r--third_party/rust/fxa-client/src/push.rs274
1 files changed, 274 insertions, 0 deletions
diff --git a/third_party/rust/fxa-client/src/push.rs b/third_party/rust/fxa-client/src/push.rs
new file mode 100644
index 0000000000..978d2f556c
--- /dev/null
+++ b/third_party/rust/fxa-client/src/push.rs
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::device::CommandFetchReason;
+use crate::{error::*, AccountEvent, FirefoxAccount};
+use serde_derive::Deserialize;
+
+impl FirefoxAccount {
+ /// Handle any incoming push message payload coming from the Firefox Accounts
+ /// servers that has been decrypted and authenticated by the Push crate.
+ ///
+ /// Due to iOS platform restrictions, a push notification must always show UI.
+ /// Since FxA sends one push notification per command received,
+ /// we must only retrieve 1 command per push message,
+ /// otherwise we risk receiving push messages for which the UI has already been shown.
+ /// However, note that this means iOS currently risks losing messages for
+ /// which a push notification doesn't arrive.
+ ///
+ /// **💾 This method alters the persisted account state.**
+ pub fn handle_push_message(&mut self, payload: &str) -> Result<Vec<AccountEvent>> {
+ let payload = serde_json::from_str(payload).or_else(|err| {
+ // Due to a limitation of serde (https://github.com/serde-rs/serde/issues/1714)
+ // we can't parse some payloads with an unknown "command" value. Try doing a
+ // less-strongly-validating parse so we can silently ignore such messages, while
+ // while reporting errors if the payload is completely unintelligible.
+ let v: serde_json::Value = serde_json::from_str(payload)?;
+ match v.get("command") {
+ Some(_) => Ok(PushPayload::Unknown),
+ None => Err(err),
+ }
+ })?;
+ match payload {
+ PushPayload::CommandReceived(CommandReceivedPushPayload { index, .. }) => {
+ if cfg!(target_os = "ios") {
+ self.ios_fetch_device_command(index)
+ .map(|cmd| vec![AccountEvent::IncomingDeviceCommand(Box::new(cmd))])
+ } else {
+ self.poll_device_commands(CommandFetchReason::Push(index))
+ .map(|cmds| {
+ cmds.into_iter()
+ .map(|cmd| AccountEvent::IncomingDeviceCommand(Box::new(cmd)))
+ .collect()
+ })
+ }
+ }
+ PushPayload::ProfileUpdated => {
+ self.state.last_seen_profile = None;
+ Ok(vec![AccountEvent::ProfileUpdated])
+ }
+ PushPayload::DeviceConnected(DeviceConnectedPushPayload { device_name }) => {
+ self.clear_devices_and_attached_clients_cache();
+ Ok(vec![AccountEvent::DeviceConnected { device_name }])
+ }
+ PushPayload::DeviceDisconnected(DeviceDisconnectedPushPayload { device_id }) => {
+ let local_device = self.get_current_device_id();
+ let is_local_device = match local_device {
+ Err(_) => false,
+ Ok(id) => id == device_id,
+ };
+ if is_local_device {
+ // Note: self.disconnect calls self.start_over which clears the state for the FirefoxAccount instance
+ self.disconnect();
+ }
+ Ok(vec![AccountEvent::DeviceDisconnected {
+ device_id,
+ is_local_device,
+ }])
+ }
+ PushPayload::AccountDestroyed(AccountDestroyedPushPayload { account_uid }) => {
+ let is_local_account = match &self.state.last_seen_profile {
+ None => false,
+ Some(profile) => profile.response.uid == account_uid,
+ };
+ Ok(if is_local_account {
+ vec![AccountEvent::AccountDestroyed]
+ } else {
+ vec![]
+ })
+ }
+ PushPayload::PasswordChanged | PushPayload::PasswordReset => {
+ let status = self.check_authorization_status()?;
+ // clear any device or client data due to password change.
+ self.clear_devices_and_attached_clients_cache();
+ Ok(if !status.active {
+ vec![AccountEvent::AccountAuthStateChanged]
+ } else {
+ vec![]
+ })
+ }
+ PushPayload::Unknown => {
+ log::info!("Unknown Push command.");
+ Ok(vec![])
+ }
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(tag = "command", content = "data")]
+pub enum PushPayload {
+ #[serde(rename = "fxaccounts:command_received")]
+ CommandReceived(CommandReceivedPushPayload),
+ #[serde(rename = "fxaccounts:profile_updated")]
+ ProfileUpdated,
+ #[serde(rename = "fxaccounts:device_connected")]
+ DeviceConnected(DeviceConnectedPushPayload),
+ #[serde(rename = "fxaccounts:device_disconnected")]
+ DeviceDisconnected(DeviceDisconnectedPushPayload),
+ #[serde(rename = "fxaccounts:password_changed")]
+ PasswordChanged,
+ #[serde(rename = "fxaccounts:password_reset")]
+ PasswordReset,
+ #[serde(rename = "fxaccounts:account_destroyed")]
+ AccountDestroyed(AccountDestroyedPushPayload),
+ #[serde(other)]
+ Unknown,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct CommandReceivedPushPayload {
+ command: String,
+ index: u64,
+ sender: String,
+ url: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DeviceConnectedPushPayload {
+ #[serde(rename = "deviceName")]
+ device_name: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct DeviceDisconnectedPushPayload {
+ #[serde(rename = "id")]
+ device_id: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct AccountDestroyedPushPayload {
+ #[serde(rename = "uid")]
+ account_uid: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::http_client::FxAClientMock;
+ use crate::http_client::IntrospectResponse;
+ use crate::CachedResponse;
+ use std::sync::Arc;
+
+ #[test]
+ fn test_deserialize_send_tab_command() {
+ let json = "{\"version\":1,\"command\":\"fxaccounts:command_received\",\"data\":{\"command\":\"send-tab-recv\",\"index\":1,\"sender\":\"bobo\",\"url\":\"https://mozilla.org\"}}";
+ let _: PushPayload = serde_json::from_str(&json).unwrap();
+ }
+
+ #[test]
+ fn test_push_profile_updated() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ fxa.add_cached_profile("123", "test@example.com");
+ let json = "{\"version\":1,\"command\":\"fxaccounts:profile_updated\"}";
+ let events = fxa.handle_push_message(json).unwrap();
+ assert!(fxa.state.last_seen_profile.is_none());
+ assert_eq!(events.len(), 1);
+ match events[0] {
+ AccountEvent::ProfileUpdated => {}
+ _ => unreachable!(),
+ };
+ }
+
+ #[test]
+ fn test_push_device_disconnected_local() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ let refresh_token_scopes = std::collections::HashSet::new();
+ fxa.state.refresh_token = Some(crate::oauth::RefreshToken {
+ token: "refresh_token".to_owned(),
+ scopes: refresh_token_scopes,
+ });
+ fxa.state.current_device_id = Some("my_id".to_owned());
+ let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"my_id\"}}";
+ let events = fxa.handle_push_message(json).unwrap();
+ assert!(fxa.state.refresh_token.is_none());
+ assert_eq!(events.len(), 1);
+ match &events[0] {
+ AccountEvent::DeviceDisconnected {
+ device_id,
+ is_local_device,
+ } => {
+ assert!(is_local_device);
+ assert_eq!(device_id, "my_id");
+ }
+ _ => unreachable!(),
+ };
+ }
+
+ #[test]
+ fn test_push_password_reset() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ let mut client = FxAClientMock::new();
+ client
+ .expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
+ token.partial_eq("refresh_token")
+ })
+ .times(1)
+ .returns_once(Ok(IntrospectResponse { active: true }));
+ fxa.set_client(Arc::new(client));
+ let refresh_token_scopes = std::collections::HashSet::new();
+ fxa.state.refresh_token = Some(crate::oauth::RefreshToken {
+ token: "refresh_token".to_owned(),
+ scopes: refresh_token_scopes,
+ });
+ fxa.state.current_device_id = Some("my_id".to_owned());
+ fxa.devices_cache = Some(CachedResponse {
+ response: vec![],
+ cached_at: 0,
+ etag: "".to_string(),
+ });
+ let json = "{\"version\":1,\"command\":\"fxaccounts:password_reset\"}";
+ assert!(fxa.devices_cache.is_some());
+ fxa.handle_push_message(json).unwrap();
+ assert!(fxa.devices_cache.is_none());
+ }
+
+ #[test]
+ fn test_push_device_disconnected_remote() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"remote_id\"}}";
+ let events = fxa.handle_push_message(json).unwrap();
+ assert_eq!(events.len(), 1);
+ match &events[0] {
+ AccountEvent::DeviceDisconnected {
+ device_id,
+ is_local_device,
+ } => {
+ assert!(!is_local_device);
+ assert_eq!(device_id, "remote_id");
+ }
+ _ => unreachable!(),
+ };
+ }
+
+ #[test]
+ fn test_handle_push_message_ignores_unknown_command() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ let json = "{\"version\":1,\"command\":\"huh\"}";
+ let events = fxa.handle_push_message(json).unwrap();
+ assert!(events.is_empty());
+ }
+
+ #[test]
+ fn test_handle_push_message_ignores_unknown_command_with_data() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ let json = "{\"version\":1,\"command\":\"huh\",\"data\":{\"value\":42}}";
+ let events = fxa.handle_push_message(json).unwrap();
+ assert!(events.is_empty());
+ }
+
+ #[test]
+ fn test_handle_push_message_errors_on_garbage_data() {
+ let mut fxa =
+ FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
+ let json = "{\"wtf\":\"bbq\"}";
+ fxa.handle_push_message(json).unwrap_err();
+ }
+}