1use std::convert::TryInto;
6
7use super::FirefoxAccount;
8use crate::{info, AccountEvent, Error, Result};
9use serde_derive::Deserialize;
10
11impl FirefoxAccount {
12 pub fn handle_push_message(&mut self, payload: &str) -> Result<AccountEvent> {
21 let payload = serde_json::from_str(payload).or_else(|err| {
22 let v: serde_json::Value = serde_json::from_str(payload)?;
23 match v.get("command") {
24 Some(_) => Ok(PushPayload::Unknown),
25 None => Err(err),
26 }
27 })?;
28 match payload {
29 PushPayload::CommandReceived(CommandReceivedPushPayload { index, .. }) => {
30 let cmd = self.get_command_for_index(index)?;
31 Ok(AccountEvent::CommandReceived {
32 command: cmd.try_into()?,
33 })
34 }
35 PushPayload::ProfileUpdated => {
36 self.state.clear_last_seen_profile();
37 Ok(AccountEvent::ProfileUpdated)
38 }
39 PushPayload::DeviceConnected(DeviceConnectedPushPayload { device_name }) => {
40 self.clear_devices_and_attached_clients_cache();
41 Ok(AccountEvent::DeviceConnected { device_name })
42 }
43 PushPayload::DeviceDisconnected(DeviceDisconnectedPushPayload { device_id }) => {
44 let local_device = self.get_current_device_id();
45 let is_local_device = match local_device {
46 Err(_) => false,
47 Ok(id) => id == device_id,
48 };
49 if is_local_device {
50 self.disconnect();
52 }
53 Ok(AccountEvent::DeviceDisconnected {
54 device_id,
55 is_local_device,
56 })
57 }
58 PushPayload::AccountDestroyed(AccountDestroyedPushPayload { account_uid }) => {
59 let is_local_account = match self.state.last_seen_profile() {
60 None => false,
61 Some(profile) => profile.response.uid == account_uid,
62 };
63 Ok(if is_local_account {
64 AccountEvent::AccountDestroyed
65 } else {
66 return Err(Error::InvalidPushEvent);
67 })
68 }
69 PushPayload::PasswordChanged | PushPayload::PasswordReset => {
70 let status = self.check_authorization_status()?;
71 self.clear_devices_and_attached_clients_cache();
73 Ok(if !status.active {
74 AccountEvent::AccountAuthStateChanged
75 } else {
76 info!("Password change event, but no action required");
77 AccountEvent::Unknown
78 })
79 }
80 PushPayload::Unknown => {
81 info!("Unknown Push command.");
82 Ok(AccountEvent::Unknown)
83 }
84 }
85 }
86}
87
88#[derive(Debug, Deserialize)]
89#[serde(tag = "command", content = "data")]
90pub enum PushPayload {
91 #[serde(rename = "fxaccounts:command_received")]
92 CommandReceived(CommandReceivedPushPayload),
93 #[serde(rename = "fxaccounts:profile_updated")]
94 ProfileUpdated,
95 #[serde(rename = "fxaccounts:device_connected")]
96 DeviceConnected(DeviceConnectedPushPayload),
97 #[serde(rename = "fxaccounts:device_disconnected")]
98 DeviceDisconnected(DeviceDisconnectedPushPayload),
99 #[serde(rename = "fxaccounts:password_changed")]
100 PasswordChanged,
101 #[serde(rename = "fxaccounts:password_reset")]
102 PasswordReset,
103 #[serde(rename = "fxaccounts:account_destroyed")]
104 AccountDestroyed(AccountDestroyedPushPayload),
105 #[serde(other)]
106 Unknown,
107}
108
109#[allow(dead_code)]
112#[derive(Debug, Deserialize)]
113pub struct CommandReceivedPushPayload {
114 command: String,
115 index: u64,
116 sender: String,
117 url: String,
118}
119
120#[derive(Debug, Deserialize)]
121pub struct DeviceConnectedPushPayload {
122 #[serde(rename = "deviceName")]
123 device_name: String,
124}
125
126#[derive(Debug, Deserialize)]
127pub struct DeviceDisconnectedPushPayload {
128 #[serde(rename = "id")]
129 device_id: String,
130}
131
132#[derive(Debug, Deserialize)]
133pub struct AccountDestroyedPushPayload {
134 #[serde(rename = "uid")]
135 account_uid: String,
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::internal::http_client::IntrospectResponse;
142 use crate::internal::http_client::MockFxAClient;
143 use crate::internal::oauth::RefreshToken;
144 use crate::internal::CachedResponse;
145 use crate::internal::Config;
146 use mockall::predicate::always;
147 use mockall::predicate::eq;
148 use std::sync::Arc;
149
150 #[test]
151 fn test_deserialize_send_tab_command() {
152 let json = "{\"version\":1,\"command\":\"fxaccounts:command_received\",\"data\":{\"command\":\"send-tab-recv\",\"index\":1,\"sender\":\"bobo\",\"url\":\"https://mozilla.org\"}}";
153 let _: PushPayload = serde_json::from_str(json).unwrap();
154 }
155
156 #[test]
157 fn test_push_profile_updated() {
158 let mut fxa = FirefoxAccount::with_config(crate::internal::Config::stable_dev(
159 "12345678",
160 "https://foo.bar",
161 ));
162 fxa.add_cached_profile("123", "test@example.com");
163 let json = "{\"version\":1,\"command\":\"fxaccounts:profile_updated\"}";
164 let event = fxa.handle_push_message(json).unwrap();
165 assert!(fxa.state.last_seen_profile().is_none());
166 assert!(matches!(event, AccountEvent::ProfileUpdated));
167 }
168
169 #[test]
170 fn test_push_device_disconnected_local() {
171 viaduct_dev::use_dev_backend();
172 let mut fxa =
173 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
174 let refresh_token_scopes = std::collections::HashSet::new();
175 fxa.state
176 .force_refresh_token(crate::internal::oauth::RefreshToken {
177 token: "refresh_token".to_owned(),
178 scopes: refresh_token_scopes,
179 });
180 fxa.state.force_current_device_id("my_id");
181 let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"my_id\"}}";
182 let event = fxa.handle_push_message(json).unwrap();
183 assert!(fxa.state.refresh_token().is_none());
184 match event {
185 AccountEvent::DeviceDisconnected {
186 device_id,
187 is_local_device,
188 } => {
189 assert!(is_local_device);
190 assert_eq!(device_id, "my_id");
191 }
192 _ => unreachable!(),
193 };
194 }
195
196 #[test]
197 fn test_push_password_reset() {
198 let mut fxa =
199 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
200 let mut client = MockFxAClient::new();
201 client
202 .expect_check_refresh_token_status()
203 .with(always(), eq("refresh_token"))
204 .times(1)
205 .returning(|_, _| Ok(IntrospectResponse { active: false }));
206 fxa.set_client(Arc::new(client));
207 let refresh_token_scopes = std::collections::HashSet::new();
208 fxa.state.force_refresh_token(RefreshToken {
209 token: "refresh_token".to_owned(),
210 scopes: refresh_token_scopes,
211 });
212 fxa.state.force_current_device_id("my_id");
213 fxa.devices_cache = Some(CachedResponse {
214 response: vec![],
215 cached_at: 0,
216 etag: "".to_string(),
217 });
218 let json = "{\"version\":1,\"command\":\"fxaccounts:password_reset\"}";
219 assert!(fxa.devices_cache.is_some());
220 let event = fxa.handle_push_message(json).unwrap();
221 assert!(matches!(event, AccountEvent::AccountAuthStateChanged));
222 assert!(fxa.devices_cache.is_none());
223 }
224
225 #[test]
226 fn test_push_password_change() {
227 let mut fxa =
228 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
229 let mut client = MockFxAClient::new();
230 client
231 .expect_check_refresh_token_status()
232 .with(always(), eq("refresh_token"))
233 .times(1)
234 .returning(|_, _| Ok(IntrospectResponse { active: true }));
235 fxa.set_client(Arc::new(client));
236 let refresh_token_scopes = std::collections::HashSet::new();
237 fxa.state.force_refresh_token(RefreshToken {
238 token: "refresh_token".to_owned(),
239 scopes: refresh_token_scopes,
240 });
241 fxa.state.force_current_device_id("my_id");
242 fxa.devices_cache = Some(CachedResponse {
243 response: vec![],
244 cached_at: 0,
245 etag: "".to_string(),
246 });
247 let json = "{\"version\":1,\"command\":\"fxaccounts:password_changed\"}";
248 assert!(fxa.devices_cache.is_some());
249 let event = fxa.handle_push_message(json).unwrap();
250 assert!(matches!(event, AccountEvent::Unknown));
251 assert!(fxa.devices_cache.is_none());
252 }
253 #[test]
254 fn test_push_device_disconnected_remote() {
255 let mut fxa = FirefoxAccount::with_config(crate::internal::Config::stable_dev(
256 "12345678",
257 "https://foo.bar",
258 ));
259 let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"remote_id\"}}";
260 let event = fxa.handle_push_message(json).unwrap();
261 match event {
262 AccountEvent::DeviceDisconnected {
263 device_id,
264 is_local_device,
265 } => {
266 assert!(!is_local_device);
267 assert_eq!(device_id, "remote_id");
268 }
269 _ => unreachable!(),
270 };
271 }
272
273 #[test]
274 fn test_handle_push_message_ignores_unknown_command() {
275 let mut fxa =
276 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
277 let json = "{\"version\":1,\"command\":\"huh\"}";
278 let event = fxa.handle_push_message(json).unwrap();
279 assert!(matches!(event, AccountEvent::Unknown));
280 }
281
282 #[test]
283 fn test_handle_push_message_ignores_unknown_command_with_data() {
284 let mut fxa =
285 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
286 let json = "{\"version\":1,\"command\":\"huh\",\"data\":{\"value\":42}}";
287 let event = fxa.handle_push_message(json).unwrap();
288 assert!(matches!(event, AccountEvent::Unknown));
289 }
290
291 #[test]
292 fn test_handle_push_message_errors_on_garbage_data() {
293 let mut fxa =
294 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
295 let json = "{\"wtf\":\"bbq\"}";
296 fxa.handle_push_message(json).unwrap_err();
297 }
298}