1use std::mem;
6
7use payload_support::Fit;
8
9use super::{
10 commands::{
11 close_tabs::{self, CloseTabsPayload},
12 decrypt_command, encrypt_command, IncomingDeviceCommand, PrivateCommandKeys,
13 },
14 device::COMMAND_MAX_PAYLOAD_SIZE,
15 http_client::GetDeviceResponse,
16 scopes, telemetry, FirefoxAccount,
17};
18use crate::{warn, CloseTabsResult, Error, Result};
19
20impl FirefoxAccount {
21 pub fn close_tabs<T>(&mut self, target_device_id: &str, urls: Vec<T>) -> Result<CloseTabsResult>
22 where
23 T: Into<String>,
24 {
25 let devices = self.get_devices(false)?;
26 let target = devices
27 .iter()
28 .find(|d| d.id == target_device_id)
29 .ok_or_else(|| Error::UnknownTargetDevice(target_device_id.to_owned()))?;
30
31 let sent_telemetry = telemetry::SentCommand::for_close_tabs();
32 let mut urls_to_retry = Vec::new();
33
34 let mut urls: Vec<_> = urls.into_iter().map(Into::into).collect();
38 urls.sort_unstable_by_key(String::len);
39
40 while !urls.is_empty() {
41 let chunk = match payload_support::try_fit_items(&urls, COMMAND_MAX_PAYLOAD_SIZE.get())
47 {
48 Fit::All => mem::take(&mut urls),
49 Fit::Some(count) => urls.drain(..count.get()).collect(),
50 Fit::None | Fit::Err(_) => {
51 urls_to_retry.append(&mut urls);
59 break;
60 }
61 };
62
63 let sent_telemetry = sent_telemetry.clone_with_new_stream_id();
64 let payload = CloseTabsPayload::with_telemetry(&sent_telemetry, chunk);
65
66 let oldsync_key = self.get_scoped_key(scopes::OLD_SYNC)?;
67 let command_payload =
68 encrypt_command(oldsync_key, target, close_tabs::COMMAND_NAME, &payload)?;
69 let result = self.invoke_command(
70 close_tabs::COMMAND_NAME,
71 target,
72 &command_payload,
73 Some(close_tabs::COMMAND_TTL),
74 );
75 match result {
76 Ok(()) => {
77 self.telemetry.record_command_sent(sent_telemetry);
78 }
79 Err(e) => {
80 error_support::report_error!(
81 "fxaclient-close-tabs-invoke",
82 "Failed to send bulk Close Tabs command: {}",
83 e
84 );
85 urls_to_retry.extend(payload.urls);
88 }
89 }
90 }
91
92 Ok(if urls_to_retry.is_empty() {
93 CloseTabsResult::Ok
94 } else {
95 CloseTabsResult::TabsNotClosed {
96 urls: urls_to_retry,
97 }
98 })
99 }
100
101 pub(crate) fn handle_close_tabs_command(
102 &mut self,
103 sender: Option<GetDeviceResponse>,
104 payload: serde_json::Value,
105 reason: telemetry::ReceivedReason,
106 ) -> Result<IncomingDeviceCommand> {
107 let close_tabs_key: PrivateCommandKeys = match self.close_tabs_key() {
108 Some(s) => PrivateCommandKeys::deserialize(s)?,
109 None => {
110 return Err(Error::IllegalState(
111 "Cannot find Close Remote Tabs keys. Has initialize_device been called before?",
112 ));
113 }
114 };
115 match decrypt_command(payload, &close_tabs_key) {
116 Ok(payload) => {
117 let recd_telemetry = telemetry::ReceivedCommand::for_close_tabs(&payload, reason);
118 self.telemetry.record_command_received(recd_telemetry);
119 Ok(IncomingDeviceCommand::TabsClosed { sender, payload })
120 }
121 Err(e) => {
122 warn!("Could not decrypt Close Remote Tabs payload. Diagnosing then resetting the Close Tabs keys.");
123 self.clear_close_tabs_keys();
124 self.reregister_current_capabilities()?;
125 Err(e)
126 }
127 }
128 }
129
130 pub(crate) fn load_or_generate_close_tabs_keys(&mut self) -> Result<PrivateCommandKeys> {
131 if let Some(s) = self.close_tabs_key() {
132 match PrivateCommandKeys::deserialize(s) {
133 Ok(keys) => return Ok(keys),
134 Err(_) => {
135 error_support::report_error!(
136 "fxaclient-close-tabs-key-deserialize",
137 "Could not deserialize Close Remote Tabs keys. Re-creating them."
138 );
139 }
140 }
141 }
142 let keys = PrivateCommandKeys::from_random()?;
143 self.set_close_tabs_key(keys.serialize()?);
144 Ok(keys)
145 }
146
147 fn close_tabs_key(&self) -> Option<&str> {
148 self.state.get_commands_data(close_tabs::COMMAND_NAME)
149 }
150
151 fn set_close_tabs_key(&mut self, key: String) {
152 self.state.set_commands_data(close_tabs::COMMAND_NAME, key)
153 }
154
155 fn clear_close_tabs_keys(&mut self) {
156 self.state.clear_commands_data(close_tabs::COMMAND_NAME);
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 use std::{collections::HashSet, sync::Arc};
165
166 use mockall::predicate::{always, eq};
167 use nss::ensure_initialized;
168 use serde_json::json;
169
170 use crate::{
171 internal::{
172 commands::PublicCommandKeys, config::Config, http_client::MockFxAClient,
173 oauth::RefreshToken, util, CachedResponse, FirefoxAccount,
174 },
175 ScopedKey,
176 };
177
178 struct OverrideCommandMaxPayloadSize(usize);
181
182 impl OverrideCommandMaxPayloadSize {
183 pub fn with_new_size(new_size: usize) -> Self {
184 Self(COMMAND_MAX_PAYLOAD_SIZE.replace(new_size))
185 }
186 }
187
188 impl Drop for OverrideCommandMaxPayloadSize {
189 fn drop(&mut self) {
190 COMMAND_MAX_PAYLOAD_SIZE.set(self.0)
191 }
192 }
193
194 fn setup() -> FirefoxAccount {
195 ensure_initialized();
196 let config = Config::stable_dev("12345678", "https://foo.bar");
197 let mut fxa = FirefoxAccount::with_config(config);
198 fxa.state.force_refresh_token(RefreshToken {
199 token: "refreshtok".to_owned(),
200 scopes: HashSet::default(),
201 });
202 fxa.state.insert_scoped_key(scopes::OLD_SYNC, ScopedKey {
203 kty: "oct".to_string(),
204 scope: "https://identity.mozilla.com/apps/oldsync".to_string(),
205 k: "kMtwpVC0ZaYFJymPza8rXK_0CgCp3KMwRStwGfBRBDtL6hXRDVJgQFaoOQ2dimw0Bko5WVv2gNTy7RX5zFYZHg".to_string(),
206 kid: "1542236016429-Ox1FbJfFfwTe5t-xq4v2hQ".to_string(),
207 });
208 fxa
209 }
210
211 #[test]
215 fn test_close_tabs_send_one() -> Result<()> {
216 let _o = OverrideCommandMaxPayloadSize::with_new_size(2048);
217
218 let mut fxa = setup();
219 let close_tabs_keys = PrivateCommandKeys::from_random()?;
220 let devices = json!([
221 {
222 "id": "device0102",
223 "name": "Emerald",
224 "isCurrentDevice": false,
225 "location": {},
226 "availableCommands": {
227 close_tabs::COMMAND_NAME: PublicCommandKeys::as_command_data(
228 &close_tabs_keys.clone().into(),
229 fxa.state.get_scoped_key(scopes::OLD_SYNC).unwrap(),
230 )?,
231 },
232 "pushEndpointExpired": false,
233 },
234 ]);
235 fxa.devices_cache = Some(CachedResponse {
236 response: serde_json::from_value(devices)?,
237 cached_at: util::now(),
238 etag: "".into(),
239 });
240 fxa.set_close_tabs_key(close_tabs_keys.serialize()?);
241
242 let mut client = MockFxAClient::new();
243 client
244 .expect_invoke_command()
245 .once()
246 .with(
247 always(),
248 always(),
249 always(),
250 eq("device0102"),
251 always(),
252 always(),
253 )
254 .returning(|_, _, _, _, _, _| Ok(()));
255 fxa.set_client(Arc::new(client));
256
257 assert_eq!(
259 fxa.close_tabs("device0102", vec!["https://example.com"])?,
260 CloseTabsResult::Ok
261 );
262
263 Ok(())
264 }
265
266 #[test]
267 fn test_close_tabs_send_two() -> Result<()> {
268 let _o = OverrideCommandMaxPayloadSize::with_new_size(2048);
269
270 let mut fxa = setup();
271 let close_tabs_keys = PrivateCommandKeys::from_random()?;
272 let devices = json!([
273 {
274 "id": "device0304",
275 "name": "Sapphire",
276 "isCurrentDevice": false,
277 "location": {},
278 "availableCommands": {
279 close_tabs::COMMAND_NAME: PublicCommandKeys::as_command_data(
280 &close_tabs_keys.clone().into(),
281 fxa.state.get_scoped_key(scopes::OLD_SYNC).unwrap(),
282 )?,
283 },
284 "pushEndpointExpired": false,
285 },
286 ]);
287 fxa.devices_cache = Some(CachedResponse {
288 response: serde_json::from_value(devices)?,
289 cached_at: util::now(),
290 etag: "".into(),
291 });
292 fxa.set_close_tabs_key(close_tabs_keys.serialize()?);
293
294 let mut client = MockFxAClient::new();
295 client
296 .expect_invoke_command()
297 .times(2)
298 .with(
299 always(),
300 always(),
301 always(),
302 eq("device0304"),
303 always(),
304 always(),
305 )
306 .returning(|_, _, _, _, _, _| Ok(()));
307 fxa.set_client(Arc::new(client));
308
309 assert_eq!(
311 fxa.close_tabs(
312 "device0304",
313 vec!["https://example.com", "https://example.org"],
314 )?,
315 CloseTabsResult::Ok
316 );
317
318 Ok(())
319 }
320
321 #[test]
322 fn test_close_tabs_all_fail() -> Result<()> {
323 let _o = OverrideCommandMaxPayloadSize::with_new_size(2048);
324
325 let mut fxa = setup();
326 let close_tabs_keys = PrivateCommandKeys::from_random()?;
327 let devices = json!([
328 {
329 "id": "device0506",
330 "name": "Ruby",
331 "isCurrentDevice": false,
332 "location": {},
333 "availableCommands": {
334 close_tabs::COMMAND_NAME: PublicCommandKeys::as_command_data(
335 &close_tabs_keys.clone().into(),
336 fxa.state.get_scoped_key(scopes::OLD_SYNC).unwrap(),
337 )?,
338 },
339 "pushEndpointExpired": false,
340 },
341 ]);
342 fxa.devices_cache = Some(CachedResponse {
343 response: serde_json::from_value(devices)?,
344 cached_at: util::now(),
345 etag: "".into(),
346 });
347 fxa.set_close_tabs_key(close_tabs_keys.serialize()?);
348
349 let mut client = MockFxAClient::new();
350 client
351 .expect_invoke_command()
352 .times(3)
353 .with(
354 always(),
355 always(),
356 always(),
357 eq("device0506"),
358 always(),
359 always(),
360 )
361 .returning(|_, _, _, _, _, _| {
362 Err(Error::RequestError(viaduct::Error::NetworkError(
363 "Simulated error".to_owned(),
364 )))
365 });
366 fxa.set_client(Arc::new(client));
367
368 assert_eq!(
370 fxa.close_tabs(
371 "device0506",
372 vec![
373 "https://example.com",
374 "https://example.org",
375 "https://example.net",
376 ],
377 )?,
378 CloseTabsResult::TabsNotClosed {
379 urls: vec![
380 "https://example.com".into(),
381 "https://example.org".into(),
382 "https://example.net".into(),
383 ]
384 }
385 );
386
387 Ok(())
388 }
389
390 #[test]
391 fn test_close_tabs_one_fails() -> Result<()> {
392 let _o = OverrideCommandMaxPayloadSize::with_new_size(2048);
393
394 let mut fxa = setup();
395 let close_tabs_keys = PrivateCommandKeys::from_random()?;
396 let devices = json!([
397 {
398 "id": "device0708",
399 "name": "Agate",
400 "isCurrentDevice": false,
401 "location": {},
402 "availableCommands": {
403 close_tabs::COMMAND_NAME: PublicCommandKeys::as_command_data(
404 &close_tabs_keys.clone().into(),
405 fxa.state.get_scoped_key(scopes::OLD_SYNC).unwrap(),
406 )?,
407 },
408 "pushEndpointExpired": false,
409 },
410 ]);
411 fxa.devices_cache = Some(CachedResponse {
412 response: serde_json::from_value(devices)?,
413 cached_at: util::now(),
414 etag: "".into(),
415 });
416 fxa.set_close_tabs_key(close_tabs_keys.serialize()?);
417
418 let mut client = MockFxAClient::new();
419 client
420 .expect_invoke_command()
421 .times(3)
422 .with(
423 always(),
424 always(),
425 always(),
426 eq("device0708"),
427 always(),
428 always(),
429 )
430 .returning(move |_, _, _, _, value, _| {
433 let payload: CloseTabsPayload = decrypt_command(value.clone(), &close_tabs_keys)?;
434 if payload.urls.iter().any(|url| url == "https://example.org") {
435 Err(Error::RequestError(viaduct::Error::NetworkError(
436 "Simulated error".to_owned(),
437 )))
438 } else {
439 Ok(())
440 }
441 });
442 fxa.set_client(Arc::new(client));
443
444 assert_eq!(
446 fxa.close_tabs(
447 "device0708",
448 vec![
449 "https://example.com",
450 "https://example.org",
451 "https://example.net",
452 ],
453 )?,
454 CloseTabsResult::TabsNotClosed {
455 urls: vec!["https://example.org".into()]
456 }
457 );
458
459 Ok(())
460 }
461
462 #[test]
463 fn test_close_tabs_never_sent() -> Result<()> {
464 let _p = OverrideCommandMaxPayloadSize::with_new_size(0);
467
468 let mut fxa = setup();
469 let close_tabs_keys = PrivateCommandKeys::from_random()?;
470 let devices = json!([
471 {
472 "id": "device0910",
473 "name": "Amethyst",
474 "isCurrentDevice": false,
475 "location": {},
476 "availableCommands": {
477 close_tabs::COMMAND_NAME: PublicCommandKeys::as_command_data(
478 &close_tabs_keys.clone().into(),
479 fxa.state.get_scoped_key(scopes::OLD_SYNC).unwrap(),
480 )?,
481 },
482 "pushEndpointExpired": false,
483 },
484 ]);
485 fxa.devices_cache = Some(CachedResponse {
486 response: serde_json::from_value(devices)?,
487 cached_at: util::now(),
488 etag: "".into(),
489 });
490 fxa.set_close_tabs_key(close_tabs_keys.serialize()?);
491
492 let mut client = MockFxAClient::new();
493 client.expect_invoke_command().never().with(
494 always(),
495 always(),
496 always(),
497 eq("device0910"),
498 always(),
499 always(),
500 );
501 fxa.set_client(Arc::new(client));
502
503 assert_eq!(
504 fxa.close_tabs("device0910", vec!["https://example.com"])?,
505 CloseTabsResult::TabsNotClosed {
506 urls: vec!["https://example.com".into()]
507 }
508 );
509
510 Ok(())
511 }
512
513 #[test]
514 fn test_close_tabs_two_per_command() -> Result<()> {
515 let _q = OverrideCommandMaxPayloadSize::with_new_size(2088);
517
518 let mut fxa = setup();
519 let close_tabs_keys = PrivateCommandKeys::from_random()?;
520 let devices = json!([
521 {
522 "id": "device1112",
523 "name": "Diamond",
524 "isCurrentDevice": false,
525 "location": {},
526 "availableCommands": {
527 close_tabs::COMMAND_NAME: PublicCommandKeys::as_command_data(
528 &close_tabs_keys.clone().into(),
529 fxa.state.get_scoped_key(scopes::OLD_SYNC).unwrap(),
530 )?,
531 },
532 "pushEndpointExpired": false,
533 },
534 ]);
535 fxa.devices_cache = Some(CachedResponse {
536 response: serde_json::from_value(devices)?,
537 cached_at: util::now(),
538 etag: "".into(),
539 });
540 fxa.set_close_tabs_key(close_tabs_keys.serialize()?);
541
542 let mut client = MockFxAClient::new();
543 client
544 .expect_invoke_command()
545 .times(2)
546 .with(
547 always(),
548 always(),
549 always(),
550 eq("device1112"),
551 always(),
552 always(),
553 )
554 .returning(|_, _, _, _, _, _| Ok(()));
555 fxa.set_client(Arc::new(client));
556
557 assert_eq!(
558 fxa.close_tabs(
559 "device1112",
560 vec![
561 "https://example.com/abcdefghi",
562 "https://example.org/jklmnopqr",
563 "https://example.net/stuvwxyza",
564 "https://example.edu/bcdefghij",
565 ],
566 )?,
567 CloseTabsResult::Ok
568 );
569
570 Ok(())
571 }
572}