diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /services/fxaccounts/rust-bridge/firefox-accounts-bridge/src | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
6 files changed, 1169 insertions, 0 deletions
diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/bridge.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/bridge.rs new file mode 100644 index 0000000000..64f0e222bc --- /dev/null +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/bridge.rs @@ -0,0 +1,358 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::punt::{ + error::{Error, Result}, + PuntTask, +}; +use fxa_client::FirefoxAccount; +use nserror::{nsresult, NS_OK}; +use nsstring::{nsACString, nsCString}; +use once_cell::unsync::OnceCell; +use std::{ + str, + sync::{Arc, Mutex}, +}; +use storage_variant::HashPropertyBag; +use thin_vec::ThinVec; +use xpcom::{ + interfaces::{mozIFirefoxAccountsBridgeCallback, nsIPropertyBag, nsISerialEventTarget}, + RefPtr, +}; + +/// This macro calls `PuntTask::for_<fn_name>(fxa, <..args>, callback)` +/// if `fxa` has been initialized. +macro_rules! punt { + ($fn_name:ident $(, $arg:ident : $ty:ty)*) => { + fn $fn_name(&self $(, $arg: $ty)*, callback: &mozIFirefoxAccountsBridgeCallback) -> Result<()> { + if let Some(fxa) = self.fxa.get() { + let task_fn = paste::expr! { PuntTask::[<for_ $fn_name>] }; + let task = task_fn(fxa $(, $arg)*, callback)?; + task.dispatch(&self.thread)?; + Ok(()) + } else { + Err(Error::Unavailable) + } + } + } +} + +/// An XPCOM binding for the Rust Firefox Accounts crate. +#[derive(xpcom)] +#[xpimplements(mozIFirefoxAccountsBridge)] +#[refcnt = "nonatomic"] // Non-atomic because we have a `RefCell`. +pub struct InitBridge { + // A background task queue used to run all our operations + // on a thread pool. + thread: RefPtr<nsISerialEventTarget>, + fxa: OnceCell<Arc<Mutex<FirefoxAccount>>>, +} + +impl Bridge { + pub fn new() -> Result<RefPtr<Bridge>> { + let thread = moz_task::create_background_task_queue(cstr!("FirefoxAccountsBridge"))?; + Ok(Bridge::allocate(InitBridge { + thread, + fxa: OnceCell::new(), + })) + } + + // This method (or InitFromJSON) must be called before any other one, because this + // is where we can finally give parameters for the Rust `FirefoxAccount` constructor + // and create an instance. + xpcom_method!( + init => Init( + options: *const nsIPropertyBag + ) + ); + fn init(&self, options: &nsIPropertyBag) -> Result<()> { + let options = HashPropertyBag::clone_from_bag(options)?; + let content_url: nsCString = options.get("content_url")?; + let content_url = str::from_utf8(&*content_url)?; + let client_id: nsCString = options.get("client_id")?; + let client_id = str::from_utf8(&*client_id)?; + let redirect_uri: nsCString = options.get("redirect_uri")?; + let redirect_uri = str::from_utf8(&*redirect_uri)?; + let token_server_url_override: nsCString = options.get("token_server_url_override")?; + let token_server_url_override = if token_server_url_override.is_empty() { + None + } else { + Some(str::from_utf8(&*token_server_url_override)?) + }; + self.fxa + .set(Arc::new(Mutex::new(FirefoxAccount::new( + &content_url, + &client_id, + &redirect_uri, + token_server_url_override, + )))) + .map_err(|_| Error::AlreadyInitialized)?; + Ok(()) + } + + xpcom_method!( + from_json => InitFromJSON( + json: *const nsACString + ) + ); + fn from_json(&self, json: &nsACString) -> Result<()> { + let json = str::from_utf8(&*json)?; + self.fxa + .set(Arc::new(Mutex::new(FirefoxAccount::from_json(json)?))) + .map_err(|_| Error::AlreadyInitialized)?; + Ok(()) + } + + xpcom_method!( + to_json => StateJSON( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(to_json); + + xpcom_method!( + begin_oauth_flow => BeginOAuthFlow( + scopes: *const ThinVec<nsCString>, + entry_point: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!( + begin_oauth_flow, + scopes: &ThinVec<nsCString>, + entry_point: &nsACString + ); + + xpcom_method!( + complete_oauth_flow => CompleteOAuthFlow( + code: *const nsACString, + state: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(complete_oauth_flow, code: &nsACString, state: &nsACString); + + xpcom_method!( + disconnect => Disconnect( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(disconnect); + + xpcom_method!( + get_access_token => GetAccessToken( + scope: *const nsACString, + ttl: u64, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(get_access_token, scope: &nsACString, ttl: u64); + + xpcom_method!( + get_session_token => GetSessionToken( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(get_session_token); + + xpcom_method!( + get_attached_clients => GetAttachedClients( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(get_attached_clients); + + xpcom_method!( + check_authorization_status => CheckAuthorizationStatus( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(check_authorization_status); + + xpcom_method!( + clear_access_token_cache => ClearAccessTokenCache( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(clear_access_token_cache); + + xpcom_method!( + handle_session_token_change => HandleSessionTokenChange( + session_token: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(handle_session_token_change, session_token: &nsACString); + + xpcom_method!( + migrate_from_session_token => MigrateFromSessionToken( + session_token: *const nsACString, + k_sync: *const nsACString, + k_xcs: *const nsACString, + copy_session_token: bool, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!( + migrate_from_session_token, + session_token: &nsACString, + k_sync: &nsACString, + k_xcs: &nsACString, + copy_session_token: bool + ); + + xpcom_method!( + retry_migrate_from_session_token => RetryMigrateFromSessionToken( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(retry_migrate_from_session_token); + + xpcom_method!( + is_in_migration_state => IsInMigrationState( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(is_in_migration_state); + + xpcom_method!( + get_profile => GetProfile( + ignore_cache: bool, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(get_profile, ignore_cache: bool); + + xpcom_method!( + get_token_server_endpoint_url => GetTokenServerEndpointURL( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(get_token_server_endpoint_url); + + xpcom_method!( + get_connection_success_url => GetConnectionSuccessURL( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(get_connection_success_url); + + xpcom_method!( + get_manage_account_url => GetManageAccountURL( + entrypoint: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(get_manage_account_url, entrypoint: &nsACString); + + xpcom_method!( + get_manage_devices_url => GetManageDevicesURL( + entrypoint: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(get_manage_devices_url, entrypoint: &nsACString); + + xpcom_method!( + fetch_devices => FetchDevices( + ignore_cache: bool, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(fetch_devices, ignore_cache: bool); + + xpcom_method!( + set_device_display_name => SetDeviceDisplayName( + name: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!(set_device_display_name, name: &nsACString); + + xpcom_method!( + handle_push_message => HandlePushMessage( + payload: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(handle_push_message, payload: &nsACString); + + xpcom_method!( + poll_device_commands => PollDeviceCommands( + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!(poll_device_commands); + + xpcom_method!( + send_single_tab => SendSingleTab( + target_id: *const nsACString, + title: *const nsACString, + url: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + punt!( + send_single_tab, + target_id: &nsACString, + title: &nsACString, + url: &nsACString + ); + + xpcom_method!( + set_device_push_subscription => SetDevicePushSubscription( + endpoint: *const nsACString, + public_key: *const nsACString, + auth_key: *const nsACString, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!( + set_device_push_subscription, + endpoint: &nsACString, + public_key: &nsACString, + auth_key: &nsACString + ); + + xpcom_method!( + initialize_device => InitializeDevice( + name: *const nsACString, + device_type: *const nsACString, + supported_capabilities: *const ThinVec<nsCString>, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!( + initialize_device, + name: &nsACString, + device_type: &nsACString, + supported_capabilities: &ThinVec<nsCString> + ); + + xpcom_method!( + ensure_capabilities => EnsureCapabilities( + supported_capabilities: *const ThinVec<nsCString>, + callback: *const mozIFirefoxAccountsBridgeCallback + ) + ); + + punt!( + ensure_capabilities, + supported_capabilities: &ThinVec<nsCString> + ); +} diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/lib.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/lib.rs new file mode 100644 index 0000000000..9d85ecfe22 --- /dev/null +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/lib.rs @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This crate, as its name suggests, serves as a bridge +//! between the Javascript world (RustFxAccount.js) +//! and the Rust world (fxa-client crate). +//! +//! The `bridge` module implements the `mozIFirefoxAccountsBridge` +//! interface, which is callable from JS. +//! +//! The `punt` module helps running the synchronous Rust operations +//! on a background thread pool managed by Gecko. + +#[macro_use] +extern crate cstr; +#[macro_use] +extern crate xpcom; + +mod bridge; +mod punt; + +use crate::bridge::Bridge; +use nserror::{nsresult, NS_OK}; +use xpcom::{interfaces::mozIFirefoxAccountsBridge, RefPtr}; + +// The constructor for our fxa implementation, exposed to C++. See +// `FirefoxAccountsBridge.h` for the declaration and the wrapper we +// register with the component manager. +#[no_mangle] +pub unsafe extern "C" fn NS_NewFirefoxAccountsBridge( + result: *mut *const mozIFirefoxAccountsBridge, +) -> nsresult { + match Bridge::new() { + Ok(bridge) => { + RefPtr::new(bridge.coerce::<mozIFirefoxAccountsBridge>()).forget(&mut *result); + NS_OK + } + Err(err) => err.into(), + } +} diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/error.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/error.rs new file mode 100644 index 0000000000..0328e15aae --- /dev/null +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/error.rs @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::{error, fmt, result, str::Utf8Error, sync::PoisonError}; + +use nserror::{ + nsresult, NS_ERROR_ALREADY_INITIALIZED, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, + NS_ERROR_NOT_AVAILABLE, NS_ERROR_UNEXPECTED, +}; + +pub type Result<T> = result::Result<T, Error>; + +#[derive(Debug)] +pub enum Error { + /// A wrapped FxA error. + FxAError(fxa_client::Error), + + /// A wrapped XPCOM error. + Nsresult(nsresult), + + /// A punt already ran. + AlreadyRan(&'static str), + + /// A punt didn't run on the background task queue. + DidNotRun(&'static str), + + /// A punt was already torn down when used. + AlreadyTornDown, + + /// A Gecko string couldn't be converted to UTF-8. + MalformedString(Box<dyn error::Error + Send + Sync + 'static>), + + /// The FxA object has already been initialized. + AlreadyInitialized, + + /// The FxA object has not been initialized before use. + Unavailable, + + /// Failure acquiring a lock on the FxA object in the background thread. + PoisonError, +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::MalformedString(error) => Some(error.as_ref()), + _ => None, + } + } +} + +impl From<Error> for nsresult { + fn from(error: Error) -> nsresult { + match error { + Error::FxAError(_) => NS_ERROR_FAILURE, + Error::AlreadyRan(_) + | Error::DidNotRun(_) + | Error::AlreadyTornDown + | Error::PoisonError => NS_ERROR_UNEXPECTED, + Error::Nsresult(result) => result, + Error::MalformedString(_) => NS_ERROR_INVALID_ARG, + Error::AlreadyInitialized => NS_ERROR_ALREADY_INITIALIZED, + Error::Unavailable => NS_ERROR_NOT_AVAILABLE, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::FxAError(error) => error.fmt(f), + Error::Nsresult(result) => write!(f, "Operation failed with {}", result.error_name()), + Error::AlreadyRan(what) => write!(f, "`{}` already ran on the background thread", what), + Error::DidNotRun(what) => write!(f, "Failed to run `{}` on background thread", what), + Error::AlreadyTornDown => { + write!(f, "Can't use a storage area that's already torn down") + } + Error::MalformedString(error) => error.fmt(f), + Error::AlreadyInitialized => f.write_str("The resource has already been initialized"), + Error::Unavailable => f.write_str("A resource for this operation is unavailable"), + Error::PoisonError => f.write_str("Error getting read/write lock"), + } + } +} + +impl From<nsresult> for Error { + fn from(result: nsresult) -> Error { + Error::Nsresult(result) + } +} + +impl From<Utf8Error> for Error { + fn from(error: Utf8Error) -> Error { + Error::MalformedString(error.into()) + } +} + +impl<T> From<PoisonError<T>> for Error { + fn from(_error: PoisonError<T>) -> Error { + Error::PoisonError + } +} + +impl From<fxa_client::error::Error> for Error { + fn from(result: fxa_client::error::Error) -> Error { + Error::FxAError(result) + } +} diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/mod.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/mod.rs new file mode 100644 index 0000000000..3a6cd63cbe --- /dev/null +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/mod.rs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Punt is pretty much a copy-paste of the Golden Gate crate in services/sync/golden_gate +//! but specialized to work on the fxa-client crate. +//! In short it helps run Rust code in a background thread, handle errors and get the results back. +//! There's an effort to factorize these helpers in https://bugzilla.mozilla.org/show_bug.cgi?id=1626703. + +pub mod error; +mod punt; +mod task; + +pub use task::PuntTask; diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/punt.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/punt.rs new file mode 100644 index 0000000000..79c16ee49f --- /dev/null +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/punt.rs @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use fxa_client::device::{ + Capability as FxaDeviceCapability, PushSubscription as FxAPushSubscription, + Type as FxaDeviceType, +}; +use nsstring::nsCString; +use storage_variant::VariantType; +use xpcom::{interfaces::nsIVariant, RefPtr}; + +/// An operation that runs on the background thread, and optionally passes a +/// result to its callback. +pub enum Punt { + ToJson, + BeginOAuthFlow(Vec<String>, String), + CompleteOAuthFlow(String, String), + Disconnect, + GetAccessToken(String, Option<u64>), + GetSessionToken, + GetAttachedClients, + CheckAuthorizationStatus, + ClearAccessTokenCache, + HandleSessionTokenChange(String), + MigrateFromSessionToken(String, String, String, bool), + RetryMigrateFromSessionToken, + IsInMigrationState, + GetProfile(bool), + GetTokenServerEndpointUrl, + GetConnectionSuccessUrl, + GetManageAccountUrl(String), + GetManageDevicesUrl(String), + FetchDevices(bool), + SetDeviceDisplayName(String), + HandlePushMessage(String), + PollDeviceCommands, + SendSingleTab(String, String, String), + SetDevicePushSubscription(FxAPushSubscription), + InitializeDevice(String, FxaDeviceType, Vec<FxaDeviceCapability>), + EnsureCapabilities(Vec<FxaDeviceCapability>), +} + +impl Punt { + /// Returns the operation name for debugging and labeling the task + /// runnable. + pub fn name(&self) -> &'static str { + match self { + Punt::ToJson => concat!(module_path!(), "toJson"), + Punt::BeginOAuthFlow { .. } => concat!(module_path!(), "beginOAuthFlow"), + Punt::CompleteOAuthFlow { .. } => concat!(module_path!(), "completeOAuthFlow"), + Punt::Disconnect => concat!(module_path!(), "disconnect"), + Punt::GetAccessToken { .. } => concat!(module_path!(), "getAccessToken"), + Punt::GetSessionToken => concat!(module_path!(), "getSessionToken"), + Punt::GetAttachedClients => concat!(module_path!(), "getAttachedClients"), + Punt::CheckAuthorizationStatus => concat!(module_path!(), "checkAuthorizationStatus"), + Punt::ClearAccessTokenCache => concat!(module_path!(), "clearAccessTokenCache"), + Punt::HandleSessionTokenChange { .. } => { + concat!(module_path!(), "handleSessionTokenChange") + } + Punt::MigrateFromSessionToken { .. } => { + concat!(module_path!(), "migrateFromSessionToken") + } + Punt::RetryMigrateFromSessionToken => { + concat!(module_path!(), "retryMigrateFromSessionToken") + } + Punt::IsInMigrationState => concat!(module_path!(), "isInMigrationState"), + Punt::GetProfile { .. } => concat!(module_path!(), "getProfile"), + Punt::GetTokenServerEndpointUrl => concat!(module_path!(), "getTokenServerEndpointUrl"), + Punt::GetConnectionSuccessUrl => concat!(module_path!(), "getConnectionSuccessUrl"), + Punt::GetManageAccountUrl { .. } => concat!(module_path!(), "getManageAccountUrl"), + Punt::GetManageDevicesUrl { .. } => concat!(module_path!(), "getManageDevicesUrl"), + Punt::FetchDevices { .. } => concat!(module_path!(), "fetchDevices"), + Punt::SetDeviceDisplayName { .. } => concat!(module_path!(), "setDeviceDisplayName"), + Punt::HandlePushMessage { .. } => concat!(module_path!(), "handlePushMessage"), + Punt::PollDeviceCommands => concat!(module_path!(), "pollDeviceCommands"), + Punt::SendSingleTab { .. } => concat!(module_path!(), "sendSingleTab"), + Punt::SetDevicePushSubscription { .. } => { + concat!(module_path!(), "setDevicePushSubscription") + } + Punt::InitializeDevice { .. } => concat!(module_path!(), "initializeDevice"), + Punt::EnsureCapabilities { .. } => concat!(module_path!(), "ensureCapabilities"), + } + } +} + +/// The result of a punt task, sent from the background thread back to the +/// main thread. Results are converted to variants, and passed as arguments to +/// the callbacks. +pub enum PuntResult { + String(String), + Boolean(bool), + Null, +} + +impl PuntResult { + pub fn url_spec(url: url::Url) -> Self { + PuntResult::String(url.into_string()) + } + pub fn json_stringify<T>(value: T) -> Self + where + T: serde::Serialize + std::marker::Sized, + { + PuntResult::String(serde_json::to_string(&value).unwrap()) + } +} + +impl From<()> for PuntResult { + fn from(_: ()) -> PuntResult { + PuntResult::Null + } +} + +impl PuntResult { + /// Converts the result to an `nsIVariant` that can be passed as an + /// argument to `callback.handleResult()`. + pub fn into_variant(self) -> RefPtr<nsIVariant> { + match self { + PuntResult::String(v) => nsCString::from(v).into_variant(), + PuntResult::Boolean(b) => b.into_variant(), + PuntResult::Null => ().into_variant(), + } + } +} diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs new file mode 100644 index 0000000000..2e2c8ad68e --- /dev/null +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs @@ -0,0 +1,523 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::punt::{ + error::{self, Error}, + punt::{Punt, PuntResult}, +}; +use atomic_refcell::AtomicRefCell; +use fxa_client::{ + device::{ + Capability as FxaDeviceCapability, CommandFetchReason, + PushSubscription as FxaPushSubscription, Type as FxaDeviceType, + }, + FirefoxAccount, +}; +use moz_task::{DispatchOptions, Task, TaskRunnable, ThreadPtrHandle, ThreadPtrHolder}; +use nserror::nsresult; +use nsstring::{nsACString, nsCString}; +use std::{ + fmt::Write, + mem, str, + sync::{Arc, Mutex, Weak}, +}; +use xpcom::{ + interfaces::{mozIFirefoxAccountsBridgeCallback, nsIEventTarget}, + RefPtr, +}; + +/// A punt task sends an operation to an fxa instance on a +/// background thread or task queue, and ferries back an optional result to +/// a callback. +pub struct PuntTask { + name: &'static str, + fxa: Weak<Mutex<FirefoxAccount>>, + punt: AtomicRefCell<Option<Punt>>, + callback: ThreadPtrHandle<mozIFirefoxAccountsBridgeCallback>, + result: AtomicRefCell<Result<PuntResult, Error>>, +} + +impl PuntTask { + /// Creates a task for a punt. The `callback` is bound to the current + /// thread, and will be called once, after the punt returns from the + /// background thread. + fn new( + fxa: &Arc<Mutex<FirefoxAccount>>, + punt: Punt, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let name = punt.name(); + Ok(Self { + name: punt.name(), + fxa: Arc::downgrade(fxa), + punt: AtomicRefCell::new(Some(punt)), + callback: ThreadPtrHolder::new( + cstr!("mozIFirefoxAccountsBridgeCallback"), + RefPtr::new(callback), + )?, + result: AtomicRefCell::new(Err(Error::DidNotRun(name).into())), + }) + } + + /// Creates a task that calls begin_oauth_flow. + #[inline] + pub fn for_begin_oauth_flow( + fxa: &Arc<Mutex<FirefoxAccount>>, + scopes: &[nsCString], + entry_point: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let scopes = scopes.iter().try_fold( + Vec::with_capacity(scopes.len()), + |mut acc, scope| -> error::Result<_> { + acc.push(std::str::from_utf8(&*scope)?.into()); + Ok(acc) + }, + )?; + let entry_point = str::from_utf8(&*entry_point)?.into(); + Self::new(fxa, Punt::BeginOAuthFlow(scopes, entry_point), callback) + } + + /// Creates a task that calls complete_oauth_flow. + #[inline] + pub fn for_complete_oauth_flow( + fxa: &Arc<Mutex<FirefoxAccount>>, + code: &nsACString, + state: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let code = str::from_utf8(&*code)?.into(); + let state = str::from_utf8(&*state)?.into(); + Self::new(fxa, Punt::CompleteOAuthFlow(code, state), callback) + } + + /// Creates a task that calls disconnect. + #[inline] + pub fn for_disconnect( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::Disconnect, callback) + } + + /// Creates a task that calls to_json. + #[inline] + pub fn for_to_json( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::ToJson, callback) + } + + /// Creates a task that calls get_access_token. + #[inline] + pub fn for_get_access_token( + fxa: &Arc<Mutex<FirefoxAccount>>, + scope: &nsACString, + ttl: u64, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let scope = str::from_utf8(&*scope)?.into(); + let ttl = if ttl > 0 { Some(ttl) } else { None }; + Self::new(fxa, Punt::GetAccessToken(scope, ttl), callback) + } + + /// Creates a task that calls get_session_token. + #[inline] + pub fn for_get_session_token( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::GetSessionToken, callback) + } + + /// Creates a task that calls get_attached_clients. + #[inline] + pub fn for_get_attached_clients( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::GetAttachedClients, callback) + } + + /// Creates a task that calls check_authorization_status. + #[inline] + pub fn for_check_authorization_status( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::CheckAuthorizationStatus, callback) + } + + /// Creates a task that calls clear_access_token_cache. + #[inline] + pub fn for_clear_access_token_cache( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::ClearAccessTokenCache, callback) + } + + /// Creates a task that calls handle_session_token_change. + #[inline] + pub fn for_handle_session_token_change( + fxa: &Arc<Mutex<FirefoxAccount>>, + session_token: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let session_token = str::from_utf8(&*session_token)?.into(); + Self::new(fxa, Punt::HandleSessionTokenChange(session_token), callback) + } + + /// Creates a task that calls migrate_from_session_token. + #[inline] + pub fn for_migrate_from_session_token( + fxa: &Arc<Mutex<FirefoxAccount>>, + session_token: &nsACString, + k_sync: &nsACString, + k_xcs: &nsACString, + copy_session_token: bool, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let session_token = str::from_utf8(&*session_token)?.into(); + let k_sync = str::from_utf8(&*k_sync)?.into(); + let k_xcs = str::from_utf8(&*k_xcs)?.into(); + Self::new( + fxa, + Punt::MigrateFromSessionToken(session_token, k_sync, k_xcs, copy_session_token), + callback, + ) + } + + /// Creates a task that calls retry_migrate_from_session_token. + #[inline] + pub fn for_retry_migrate_from_session_token( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::RetryMigrateFromSessionToken, callback) + } + + /// Creates a task that calls is_in_migration_state. + #[inline] + pub fn for_is_in_migration_state( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::IsInMigrationState, callback) + } + + /// Creates a task that calls get_profile. + #[inline] + pub fn for_get_profile( + fxa: &Arc<Mutex<FirefoxAccount>>, + ignore_cache: bool, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::GetProfile(ignore_cache), callback) + } + + /// Creates a task that calls get_token_server_endpoint_url. + #[inline] + pub fn for_get_token_server_endpoint_url( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::GetTokenServerEndpointUrl, callback) + } + + /// Creates a task that calls get_connection_success_url. + #[inline] + pub fn for_get_connection_success_url( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::GetConnectionSuccessUrl, callback) + } + + /// Creates a task that calls get_manage_account_url. + #[inline] + pub fn for_get_manage_account_url( + fxa: &Arc<Mutex<FirefoxAccount>>, + entrypoint: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let entrypoint = str::from_utf8(&*entrypoint)?.into(); + Self::new(fxa, Punt::GetManageAccountUrl(entrypoint), callback) + } + + /// Creates a task that calls get_manage_devices_url. + #[inline] + pub fn for_get_manage_devices_url( + fxa: &Arc<Mutex<FirefoxAccount>>, + entrypoint: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let entrypoint = str::from_utf8(&*entrypoint)?.into(); + Self::new(fxa, Punt::GetManageDevicesUrl(entrypoint), callback) + } + + /// Creates a task that calls fetch_devices. + #[inline] + pub fn for_fetch_devices( + fxa: &Arc<Mutex<FirefoxAccount>>, + ignore_cache: bool, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::FetchDevices(ignore_cache), callback) + } + + /// Creates a task that calls set_device_display_name. + #[inline] + pub fn for_set_device_display_name( + fxa: &Arc<Mutex<FirefoxAccount>>, + name: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let name = str::from_utf8(&*name)?.into(); + Self::new(fxa, Punt::SetDeviceDisplayName(name), callback) + } + + /// Creates a task that calls handle_push_message. + #[inline] + pub fn for_handle_push_message( + fxa: &Arc<Mutex<FirefoxAccount>>, + payload: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let payload = str::from_utf8(&*payload)?.into(); + Self::new(fxa, Punt::HandlePushMessage(payload), callback) + } + + /// Creates a task that calls poll_device_commands. + #[inline] + pub fn for_poll_device_commands( + fxa: &Arc<Mutex<FirefoxAccount>>, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + Self::new(fxa, Punt::PollDeviceCommands, callback) + } + + /// Creates a task that calls send_single_tab. + #[inline] + pub fn for_send_single_tab( + fxa: &Arc<Mutex<FirefoxAccount>>, + target_id: &nsACString, + title: &nsACString, + url: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let target_id = str::from_utf8(&*target_id)?.into(); + let title = str::from_utf8(&*title)?.into(); + let url = str::from_utf8(&*url)?.into(); + Self::new(fxa, Punt::SendSingleTab(target_id, title, url), callback) + } + + /// Creates a task that calls set_device_push_subscription. + #[inline] + pub fn for_set_device_push_subscription( + fxa: &Arc<Mutex<FirefoxAccount>>, + endpoint: &nsACString, + public_key: &nsACString, + auth_key: &nsACString, + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let push_subscription = FxaPushSubscription { + endpoint: str::from_utf8(&*endpoint)?.into(), + public_key: str::from_utf8(&*public_key)?.into(), + auth_key: str::from_utf8(&*auth_key)?.into(), + }; + Self::new( + fxa, + Punt::SetDevicePushSubscription(push_subscription), + callback, + ) + } + + /// Creates a task that calls initialize_device. + #[inline] + pub fn for_initialize_device( + fxa: &Arc<Mutex<FirefoxAccount>>, + name: &nsACString, + device_type: &nsACString, + supported_capabilities: &[nsCString], + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let name = str::from_utf8(&*name)?.into(); + let device_type = to_device_type(device_type)?; + let supported_capabilities = to_capabilities(supported_capabilities)?; + Self::new( + fxa, + Punt::InitializeDevice(name, device_type, supported_capabilities), + callback, + ) + } + + /// Creates a task that calls ensure_capabilities. + #[inline] + pub fn for_ensure_capabilities( + fxa: &Arc<Mutex<FirefoxAccount>>, + supported_capabilities: &[nsCString], + callback: &mozIFirefoxAccountsBridgeCallback, + ) -> error::Result<PuntTask> { + let supported_capabilities = to_capabilities(supported_capabilities)?; + Self::new( + fxa, + Punt::EnsureCapabilities(supported_capabilities), + callback, + ) + } + + /// Dispatches the task to the given thread `target`. + pub fn dispatch(self, target: &nsIEventTarget) -> Result<(), Error> { + let runnable = TaskRunnable::new(self.name, Box::new(self))?; + // `may_block` schedules the task on the I/O thread pool, since we + // expect most operations to wait on I/O. + TaskRunnable::dispatch_with_options( + runnable, + target, + DispatchOptions::default().may_block(true), + )?; + Ok(()) + } + + fn run_with_punt(&self, punt: Punt) -> Result<PuntResult, Error> { + let fxa = self.fxa.upgrade().ok_or_else(|| Error::AlreadyTornDown)?; + let mut fxa = fxa.lock()?; + Ok(match punt { + Punt::ToJson => fxa.to_json().map(PuntResult::String), + Punt::BeginOAuthFlow(scopes, entry_point) => { + let scopes: Vec<&str> = scopes.iter().map(AsRef::as_ref).collect(); + fxa.begin_oauth_flow(&scopes, &entry_point, None) + .map(PuntResult::String) + } + Punt::CompleteOAuthFlow(code, state) => fxa + .complete_oauth_flow(&code, &state) + .map(|_| PuntResult::Null), + Punt::Disconnect => { + fxa.disconnect(); + Ok(PuntResult::Null) + } + Punt::GetAccessToken(scope, ttl) => fxa + .get_access_token(&scope, ttl) + .map(PuntResult::json_stringify), + Punt::GetSessionToken => fxa.get_session_token().map(PuntResult::String), + Punt::GetAttachedClients => fxa.get_attached_clients().map(PuntResult::json_stringify), + Punt::CheckAuthorizationStatus => fxa + .check_authorization_status() + .map(PuntResult::json_stringify), + Punt::ClearAccessTokenCache => { + fxa.clear_access_token_cache(); + Ok(PuntResult::Null) + } + Punt::HandleSessionTokenChange(session_token) => fxa + .handle_session_token_change(&session_token) + .map(|_| PuntResult::Null), + Punt::MigrateFromSessionToken(session_token, k_sync, k_xcs, copy_session_token) => fxa + .migrate_from_session_token(&session_token, &k_sync, &k_xcs, copy_session_token) + .map(PuntResult::json_stringify), + Punt::RetryMigrateFromSessionToken => { + fxa.try_migration().map(PuntResult::json_stringify) + } + Punt::IsInMigrationState => { + Ok(PuntResult::Boolean(match fxa.is_in_migration_state() { + fxa_client::migrator::MigrationState::None => false, + _ => true, + })) + } + Punt::GetProfile(ignore_cache) => fxa + .get_profile(ignore_cache) + .map(PuntResult::json_stringify), + Punt::GetTokenServerEndpointUrl => fxa + .get_token_server_endpoint_url() + .map(PuntResult::url_spec), + Punt::GetConnectionSuccessUrl => { + fxa.get_connection_success_url().map(PuntResult::url_spec) + } + Punt::GetManageAccountUrl(entrypoint) => fxa + .get_manage_account_url(&entrypoint) + .map(PuntResult::url_spec), + Punt::GetManageDevicesUrl(entrypoint) => fxa + .get_manage_devices_url(&entrypoint) + .map(PuntResult::url_spec), + Punt::FetchDevices(ignore_cache) => fxa + .get_devices(ignore_cache) + .map(PuntResult::json_stringify), + Punt::SetDeviceDisplayName(name) => { + fxa.set_device_name(&name).map(PuntResult::json_stringify) + } + Punt::HandlePushMessage(payload) => fxa + .handle_push_message(&payload) + .map(PuntResult::json_stringify), + Punt::PollDeviceCommands => fxa + .poll_device_commands(CommandFetchReason::Poll) + .map(PuntResult::json_stringify), + Punt::SendSingleTab(target_id, title, url) => fxa + .send_tab(&target_id, &title, &url) + .map(|_| PuntResult::Null), + Punt::SetDevicePushSubscription(push_subscription) => fxa + .set_push_subscription(&push_subscription) + .map(PuntResult::json_stringify), + Punt::InitializeDevice(name, device_type, capabilities) => fxa + .initialize_device(&name, device_type.clone(), &capabilities) + .map(|_| PuntResult::Null), + Punt::EnsureCapabilities(capabilities) => fxa + .ensure_capabilities(&capabilities) + .map(|_| PuntResult::Null), + }?) + } +} + +fn to_capabilities(capabilities: &[nsCString]) -> error::Result<Vec<FxaDeviceCapability>> { + Ok(capabilities.iter().try_fold( + Vec::with_capacity(capabilities.len()), + |mut acc, capability| -> error::Result<_> { + let capability = str::from_utf8(&*capability)?; + acc.push(match capability { + "sendTab" => FxaDeviceCapability::SendTab, + _ => return Err(Error::Nsresult(nserror::NS_ERROR_INVALID_ARG)), + }); + Ok(acc) + }, + )?) +} + +fn to_device_type(device_type: &nsACString) -> error::Result<FxaDeviceType> { + let device_type = str::from_utf8(&*device_type)?; + Ok(match device_type { + "desktop" => FxaDeviceType::Desktop, + "mobile" => FxaDeviceType::Mobile, + "tablet" => FxaDeviceType::Tablet, + "tv" => FxaDeviceType::TV, + "vr" => FxaDeviceType::VR, + _ => return Err(Error::Nsresult(nserror::NS_ERROR_INVALID_ARG)), + }) +} + +impl Task for PuntTask { + fn run(&self) { + *self.result.borrow_mut() = match mem::take(&mut *self.punt.borrow_mut()) { + Some(punt) => self.run_with_punt(punt), + // A task should never run on the background queue twice, but we + // return an error just in case. + None => Err(Error::AlreadyRan(self.name)), + }; + } + + fn done(&self) -> Result<(), nsresult> { + let callback = self.callback.get().unwrap(); + match mem::replace( + &mut *self.result.borrow_mut(), + Err(Error::AlreadyRan(self.name)), + ) { + Ok(result) => unsafe { callback.HandleSuccess(result.into_variant().coerce()) }, + Err(err) => { + let mut message = nsCString::new(); + write!(message, "{}", err).unwrap(); + unsafe { callback.HandleError(err.into(), &*message) } + } + } + .to_result() + } +} |