use std::collections::{HashMap, HashSet};
pub use super::http_client::{
DeviceLocation as Location, GetDeviceResponse as Device, PushSubscription,
};
use super::{
commands::{self, IncomingDeviceCommand},
http_client::{DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand},
telemetry, util, CachedResponse, FirefoxAccount,
};
use crate::{DeviceCapability, Error, Result};
use sync15::DeviceType;
const DEVICES_FRESHNESS_THRESHOLD: u64 = 60_000; #[derive(Clone, Copy)]
pub enum CommandFetchReason {
Poll,
Push(u64),
}
impl FirefoxAccount {
pub fn get_devices(&mut self, ignore_cache: bool) -> Result<Vec<Device>> {
if let Some(d) = &self.devices_cache {
if !ignore_cache && util::now() < d.cached_at + DEVICES_FRESHNESS_THRESHOLD {
return Ok(d.response.clone());
}
}
let refresh_token = self.get_refresh_token()?;
let response = self
.client
.get_devices(self.state.config(), refresh_token)?;
self.devices_cache = Some(CachedResponse {
response: response.clone(),
cached_at: util::now(),
etag: "".into(),
});
Ok(response)
}
pub fn get_current_device(&mut self) -> Result<Option<Device>> {
Ok(self
.get_devices(false)?
.into_iter()
.find(|d| d.is_current_device))
}
fn register_capabilities(
&mut self,
capabilities: &[DeviceCapability],
) -> Result<HashMap<String, String>> {
let mut capabilities_set = HashSet::new();
let mut commands = HashMap::new();
for capability in capabilities {
match capability {
DeviceCapability::SendTab => {
let send_tab_command = self.generate_send_tab_command_data()?;
commands.insert(
commands::send_tab::COMMAND_NAME.to_owned(),
send_tab_command.to_owned(),
);
capabilities_set.insert(DeviceCapability::SendTab);
}
}
}
self.state
.update_last_sent_device_capabilities(capabilities_set);
Ok(commands)
}
pub fn initialize_device(
&mut self,
name: &str,
device_type: DeviceType,
capabilities: &[DeviceCapability],
) -> Result<()> {
let commands = self.register_capabilities(capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.display_name(name)
.device_type(&device_type)
.available_commands(&commands)
.build();
self.update_device(update)
}
pub fn ensure_capabilities(&mut self, capabilities: &[DeviceCapability]) -> Result<()> {
if !self.state.last_sent_device_capabilities().is_empty()
&& self.state.last_sent_device_capabilities().len() == capabilities.len()
&& capabilities
.iter()
.all(|c| self.state.last_sent_device_capabilities().contains(c))
{
return Ok(());
}
let commands = self.register_capabilities(capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.available_commands(&commands)
.build();
self.update_device(update)
}
pub(crate) fn reregister_current_capabilities(&mut self) -> Result<()> {
let current_capabilities: Vec<DeviceCapability> = self
.state
.last_sent_device_capabilities()
.clone()
.into_iter()
.collect();
let commands = self.register_capabilities(¤t_capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.available_commands(&commands)
.build();
self.update_device(update)?;
Ok(())
}
pub(crate) fn invoke_command(
&self,
command: &str,
target: &Device,
payload: &serde_json::Value,
) -> Result<()> {
let refresh_token = self.get_refresh_token()?;
self.client.invoke_command(
self.state.config(),
refresh_token,
command,
&target.id,
payload,
)
}
pub fn poll_device_commands(
&mut self,
reason: CommandFetchReason,
) -> Result<Vec<IncomingDeviceCommand>> {
let last_command_index = self.state.last_handled_command_index().unwrap_or(0);
self.fetch_and_parse_commands(last_command_index + 1, None, reason)
}
pub fn get_command_for_index(&mut self, index: u64) -> Result<IncomingDeviceCommand> {
let refresh_token = self.get_refresh_token()?;
let pending_commands =
self.client
.get_pending_commands(self.state.config(), refresh_token, index, Some(1))?;
self.parse_commands_messages(pending_commands.messages, CommandFetchReason::Push(index))?
.into_iter()
.next()
.ok_or_else(|| Error::CommandNotFound)
}
fn fetch_and_parse_commands(
&mut self,
index: u64,
limit: Option<u64>,
reason: CommandFetchReason,
) -> Result<Vec<IncomingDeviceCommand>> {
let refresh_token = self.get_refresh_token()?;
let pending_commands =
self.client
.get_pending_commands(self.state.config(), refresh_token, index, limit)?;
if pending_commands.messages.is_empty() {
return Ok(Vec::new());
}
log::info!("Handling {} messages", pending_commands.messages.len());
let device_commands = self.parse_commands_messages(pending_commands.messages, reason)?;
self.state
.set_last_handled_command_index(pending_commands.index);
Ok(device_commands)
}
fn parse_commands_messages(
&mut self,
messages: Vec<PendingCommand>,
reason: CommandFetchReason,
) -> Result<Vec<IncomingDeviceCommand>> {
let devices = self.get_devices(false)?;
let parsed_commands = messages
.into_iter()
.filter_map(|msg| match self.parse_command(msg, &devices, reason) {
Ok(device_command) => Some(device_command),
Err(e) => {
error_support::report_error!(
"fxaclient-command",
"Error while processing command: {}",
e
);
None
}
})
.collect();
Ok(parsed_commands)
}
fn parse_command(
&mut self,
command: PendingCommand,
devices: &[Device],
reason: CommandFetchReason,
) -> Result<IncomingDeviceCommand> {
let telem_reason = match reason {
CommandFetchReason::Poll => telemetry::ReceivedReason::Poll,
CommandFetchReason::Push(index) if command.index < index => {
telemetry::ReceivedReason::PushMissed
}
_ => telemetry::ReceivedReason::Push,
};
let command_data = command.data;
let sender = command_data
.sender
.and_then(|s| devices.iter().find(|i| i.id == s).cloned());
match command_data.command.as_str() {
commands::send_tab::COMMAND_NAME => {
self.handle_send_tab_command(sender, command_data.payload, telem_reason)
}
_ => Err(Error::UnknownCommand(command_data.command)),
}
}
pub fn set_device_name(&mut self, name: &str) -> Result<()> {
let update = DeviceUpdateRequestBuilder::new().display_name(name).build();
self.update_device(update)
}
pub fn clear_device_name(&mut self) -> Result<()> {
let update = DeviceUpdateRequestBuilder::new()
.clear_display_name()
.build();
self.update_device(update)
}
pub fn set_push_subscription(&mut self, push_subscription: PushSubscription) -> Result<()> {
let update = DeviceUpdateRequestBuilder::new()
.push_subscription(&push_subscription)
.build();
self.update_device(update)
}
pub(crate) fn replace_device(
&mut self,
display_name: &str,
device_type: &DeviceType,
push_subscription: &Option<PushSubscription>,
commands: &HashMap<String, String>,
) -> Result<()> {
self.state.clear_last_sent_device_capabilities();
let mut builder = DeviceUpdateRequestBuilder::new()
.display_name(display_name)
.device_type(device_type)
.available_commands(commands);
if let Some(push_subscription) = push_subscription {
builder = builder.push_subscription(push_subscription)
}
self.update_device(builder.build())
}
fn update_device(&mut self, update: DeviceUpdateRequest<'_>) -> Result<()> {
let refresh_token = self.get_refresh_token()?;
let res = self
.client
.update_device_record(self.state.config(), refresh_token, update);
match res {
Ok(resp) => {
self.state.set_current_device_id(resp.id);
Ok(())
}
Err(err) => {
self.state.clear_last_sent_device_capabilities();
Err(err)
}
}
}
pub fn get_current_device_id(&mut self) -> Result<String> {
match self.state.current_device_id() {
Some(ref device_id) => Ok(device_id.to_string()),
None => Err(Error::NoCurrentDeviceId),
}
}
}
impl TryFrom<Device> for crate::Device {
type Error = Error;
fn try_from(d: Device) -> Result<Self> {
let capabilities: Vec<_> = d
.available_commands
.keys()
.filter_map(|k| match k.as_str() {
commands::send_tab::COMMAND_NAME => Some(DeviceCapability::SendTab),
_ => None,
})
.map(Into::into)
.collect();
Ok(crate::Device {
id: d.common.id,
display_name: d.common.display_name,
device_type: d.common.device_type,
capabilities,
push_subscription: d.common.push_subscription.map(Into::into),
push_endpoint_expired: d.common.push_endpoint_expired,
is_current_device: d.is_current_device,
last_access_time: d.last_access_time.map(TryFrom::try_from).transpose()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::internal::http_client::*;
use crate::internal::oauth::RefreshToken;
use crate::internal::Config;
use crate::ScopedKey;
use std::collections::HashSet;
use std::sync::Arc;
fn setup() -> FirefoxAccount {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.state.force_refresh_token(RefreshToken {
token: "refreshtok".to_string(),
scopes: HashSet::default(),
});
fxa.state.insert_scoped_key("https://identity.mozilla.com/apps/oldsync", ScopedKey {
kty: "oct".to_string(),
scope: "https://identity.mozilla.com/apps/oldsync".to_string(),
k: "kMtwpVC0ZaYFJymPza8rXK_0CgCp3KMwRStwGfBRBDtL6hXRDVJgQFaoOQ2dimw0Bko5WVv2gNTy7RX5zFYZHg".to_string(),
kid: "1542236016429-Ox1FbJfFfwTe5t-xq4v2hQ".to_string(),
});
fxa
}
#[test]
fn test_ensure_capabilities_does_not_hit_the_server_if_nothing_has_changed() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
let saved = fxa.to_json().unwrap();
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
restored.set_client(Arc::new(FxAClientMock::new()));
restored
.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
}
#[test]
fn test_ensure_capabilities_updates_the_server_if_capabilities_increase() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[]).unwrap();
let saved = fxa.to_json().unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
restored.set_client(Arc::new(client));
restored
.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
}
#[test]
fn test_ensure_capabilities_updates_the_server_if_capabilities_reduce() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
let saved = fxa.to_json().unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[]).unwrap();
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
restored.set_client(Arc::new(client));
restored.ensure_capabilities(&[]).unwrap();
}
#[test]
fn test_ensure_capabilities_will_reregister_after_new_login_flow() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
let mut client = FxAClientMock::new();
client
.expect_destroy_access_token(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(Error::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}));
client
.expect_get_devices(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(Error::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}));
client
.expect_destroy_refresh_token(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(Error::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}));
fxa.set_client(Arc::new(client));
fxa.handle_oauth_response(
OAuthTokenResponse {
keys_jwe: None,
refresh_token: Some("newRefreshTok".to_string()),
session_token: None,
expires_in: 12345,
scope: "profile".to_string(),
access_token: "accesstok".to_string(),
},
None,
)
.unwrap();
assert!(fxa.state.last_sent_device_capabilities().is_empty());
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("newRefreshTok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
}
#[test]
fn test_ensure_capabilities_updates_the_server_if_previous_attempt_failed() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Err(Error::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap_err();
let mut client = FxAClientMock::new();
client
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Ok(UpdateDeviceResponse {
id: "device1".to_string(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
}));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[DeviceCapability::SendTab])
.unwrap();
}
#[test]
fn test_get_devices() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_get_devices(mockiato::Argument::any, mockiato::Argument::any)
.times(1)
.returns_once(Ok(vec![Device {
common: DeviceResponseCommon {
id: "device1".into(),
display_name: "".to_string(),
device_type: DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::new(),
push_endpoint_expired: true,
},
is_current_device: true,
location: DeviceLocation {
city: None,
country: None,
state: None,
state_code: None,
},
last_access_time: None,
}]));
fxa.set_client(Arc::new(client));
assert!(fxa.devices_cache.is_none());
assert!(fxa.get_devices(false).is_ok());
assert!(fxa.devices_cache.is_some());
let cache = fxa.devices_cache.clone().unwrap();
assert!(!cache.response.is_empty());
assert!(cache.cached_at > 0);
let cached_devices = cache.response;
assert_eq!(cached_devices[0].id, "device1".to_string());
assert!(fxa.get_devices(false).is_ok());
assert!(fxa.devices_cache.is_some());
let cache2 = fxa.devices_cache.unwrap();
let cached_devices2 = cache2.response;
assert_eq!(cache.cached_at, cache2.cached_at);
assert_eq!(cached_devices.len(), cached_devices2.len());
assert_eq!(cached_devices[0].id, cached_devices2[0].id);
}
#[test]
fn test_get_devices_network_errors() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_get_devices(mockiato::Argument::any, mockiato::Argument::any)
.times(1)
.returns_once(Err(Error::RemoteError {
code: 500,
errno: 101,
error: "Did not work!".to_owned(),
message: "Did not work!".to_owned(),
info: "Did not work!".to_owned(),
}));
fxa.set_client(Arc::new(client));
assert!(fxa.devices_cache.is_none());
let res = fxa.get_devices(false);
assert!(res.is_err());
assert!(fxa.devices_cache.is_none());
}
}