pub use crate::http_client::{
DeviceLocation as Location, DeviceType as Type, GetDeviceResponse as Device, PushSubscription,
};
use crate::{
commands,
error::*,
http_client::{
DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand, UpdateDeviceResponse,
},
telemetry, util, CachedResponse, FirefoxAccount, IncomingDeviceCommand,
};
use serde_derive::*;
use std::collections::{HashMap, HashSet};
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.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: &[Capability],
) -> Result<HashMap<String, String>> {
let mut capabilities_set = HashSet::new();
let mut commands = HashMap::new();
for capability in capabilities {
match capability {
Capability::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(Capability::SendTab);
}
}
}
self.state.device_capabilities = capabilities_set;
Ok(commands)
}
pub fn initialize_device(
&mut self,
name: &str,
device_type: Type,
capabilities: &[Capability],
) -> Result<()> {
let commands = self.register_capabilities(capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.display_name(name)
.device_type(&device_type)
.available_commands(&commands)
.build();
let resp = self.update_device(update)?;
self.state.current_device_id = Option::from(resp.id);
Ok(())
}
pub fn ensure_capabilities(&mut self, capabilities: &[Capability]) -> Result<()> {
if !self.state.device_capabilities.is_empty()
&& self.state.device_capabilities.len() == capabilities.len()
&& capabilities
.iter()
.all(|c| self.state.device_capabilities.contains(c))
{
return Ok(());
}
let commands = self.register_capabilities(capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.available_commands(&commands)
.build();
let resp = self.update_device(update)?;
self.state.current_device_id = Option::from(resp.id);
Ok(())
}
pub(crate) fn reregister_current_capabilities(&mut self) -> Result<()> {
let current_capabilities: Vec<Capability> =
self.state.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.unwrap_or(0);
self.fetch_and_parse_commands(last_command_index + 1, None, reason)
}
pub fn ios_fetch_device_command(&mut self, index: u64) -> Result<IncomingDeviceCommand> {
let mut device_commands =
self.fetch_and_parse_commands(index, Some(1), CommandFetchReason::Push(index))?;
let device_command = device_commands
.pop()
.ok_or_else(|| ErrorKind::IllegalState("Index fetch came out empty."))?;
if !device_commands.is_empty() {
log::warn!("Index fetch resulted in more than 1 element");
}
Ok(device_command)
}
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
.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.last_handled_command = Some(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) => {
log::error!("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(ErrorKind::UnknownCommand(command_data.command).into()),
}
}
pub fn set_device_name(&mut self, name: &str) -> Result<UpdateDeviceResponse> {
let update = DeviceUpdateRequestBuilder::new().display_name(name).build();
self.update_device(update)
}
pub fn clear_device_name(&mut self) -> Result<UpdateDeviceResponse> {
let update = DeviceUpdateRequestBuilder::new()
.clear_display_name()
.build();
self.update_device(update)
}
pub fn set_push_subscription(
&mut self,
push_subscription: &PushSubscription,
) -> Result<UpdateDeviceResponse> {
let update = DeviceUpdateRequestBuilder::new()
.push_subscription(&push_subscription)
.build();
self.update_device(update)
}
#[allow(dead_code)]
pub(crate) fn register_command(
&mut self,
command: &str,
value: &str,
) -> Result<UpdateDeviceResponse> {
self.state.device_capabilities.clear();
let mut commands = HashMap::new();
commands.insert(command.to_owned(), value.to_owned());
let update = DeviceUpdateRequestBuilder::new()
.available_commands(&commands)
.build();
self.update_device(update)
}
#[allow(dead_code)]
pub(crate) fn unregister_command(&mut self, _: &str) -> Result<UpdateDeviceResponse> {
self.state.device_capabilities.clear();
let commands = HashMap::new();
let update = DeviceUpdateRequestBuilder::new()
.available_commands(&commands)
.build();
self.update_device(update)
}
#[allow(dead_code)]
pub(crate) fn clear_commands(&mut self) -> Result<UpdateDeviceResponse> {
self.state.device_capabilities.clear();
let update = DeviceUpdateRequestBuilder::new()
.clear_available_commands()
.build();
self.update_device(update)
}
pub(crate) fn replace_device(
&mut self,
display_name: &str,
device_type: &Type,
push_subscription: &Option<PushSubscription>,
commands: &HashMap<String, String>,
) -> Result<UpdateDeviceResponse> {
self.state.device_capabilities.clear();
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<UpdateDeviceResponse> {
let refresh_token = self.get_refresh_token()?;
let res = self
.client
.update_device(&self.state.config, refresh_token, update);
match res {
Ok(resp) => Ok(resp),
Err(err) => {
self.state.device_capabilities.clear();
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(ErrorKind::NoCurrentDeviceId.into()),
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Capability {
SendTab,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http_client::*;
use crate::oauth::RefreshToken;
use crate::scoped_keys::ScopedKey;
use crate::Config;
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.refresh_token = Some(RefreshToken {
token: "refreshtok".to_string(),
scopes: HashSet::default(),
});
fxa.state.scoped_keys.insert("https://identity.mozilla.com/apps/oldsync".to_string(), 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(
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(&[Capability::SendTab]).unwrap();
let saved = fxa.to_json().unwrap();
fxa.ensure_capabilities(&[Capability::SendTab]).unwrap();
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
restored.set_client(Arc::new(FxAClientMock::new()));
restored
.ensure_capabilities(&[Capability::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(
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(
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(&[Capability::SendTab]).unwrap();
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device(
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(&[Capability::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(
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(&[Capability::SendTab]).unwrap();
let saved = fxa.to_json().unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device(
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(
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(
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(&[Capability::SendTab]).unwrap();
let mut client = FxAClientMock::new();
client
.expect_destroy_access_token(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}
.into()));
client
.expect_devices(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}
.into()));
client
.expect_destroy_refresh_token(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}
.into()));
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.device_capabilities.is_empty());
let mut client = FxAClientMock::new();
client
.expect_update_device(
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(&[Capability::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(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 999,
error: "server error".to_string(),
message: "this will be ignored anyway".to_string(),
info: "".to_string(),
}
.into()));
fxa.set_client(Arc::new(client));
fxa.ensure_capabilities(&[Capability::SendTab]).unwrap_err();
let mut client = FxAClientMock::new();
client
.expect_update_device(
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(&[Capability::SendTab]).unwrap();
}
#[test]
fn test_get_devices() {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_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_devices(mockiato::Argument::any, mockiato::Argument::any)
.times(1)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 101,
error: "Did not work!".to_owned(),
message: "Did not work!".to_owned(),
info: "Did not work!".to_owned(),
}
.into()));
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());
}
}