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 let mut fxa =
172 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
173 let refresh_token_scopes = std::collections::HashSet::new();
174 fxa.state
175 .force_refresh_token(crate::internal::oauth::RefreshToken {
176 token: "refresh_token".to_owned(),
177 scopes: refresh_token_scopes,
178 });
179 fxa.state.force_current_device_id("my_id");
180 let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"my_id\"}}";
181 let event = fxa.handle_push_message(json).unwrap();
182 assert!(fxa.state.refresh_token().is_none());
183 match event {
184 AccountEvent::DeviceDisconnected {
185 device_id,
186 is_local_device,
187 } => {
188 assert!(is_local_device);
189 assert_eq!(device_id, "my_id");
190 }
191 _ => unreachable!(),
192 };
193 }
194
195 #[test]
196 fn test_push_password_reset() {
197 let mut fxa =
198 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
199 let mut client = MockFxAClient::new();
200 client
201 .expect_check_refresh_token_status()
202 .with(always(), eq("refresh_token"))
203 .times(1)
204 .returning(|_, _| Ok(IntrospectResponse { active: false }));
205 fxa.set_client(Arc::new(client));
206 let refresh_token_scopes = std::collections::HashSet::new();
207 fxa.state.force_refresh_token(RefreshToken {
208 token: "refresh_token".to_owned(),
209 scopes: refresh_token_scopes,
210 });
211 fxa.state.force_current_device_id("my_id");
212 fxa.devices_cache = Some(CachedResponse {
213 response: vec![],
214 cached_at: 0,
215 etag: "".to_string(),
216 });
217 let json = "{\"version\":1,\"command\":\"fxaccounts:password_reset\"}";
218 assert!(fxa.devices_cache.is_some());
219 let event = fxa.handle_push_message(json).unwrap();
220 assert!(matches!(event, AccountEvent::AccountAuthStateChanged));
221 assert!(fxa.devices_cache.is_none());
222 }
223
224 #[test]
225 fn test_push_password_change() {
226 let mut fxa =
227 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
228 let mut client = MockFxAClient::new();
229 client
230 .expect_check_refresh_token_status()
231 .with(always(), eq("refresh_token"))
232 .times(1)
233 .returning(|_, _| Ok(IntrospectResponse { active: true }));
234 fxa.set_client(Arc::new(client));
235 let refresh_token_scopes = std::collections::HashSet::new();
236 fxa.state.force_refresh_token(RefreshToken {
237 token: "refresh_token".to_owned(),
238 scopes: refresh_token_scopes,
239 });
240 fxa.state.force_current_device_id("my_id");
241 fxa.devices_cache = Some(CachedResponse {
242 response: vec![],
243 cached_at: 0,
244 etag: "".to_string(),
245 });
246 let json = "{\"version\":1,\"command\":\"fxaccounts:password_changed\"}";
247 assert!(fxa.devices_cache.is_some());
248 let event = fxa.handle_push_message(json).unwrap();
249 assert!(matches!(event, AccountEvent::Unknown));
250 assert!(fxa.devices_cache.is_none());
251 }
252 #[test]
253 fn test_push_device_disconnected_remote() {
254 let mut fxa = FirefoxAccount::with_config(crate::internal::Config::stable_dev(
255 "12345678",
256 "https://foo.bar",
257 ));
258 let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"remote_id\"}}";
259 let event = fxa.handle_push_message(json).unwrap();
260 match event {
261 AccountEvent::DeviceDisconnected {
262 device_id,
263 is_local_device,
264 } => {
265 assert!(!is_local_device);
266 assert_eq!(device_id, "remote_id");
267 }
268 _ => unreachable!(),
269 };
270 }
271
272 #[test]
273 fn test_handle_push_message_ignores_unknown_command() {
274 let mut fxa =
275 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
276 let json = "{\"version\":1,\"command\":\"huh\"}";
277 let event = fxa.handle_push_message(json).unwrap();
278 assert!(matches!(event, AccountEvent::Unknown));
279 }
280
281 #[test]
282 fn test_handle_push_message_ignores_unknown_command_with_data() {
283 let mut fxa =
284 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
285 let json = "{\"version\":1,\"command\":\"huh\",\"data\":{\"value\":42}}";
286 let event = fxa.handle_push_message(json).unwrap();
287 assert!(matches!(event, AccountEvent::Unknown));
288 }
289
290 #[test]
291 fn test_handle_push_message_errors_on_garbage_data() {
292 let mut fxa =
293 FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
294 let json = "{\"wtf\":\"bbq\"}";
295 fxa.handle_push_message(json).unwrap_err();
296 }
297}