diff --git a/mobile/android/base/AccountsHelper.java b/mobile/android/base/AccountsHelper.java index 5bb9c4e2133..535e48d4846 100644 --- a/mobile/android/base/AccountsHelper.java +++ b/mobile/android/base/AccountsHelper.java @@ -22,7 +22,6 @@ import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.NativeEventListener; import org.mozilla.gecko.util.NativeJSObject; -import java.io.IOError; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.security.GeneralSecurityException; @@ -109,7 +108,7 @@ public class AccountsHelper implements NativeEventListener { final Account account = FirefoxAccounts.getFirefoxAccount(mContext); if (account == null) { if (callback != null) { - callback.sendError("Could not update Firefox Account since non exists"); + callback.sendError("Could not update Firefox Account since none exists"); } return; } @@ -117,6 +116,17 @@ public class AccountsHelper implements NativeEventListener { final NativeJSObject json = message.getObject("json"); final String email = json.getString("email"); final String uid = json.getString("uid"); + + // Protect against cross-connecting accounts. + if (account.name == null || !account.name.equals(email)) { + final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!"; + Log.e(LOGTAG, errorMessage); + if (callback != null) { + callback.sendError(errorMessage); + } + return; + } + final boolean verified = json.optBoolean("verified", false); final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey")); final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken")); @@ -129,7 +139,7 @@ public class AccountsHelper implements NativeEventListener { if (callback != null) { callback.sendSuccess(true); } - } catch (Exception e) { + } catch (NativeJSObject.InvalidPropertyException e) { Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e); if (callback != null) { callback.sendError("Could not update Firefox Account from JSON: " + e.toString()); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index c924ec5fdcb..6194de0b666 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -546,6 +546,16 @@ var BrowserApp = { InitLater(() => AccessFu.attach(window), window, "AccessFu"); } + if (!AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) { + // We can't delay registering WebChannel listeners: if the first page is + // about:accounts, which can happen when starting the Firefox Account flow + // from the first run experience, or via the Firefox Account Status + // Activity, we can and do miss messages from the fxa-content-server. + console.log("browser.js: loading Firefox Accounts WebChannel"); + Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm"); + EnsureFxAccountsWebChannel(); + } + // Notify Java that Gecko has loaded. Messaging.sendRequest({ type: "Gecko:Ready" }); diff --git a/mobile/android/modules/FxAccountsWebChannel.jsm b/mobile/android/modules/FxAccountsWebChannel.jsm new file mode 100644 index 00000000000..985da1523cb --- /dev/null +++ b/mobile/android/modules/FxAccountsWebChannel.jsm @@ -0,0 +1,235 @@ +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* 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/. */ + +/** + * Firefox Accounts Web Channel. + * + * Use the WebChannel component to receive messages about account + * state changes. + */ +this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */ + +Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */ +Cu.import("resource://gre/modules/Notifications.jsm"); /*global Notifications */ +Cu.import("resource://gre/modules/Prompt.jsm"); /*global Prompt */ +Cu.import("resource://gre/modules/Services.jsm"); /*global Services */ +Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */ + +const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts"); + +const WEBCHANNEL_ID = "account_updates"; + +const COMMAND_LOADED = "fxaccounts:loaded"; +const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account"; +const COMMAND_LOGIN = "fxaccounts:login"; +const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password"; + +/** + * Create a new FxAccountsWebChannel to listen for account updates. + * + * @param {Object} options Options + * @param {Object} options + * @param {String} options.content_uri + * The FxA Content server uri + * @param {String} options.channel_id + * The ID of the WebChannel + * @param {String} options.helpers + * Helpers functions. Should only be passed in for testing. + * @constructor + */ +this.FxAccountsWebChannel = function(options) { + if (!options) { + throw new Error("Missing configuration options"); + } + if (!options["content_uri"]) { + throw new Error("Missing 'content_uri' option"); + } + this._contentUri = options.content_uri; + + if (!options["channel_id"]) { + throw new Error("Missing 'channel_id' option"); + } + this._webChannelId = options.channel_id; + + this._setupChannel(); +}; + +this.FxAccountsWebChannel.prototype = { + /** + * WebChannel that is used to communicate with content page + */ + _channel: null, + + /** + * WebChannel ID. + */ + _webChannelId: null, + /** + * WebChannel origin, used to validate origin of messages + */ + _webChannelOrigin: null, + + /** + * Release all resources that are in use. + */ + tearDown() { + this._channel.stopListening(); + this._channel = null; + this._channelCallback = null; + }, + + /** + * Configures and registers a new WebChannel + * + * @private + */ + _setupChannel() { + // if this.contentUri is present but not a valid URI, then this will throw an error. + try { + this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null); + this._registerChannel(); + } catch (e) { + log.e(e); + throw e; + } + }, + + /** + * Create a new channel with the WebChannelBroker, setup a callback listener + * @private + */ + _registerChannel() { + /** + * Processes messages that are called back from the FxAccountsChannel + * + * @param webChannelId {String} + * Command webChannelId + * @param message {Object} + * Command message + * @param sendingContext {Object} + * Message sending context. + * @param sendingContext.browser {browser} + * The object that captured the + * WebChannelMessageToChrome. + * @param sendingContext.eventTarget {EventTarget} + * The where the message was sent. + * @param sendingContext.principal {Principal} + * The of the EventTarget where the message was sent. + * @private + * + */ + let listener = (webChannelId, message, sendingContext) => { + if (message) { + let command = message.command; + let data = message.data; + log.d("FxAccountsWebChannel message received, command: " + command); + + // Respond to the message with true or false. + let respond = (data) => { + let response = { + command: command, + messageId: message.messageId, + data: data + }; + log.d("Sending response to command: " + command); + this._channel.send(response, sendingContext); + }; + + switch (command) { + case COMMAND_LOADED: + let mm = sendingContext.browser.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + mm.sendAsyncMessage(COMMAND_LOADED); + break; + + case COMMAND_CAN_LINK_ACCOUNT: + // Temporarily accept any login. + respond({ ok: true }); + break; + + case COMMAND_LOGIN: + // Either create a new Android Account or re-connect an existing + // Android Account here. There's not much to be done if we don't + // succeed or get an error. + Accounts.getFirefoxAccount().then(account => { + if (!account) { + return Accounts.createFirefoxAccountFromJSON(data).then(success => { + if (!success) { + throw new Error("Could not create Firefox Account!"); + } + return success; + }); + } else { + return Accounts.updateFirefoxAccountFromJSON(data).then(success => { + if (!success) { + throw new Error("Could not update Firefox Account!"); + } + return success; + }); + } + }) + .then(success => { + if (!success) { + throw new Error("Could not create or update Firefox Account!"); + } + log.i("Created or updated Firefox Account."); + }) + .catch(e => { + log.e(e.toString()); + }); + break; + + case COMMAND_CHANGE_PASSWORD: + // Only update an existing Android Account. + Accounts.getFirefoxAccount().then(account => { + if (!account) { + throw new Error("Can't change password of non-existent Firefox Account!"); + } + return Accounts.updateFirefoxAccountFromJSON(data); + }) + .then(success => { + if (!success) { + throw new Error("Could not change Firefox Account password!"); + } + log.i("Changed Firefox Account password."); + }) + .catch(e => { + log.e(e.toString()); + }); + break; + + default: + log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command)); + break; + } + } + }; + + this._channelCallback = listener; + this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin); + this._channel.listen(listener); + + log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath); + } +}; + +let singleton; +// The entry-point for this module, which ensures only one of our channels is +// ever created - we require this because the WebChannel is global in scope and +// allowing multiple channels would cause such notifications to be sent multiple +// times. +this.EnsureFxAccountsWebChannel = function() { + if (!singleton) { + let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri"); + // The FxAccountsWebChannel listens for events and updates the Java layer. + singleton = new this.FxAccountsWebChannel({ + content_uri: contentUri, + channel_id: WEBCHANNEL_ID, + }); + } +}; diff --git a/mobile/android/modules/moz.build b/mobile/android/modules/moz.build index ed9ce49e93c..5aa70a00dc4 100644 --- a/mobile/android/modules/moz.build +++ b/mobile/android/modules/moz.build @@ -11,6 +11,7 @@ EXTRA_JS_MODULES += [ 'dbg-browser-actors.js', 'DelayedInit.jsm', 'DownloadNotifications.jsm', + 'FxAccountsWebChannel.jsm', 'HelperApps.jsm', 'Home.jsm', 'HomeProvider.jsm',