Bug 1442133 - FxA messages client implementation. r=markh,tcsc

MozReview-Commit-ID: EWYlZLdyUA0

--HG--
extra : rebase_source : ac540a5d1c26067c95314d07a32db8994f3dcee6
This commit is contained in:
Edouard Oger 2018-03-07 13:38:12 -05:00
parent be53524747
commit 783ac499bc
15 changed files with 658 additions and 18 deletions

View File

@ -1432,6 +1432,9 @@ pref("identity.fxaccounts.migrateToDevEdition", true);
pref("identity.fxaccounts.migrateToDevEdition", false);
#endif
// If activated, send tab will use the new FxA messages backend.
pref("identity.fxaccounts.messages.enabled", false);
// On GTK, we now default to showing the menubar only when alt is pressed:
#ifdef MOZ_WIDGET_GTK
pref("ui.key.menuAccessKeyFocuses", true);

View File

@ -319,10 +319,34 @@ var gSync = {
switchToTabHavingURI(url, true, { replaceQueryString: true });
},
sendTabToDevice(url, clientId, title) {
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title).catch(e => {
console.error("Could not send tab to device", e);
});
async sendTabToDevice(url, clients, title) {
let devices;
try {
devices = await fxAccounts.getDeviceList();
} catch (e) {
console.error("Could not get the FxA device list", e);
devices = []; // We can still run in degraded mode.
}
const toSendMessages = [];
for (const client of clients) {
const device = devices.find(d => d.id == client.fxaDeviceId);
if (device && fxAccounts.messages.canReceiveSendTabMessages(device)) {
toSendMessages.push(device);
} else {
try {
await Weave.Service.clientsEngine.sendURIToClientForDisplay(url, client.id, title);
} catch (e) {
console.error("Could not send tab to device", e);
}
}
}
if (toSendMessages.length) {
try {
await fxAccounts.messages.sendTab(toSendMessages, {url, title});
} catch (e) {
console.error("Could not send tab to device", e);
}
}
},
populateSendTabToDevicesMenu(devicesPopup, url, title, createDeviceNodeFn) {
@ -363,18 +387,23 @@ var gSync = {
devicesPopup.appendChild(fragment);
},
// TODO: once our transition from the old-send tab world is complete,
// this list should be built using the FxA device list instead of the client
// collection.
_appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title) {
const onSendAllCommand = (event) => {
this.sendTabToDevice(url, this.remoteClients, title);
};
const onTargetDeviceCommand = (event) => {
let clients = event.target.getAttribute("clientId") ?
[event.target.getAttribute("clientId")] :
this.remoteClients.map(client => client.id);
clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
const clientId = event.target.getAttribute("clientId");
const client = this.remoteClients.find(c => c.id == clientId);
this.sendTabToDevice(url, [client], title);
};
function addTargetDevice(clientId, name, clientType, lastModified) {
const targetDevice = createDeviceNodeFn(clientId, name, clientType, lastModified);
targetDevice.addEventListener("command", onTargetDeviceCommand, true);
targetDevice.addEventListener("command", clientId ? onTargetDeviceCommand :
onSendAllCommand, true);
targetDevice.classList.add("sync-menuitem", "sendtab-target");
targetDevice.setAttribute("clientId", clientId);
targetDevice.setAttribute("clientType", clientType);

View File

@ -444,6 +444,7 @@ BrowserGlue.prototype = {
this._onDeviceDisconnected();
}
break;
case "fxaccounts:messages:display-tabs":
case "weave:engine:clients:display-uris":
this._onDisplaySyncURIs(subject);
break;
@ -598,6 +599,7 @@ BrowserGlue.prototype = {
os.addObserver(this, "fxaccounts:device_connected");
os.addObserver(this, "fxaccounts:verify_login");
os.addObserver(this, "fxaccounts:device_disconnected");
os.addObserver(this, "fxaccounts:messages:display-tabs");
os.addObserver(this, "weave:engine:clients:display-uris");
os.addObserver(this, "session-save");
os.addObserver(this, "places-init-complete");
@ -640,6 +642,7 @@ BrowserGlue.prototype = {
os.removeObserver(this, "fxaccounts:device_connected");
os.removeObserver(this, "fxaccounts:verify_login");
os.removeObserver(this, "fxaccounts:device_disconnected");
os.removeObserver(this, "fxaccounts:messages:display-tabs");
os.removeObserver(this, "weave:engine:clients:display-uris");
os.removeObserver(this, "session-save");
if (this._bookmarksBackupIdleTime) {
@ -2543,7 +2546,7 @@ BrowserGlue.prototype = {
await Promise.all(URIs.slice(1).map(URI => openTab(URI)));
let title, body;
const deviceName = Weave.Service.clientsEngine.getClientName(URIs[0].clientId);
const deviceName = URIs[0].sender.name;
const bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
if (URIs.length == 1) {
// Due to bug 1305895, tabs from iOS may not have device information, so
@ -2567,7 +2570,7 @@ BrowserGlue.prototype = {
}
} else {
title = bundle.GetStringFromName("multipleTabsArrivingNotification.title");
const allSameDevice = URIs.every(URI => URI.clientId == URIs[0].clientId);
const allSameDevice = URIs.every(URI => URI.sender.id == URIs[0].sender.id);
const unknownDevice = allSameDevice && !deviceName;
let tabArrivingBody;
if (unknownDevice) {

View File

@ -30,6 +30,9 @@ ChromeUtils.defineModuleGetter(this, "jwcrypto",
ChromeUtils.defineModuleGetter(this, "FxAccountsOAuthGrantClient",
"resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
ChromeUtils.defineModuleGetter(this, "FxAccountsMessages",
"resource://gre/modules/FxAccountsMessages.js");
ChromeUtils.defineModuleGetter(this, "FxAccountsProfile",
"resource://gre/modules/FxAccountsProfile.jsm");
@ -41,6 +44,7 @@ XPCOMUtils.defineLazyPreferenceGetter(this, "FXA_ENABLED",
// All properties exposed by the public FxAccounts API.
var publicProperties = [
"_withCurrentAccountState", // fxaccounts package only!
"accountStatus",
"canGetKeys",
"checkVerificationStatus",
@ -51,6 +55,7 @@ var publicProperties = [
"getKeys",
"getOAuthToken",
"getProfileCache",
"getPushSubscription",
"getSignedInUser",
"getSignedInUserProfile",
"handleAccountDestroyed",
@ -60,6 +65,7 @@ var publicProperties = [
"invalidateCertificate",
"loadAndPoll",
"localtimeOffsetMsec",
"messages",
"notifyDevices",
"now",
"removeCachedOAuthToken",
@ -410,6 +416,14 @@ FxAccountsInternal.prototype = {
return this._profile;
},
_messages: null,
get messages() {
if (!this._messages) {
this._messages = new FxAccountsMessages(this);
}
return this._messages;
},
// A hook-point for tests who may want a mocked AccountState or mocked storage.
newAccountState(credentials) {
let storage = new FxAccountsStorageManager();
@ -417,6 +431,22 @@ FxAccountsInternal.prototype = {
return new AccountState(storage);
},
// "Friend" classes of FxAccounts (e.g. FxAccountsMessages) know about the
// "current account state" system. This method allows them to read and write
// safely in it.
// Example of usage:
// fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
// const userData = await getUserData(['device']);
// ...
// await updateUserData({device: null});
// });
_withCurrentAccountState(func) {
const state = this.currentAccountState;
const getUserData = (fields) => state.getUserAccountData(fields);
const updateUserData = (data) => state.updateUserAccountData(data);
return func(getUserData, updateUserData);
},
/**
* Send a message to a set of devices in the same account
*
@ -741,6 +771,9 @@ FxAccountsInternal.prototype = {
this._profile.tearDown();
this._profile = null;
}
if (this._messages) {
this._messages = null;
}
// We "abort" the accountState and assume our caller is about to throw it
// away and replace it with a new one.
return this.currentAccountState.abort();
@ -994,7 +1027,7 @@ FxAccountsInternal.prototype = {
throw new Error("Signed in user changed while fetching keys!");
}
// Next statements must be synchronous until we setUserAccountData
// Next statements must be synchronous until we updateUserAccountData
// so that we don't risk getting into a weird state.
let kBbytes = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
wrapKB);
@ -1649,6 +1682,20 @@ FxAccountsInternal.prototype = {
});
},
// @returns Promise<Subscription>.
getPushSubscription() {
return this.fxaPushService.getSubscription();
},
// Once FxA messages is stable, remove this, hardcode the capabilities,
// and reset the device registration version.
get deviceCapabilities() {
if (Services.prefs.getBoolPref("identity.fxaccounts.messages.enabled", true)) {
return [CAPABILITY_MESSAGES, CAPABILITY_MESSAGES_SENDTAB];
}
return [];
},
// If you change what we send to the FxA servers during device registration,
// you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older
// devices to re-register when Firefox updates
@ -1673,6 +1720,7 @@ FxAccountsInternal.prototype = {
deviceOptions.pushAuthKey = urlsafeBase64Encode(authKey);
}
}
deviceOptions.capabilities = this.deviceCapabilities;
let device;
if (currentDevice && currentDevice.id) {
@ -1687,6 +1735,7 @@ FxAccountsInternal.prototype = {
await this.currentAccountState.updateUserAccountData({
device: {
...currentDevice, // Copy the other properties (e.g. messagesIndex).
id: device.id,
registrationVersion: this.DEVICE_REGISTRATION_VERSION
}

View File

@ -409,6 +409,7 @@ this.FxAccountsClient.prototype = {
body.pushPublicKey = options.pushPublicKey;
body.pushAuthKey = options.pushAuthKey;
}
body.capabilities = options.capabilities;
return this._request(path, "POST", creds, body);
},
@ -446,6 +447,49 @@ this.FxAccountsClient.prototype = {
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
},
/**
* Retrieves messages from our device's message box.
*
* @method getMessages
* @param sessionTokenHex - Session token obtained from signIn
* @param [index] - If specified, only messages received after the one who
* had that index will be retrieved.
* @param [limit] - Maximum number of messages to retrieve.
*/
getMessages(sessionTokenHex, {index, limit}) {
const params = new URLSearchParams();
if (index != undefined) {
params.set("index", index);
}
if (limit != undefined) {
params.set("limit", limit);
}
const path = `/account/device/messages?${params.toString()}`;
return this._request(path, "GET",
deriveHawkCredentials(sessionTokenHex, "sessionToken"));
},
/**
* Stores a message in the recipient's message box.
*
* @method sendMessage
* @param sessionTokenHex - Session token obtained from signIn
* @param topic
* @param to - Recipient device ID.
* @param data
* @return Promise
* Resolves to the request's response, (which should be an empty object)
*/
sendMessage(sessionTokenHex, topic, to, data) {
const body = {
topic,
to,
data
};
return this._request("/account/devices/messages", "POST",
deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
},
/**
* Update the session or name for an existing device
*
@ -458,6 +502,8 @@ this.FxAccountsClient.prototype = {
* Device name
* @param [options]
* Extra device options
* @param options.capabilities
* Device capabilities
* @param [options.pushCallback]
* `pushCallback` push endpoint callback
* @param [options.pushPublicKey]
@ -483,6 +529,7 @@ this.FxAccountsClient.prototype = {
body.pushPublicKey = options.pushPublicKey;
body.pushAuthKey = options.pushAuthKey;
}
body.capabilities = options.capabilities;
return this._request(path, "POST", creds, body);
},

View File

@ -74,6 +74,9 @@ exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange"; // WebChannel
exports.ON_ACCOUNT_STATE_CHANGE_NOTIFICATION = "fxaccounts:statechange";
exports.CAPABILITY_MESSAGES = "messages";
exports.CAPABILITY_MESSAGES_SENDTAB = "messages.sendtab";
// UI Requests.
exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow";
exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";

View File

@ -0,0 +1,223 @@
/* 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/. */
const EXPORTED_SYMBOLS = ["FxAccountsMessages", /* the rest are for testing only */
"FxAccountsMessagesSender", "FxAccountsMessagesReceiver",
"FxAccountsMessagesHandler"];
ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
ChromeUtils.defineModuleGetter(this, "PushCrypto",
"resource://gre/modules/PushCrypto.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://services-common/observers.js");
const AES128GCM_ENCODING = "aes128gcm"; // The only Push encoding we support.
const TOPICS = {
SEND_TAB: "sendtab"
};
class FxAccountsMessages {
constructor(fxAccounts, options = {}) {
this.fxAccounts = fxAccounts;
this.sender = options.sender || new FxAccountsMessagesSender(fxAccounts);
this.receiver = options.receiver || new FxAccountsMessagesReceiver(fxAccounts);
}
_isDeviceMessagesAware(device) {
return device.capabilities && device.capabilities.includes(CAPABILITY_MESSAGES);
}
canReceiveSendTabMessages(device) {
return this._isDeviceMessagesAware(device) &&
device.capabilities.includes(CAPABILITY_MESSAGES_SENDTAB);
}
consumeRemoteMessages() {
if (!Services.prefs.getBoolPref("identity.fxaccounts.messages.enabled", true)) {
return Promise.resolve();
}
return this.receiver.consumeRemoteMessages();
}
/**
* @param {Device[]} to - Device objects (typically returned by fxAccounts.getDevicesList()).
* @param {Object} tab
* @param {string} tab.url
* @param {string} tab.title
*/
async sendTab(to, tab) {
log.info(`Sending a tab to ${to.length} devices.`);
const ourDeviceId = await this.fxAccounts.getDeviceId();
const payload = {
topic: TOPICS.SEND_TAB,
data: {
from: ourDeviceId,
entries: [{title: tab.title, url: tab.url}]
}
};
return this.sender.send(TOPICS.SEND_TAB, to, payload);
}
}
class FxAccountsMessagesSender {
constructor(fxAccounts) {
this.fxAccounts = fxAccounts;
}
async send(topic, to, data) {
const userData = await this.fxAccounts.getSignedInUser();
if (!userData) {
throw new Error("No user.");
}
const {sessionToken} = userData;
if (!sessionToken) {
throw new Error("_send called without a session token.");
}
const encoder = new TextEncoder("utf8");
const client = this.fxAccounts.getAccountsClient();
for (const device of to) {
try {
const bytes = encoder.encode(JSON.stringify(data));
const payload = await this._encrypt(bytes, device, encoder);
await client.sendMessage(sessionToken, topic, device.id, payload);
log.info(`Payload sent to device ${device.id}.`);
} catch (e) {
log.error(`Could not send data to device ${device.id}.`, e);
}
}
}
async _encrypt(bytes, device) {
let {pushPublicKey, pushAuthKey} = device;
if (!pushPublicKey || !pushAuthKey) {
throw new Error(`Device ${device.id} does not have push keys.`);
}
pushPublicKey = ChromeUtils.base64URLDecode(pushPublicKey, {padding: "ignore"});
pushAuthKey = ChromeUtils.base64URLDecode(pushAuthKey, {padding: "ignore"});
const {ciphertext} = await PushCrypto.encrypt(bytes, pushPublicKey, pushAuthKey);
return ChromeUtils.base64URLEncode(ciphertext, {pad: false});
}
}
class FxAccountsMessagesReceiver {
constructor(fxAccounts, options = {}) {
this.fxAccounts = fxAccounts;
this.handler = options.handler || new FxAccountsMessagesHandler(this.fxAccounts);
}
async consumeRemoteMessages() {
log.info(`Consuming unread messages.`);
const messages = await this._fetchMessages();
if (!messages || !messages.length) {
log.info(`No new messages.`);
return;
}
const decoder = new TextDecoder("utf8");
const keys = await this._getOwnKeys();
const payloads = [];
for (const {index, data} of messages) {
try {
const bytes = await this._decrypt(data, keys);
const payload = JSON.parse(decoder.decode(bytes));
payloads.push(payload);
} catch (e) {
log.error(`Could not unwrap message ${index}`, e);
}
}
if (payloads.length) {
await this.handler.handle(payloads);
}
}
async _fetchMessages() {
return this.fxAccounts._withCurrentAccountState(async (getUserData, updateUserData) => {
const userData = await getUserData(["sessionToken", "device"]);
if (!userData) {
throw new Error("No user.");
}
const {sessionToken, device} = userData;
if (!sessionToken) {
throw new Error("No session token.");
}
if (!device) {
throw new Error("No device registration.");
}
const opts = {};
if (device.messagesIndex) {
opts.index = device.messagesIndex;
}
const client = this.fxAccounts.getAccountsClient();
log.info(`Fetching unread messages with ${JSON.stringify(opts)}.`);
const {index: newIndex, messages} = await client.getMessages(sessionToken, opts);
await updateUserData({
device: {...device, messagesIndex: newIndex}
});
return messages;
});
}
async _getOwnKeys() {
const subscription = await this.fxAccounts.getPushSubscription();
return {
pushPrivateKey: subscription.p256dhPrivateKey,
pushPublicKey: new Uint8Array(subscription.getKey("p256dh")),
pushAuthKey: new Uint8Array(subscription.getKey("auth"))
};
}
async _decrypt(ciphertext, {pushPrivateKey, pushPublicKey, pushAuthKey}) {
ciphertext = ChromeUtils.base64URLDecode(ciphertext, {padding: "reject"});
return PushCrypto.decrypt(pushPrivateKey, pushPublicKey,
pushAuthKey,
{encoding: AES128GCM_ENCODING},
ciphertext);
}
}
class FxAccountsMessagesHandler {
constructor(fxAccounts) {
this.fxAccounts = fxAccounts;
}
async handle(payloads) {
const sendTabPayloads = [];
for (const payload of payloads) {
switch (payload.topic) {
case TOPICS.SEND_TAB:
sendTabPayloads.push(payload.data);
default:
log.info(`Unknown messages topic: ${payload.topic}.`);
}
}
// Only one type of payload so far!
if (sendTabPayloads.length) {
await this._handleSendTabPayloads(sendTabPayloads);
}
}
async _handleSendTabPayloads(payloads) {
const toDisplay = [];
const fxaDevices = await this.fxAccounts.getDeviceList();
for (const payload of payloads) {
const current = payload.hasOwnProperty("current") ? payload.current :
payload.entries.length - 1;
const device = fxaDevices.find(d => d.id == payload.from);
if (!device) {
log.warn("Incoming tab is from an unknown device (maybe disconnected?)");
}
const sender = {
id: device ? device.id : "",
name: device ? device.name : ""
};
const {title, url: uri} = payload.entries[current];
toDisplay.push({uri, title, sender});
}
Observers.notify("fxaccounts:messages:display-tabs", toDisplay);
}
}

View File

@ -158,6 +158,11 @@ FxAccountsPushService.prototype = {
return;
}
let payload = message.data.json();
if (payload.topic) {
this.log.debug(`received messages tickle with topic ${payload.topic}`);
this.fxAccounts.messages.consumeRemoteMessages();
return;
}
this.log.debug(`push command: ${payload.command}`);
switch (payload.command) {
case ON_DEVICE_CONNECTED_NOTIFICATION:
@ -241,6 +246,27 @@ FxAccountsPushService.prototype = {
});
});
},
/**
* Get our Push server subscription.
*
* Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription()
*
* @returns {Promise}
*/
getSubscription() {
return new Promise((resolve) => {
this.pushService.getSubscription(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
Services.scriptSecurityManager.getSystemPrincipal(),
(result, subscription) => {
if (!subscription) {
this.log.info("FxAccountsPushService no subscription found");
return resolve(null);
}
return resolve(subscription);
});
});
},
};
// Service registration below registers with FxAccountsComponents.manifest

View File

@ -26,6 +26,7 @@ EXTRA_JS_MODULES += [
'FxAccountsClient.jsm',
'FxAccountsCommon.js',
'FxAccountsConfig.jsm',
'FxAccountsMessages.js',
'FxAccountsOAuthGrantClient.jsm',
'FxAccountsProfile.jsm',
'FxAccountsProfileClient.jsm',

View File

@ -0,0 +1,210 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
ChromeUtils.import("resource://testing-common/Assert.jsm");
ChromeUtils.import("resource://gre/modules/FxAccountsMessages.js");
add_task(async function test_sendTab() {
const fxAccounts = {
async getDeviceId() {
return "my-device-id";
}
};
const sender = {
send: sinon.spy()
};
const fxAccountsMessages = new FxAccountsMessages(fxAccounts, {sender});
const to = [{
id: "deviceid-1",
pushPublicKey: "pubkey-1",
pushAuthKey: "authkey-1"
}];
const tab = {url: "https://foo.com", title: "Foo"};
await fxAccountsMessages.sendTab(to, tab);
Assert.ok(sender.send.calledOnce);
Assert.equal(sender.send.args[0][0], "sendtab");
Assert.deepEqual(sender.send.args[0][1], to);
Assert.deepEqual(sender.send.args[0][2], {
topic: "sendtab",
data: {
from: "my-device-id",
entries: [{title: "Foo", url: "https://foo.com"}]
}
});
});
add_task(async function test_consumeRemoteMessages() {
const fxAccounts = {};
const receiver = {
consumeRemoteMessages: sinon.spy()
};
const fxAccountsMessages = new FxAccountsMessages(fxAccounts, {receiver});
fxAccountsMessages.consumeRemoteMessages();
Assert.ok(receiver.consumeRemoteMessages.calledOnce);
});
add_task(async function test_canReceiveSendTabMessages() {
const fxAccounts = {};
const messages = new FxAccountsMessages(fxAccounts);
Assert.ok(!messages.canReceiveSendTabMessages({id: "device-id-1"}));
Assert.ok(!messages.canReceiveSendTabMessages({id: "device-id-1", capabilities: []}));
Assert.ok(!messages.canReceiveSendTabMessages({id: "device-id-1", capabilities: ["messages"]}));
Assert.ok(messages.canReceiveSendTabMessages({id: "device-id-1", capabilities: ["messages", "messages.sendtab"]}));
});
add_task(async function test_sender_send() {
const sandbox = sinon.sandbox.create();
const fxaClient = {
sendMessage: sinon.spy()
};
const sessionToken = "toktok";
const fxAccounts = {
async getSignedInUser() {
return {sessionToken};
},
getAccountsClient() {
return fxaClient;
}
};
const sender = new FxAccountsMessagesSender(fxAccounts);
sandbox.stub(sender, "_encrypt").callsFake((_, device) => {
if (device.pushPublicKey == "pubkey-1") {
return "encrypted-text-1";
}
return "encrypted-text-2";
});
const topic = "mytopic";
const to = [{
id: "deviceid-1",
pushPublicKey: "pubkey-1",
pushAuthKey: "authkey-1"
}, {
id: "deviceid-2",
pushPublicKey: "pubkey-2",
pushAuthKey: "authkey-2"
}];
const payload = {foo: "bar"};
await sender.send(topic, to, payload);
Assert.ok(fxaClient.sendMessage.calledTwice);
const checkCallArgs = (callNum, deviceId, encrypted) => {
Assert.equal(fxaClient.sendMessage.args[callNum][0], sessionToken);
Assert.equal(fxaClient.sendMessage.args[callNum][1], topic);
Assert.equal(fxaClient.sendMessage.args[callNum][2], deviceId);
Assert.equal(fxaClient.sendMessage.args[callNum][3], encrypted);
};
checkCallArgs(0, "deviceid-1", "encrypted-text-1");
checkCallArgs(1, "deviceid-2", "encrypted-text-2");
sandbox.restore();
});
add_task(async function test_receiver_consumeRemoteMessages() {
const fxaClient = {
getMessages: sinon.spy(async () => {
return {
index: "idx-2",
messages: [{
index: "idx-1",
data: "#giberish#"
}, {
index: "idx-2",
data: "#encrypted#"
}]
};
})
};
const fxAccounts = {
accountState: {sessionToken: "toktok", device: {}},
_withCurrentAccountState(fun) {
const get = () => this.accountState;
const update = (obj) => { this.accountState = {...this.accountState, ...obj}; };
return fun(get, update);
},
getAccountsClient() {
return fxaClient;
}
};
const sandbox = sinon.sandbox.create();
const messagesHandler = {
handle: sinon.spy()
};
const receiver = new FxAccountsMessagesReceiver(fxAccounts, {
handler: messagesHandler
});
sandbox.stub(receiver, "_getOwnKeys").callsFake(async () => {});
sandbox.stub(receiver, "_decrypt").callsFake((ciphertext) => {
if (ciphertext == "#encrypted#") {
return new TextEncoder("utf-8").encode(JSON.stringify({"foo": "bar"}));
}
throw new Error("Boom!");
});
await receiver.consumeRemoteMessages();
Assert.ok(fxaClient.getMessages.calledOnce);
Assert.equal(fxaClient.getMessages.args[0][0], "toktok");
Assert.deepEqual(fxaClient.getMessages.args[0][1], {});
Assert.ok(messagesHandler.handle.calledOnce);
Assert.deepEqual(messagesHandler.handle.args[0][0], [{"foo": "bar"}]);
fxaClient.getMessages.reset();
await receiver.consumeRemoteMessages();
Assert.ok(fxaClient.getMessages.calledOnce);
Assert.equal(fxaClient.getMessages.args[0][0], "toktok");
Assert.deepEqual(fxaClient.getMessages.args[0][1], {"index": "idx-2"});
sandbox.restore();
});
add_task(async function test_handler_handle_sendtab() {
const fxAccounts = {
async getDeviceList() {
return [{id: "1234a", name: "My Computer"}];
}
};
const handler = new FxAccountsMessagesHandler(fxAccounts);
const payloads = [{
topic: "sendtab",
data: {
from: "1234a",
current: 0,
entries: [{title: "Foo", url: "https://foo.com"},
{title: "Bar", url: "https://bar.com"}]
}
}, {
topic: "sendtab",
data: {
from: "unknown_device",
entries: [{title: "Foo2", url: "https://foo2.com"},
{title: "Bar2", url: "https://bar2.com"}]
}
}, {
topic: "unknowntopic",
data: {foo: "bar"}
}];
const notificationPromise = promiseObserver("fxaccounts:messages:display-tabs");
await handler.handle(payloads);
const {subject} = await notificationPromise;
const toDisplay = subject.wrappedJSObject.object;
const expected = [
{uri: "https://foo.com", title: "Foo", sender: {id: "1234a", name: "My Computer"}},
{uri: "https://bar2.com", title: "Bar2", sender: {id: "", name: ""}}
];
Assert.deepEqual(toDisplay, expected);
});
function promiseObserver(aTopic) {
return new Promise(resolve => {
Services.obs.addObserver(function onNotification(subject, topic, data) {
Services.obs.removeObserver(onNotification, topic);
resolve({subject, data});
}, aTopic);
});
}

View File

@ -406,6 +406,36 @@ add_test(function observePushTopicPasswordReset() {
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
add_task(async function messagesTickle() {
let msg = {
data: {
json: () => ({
topic: "sendtab"
})
},
QueryInterface() {
return this;
}
};
let fxAccountsMock = {};
const promiseConsumeRemoteMessagesCalled = new Promise(res => {
fxAccountsMock.messages = {
consumeRemoteMessages() {
res();
}
};
});
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: fxAccountsMock,
});
pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
await promiseConsumeRemoteMessagesCalled;
});
add_test(function observeSubscriptionChangeTopic() {
let customAccounts = Object.assign(mockFxAccounts, {
updateDeviceRegistration() {

View File

@ -11,6 +11,7 @@ support-files =
[test_client.js]
[test_credentials.js]
[test_loginmgr_storage.js]
[test_messages.js]
[test_oauth_grant_client.js]
[test_oauth_grant_client_server.js]
[test_oauth_tokens.js]

View File

@ -903,9 +903,10 @@ ClientEngine.prototype = {
* topic. The callback will receive an array as the subject parameter
* containing objects with the following keys:
*
* uri URI (string) that is requested for display.
* clientId ID of client that sent the command.
* title Title of page that loaded URI (likely) corresponds to.
* uri URI (string) that is requested for display.
* sender.id ID of client that sent the command.
* sender.name Name of client that sent the command.
* title Title of page that loaded URI (likely) corresponds to.
*
* The 'data' parameter to the callback will not be defined.
*
@ -919,7 +920,13 @@ ClientEngine.prototype = {
* String title of page that URI corresponds to. Older clients may not
* send this.
*/
_handleDisplayURIs: function _handleDisplayURIs(uris) {
_handleDisplayURIs(uris) {
uris.forEach(uri => {
uri.sender = {
id: uri.clientId,
name: this.getClientName(uri.clientId)
};
});
Svc.Obs.notify("weave:engine:clients:display-uris", uris);
},

View File

@ -20,6 +20,8 @@ ChromeUtils.defineModuleGetter(this, "Status",
"resource://services-sync/status.js");
ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "IdleService",
"@mozilla.org/widget/idleservice;1",
"nsIIdleService");
@ -531,6 +533,11 @@ SyncScheduler.prototype = {
return;
}
Services.tm.dispatchToMainThread(() => {
// Terrible hack below: we do the fxa messages polling in the sync
// scheduler to get free post-wake/link-state etc detection.
fxAccounts.messages.consumeRemoteMessages().catch(e => {
this._log.error("Error while polling for FxA messages.", e);
});
this.service.sync({engines, why});
});
},

View File

@ -76,7 +76,8 @@
"fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
"fxaccounts.jsm": ["Authentication"],
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
"FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "CAPABILITY_MESSAGES", "CAPABILITY_MESSAGES_SENDTAB", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsMessages.js": ["FxAccountsMessages", "FxAccountsMessagesSender", "FxAccountsMessagesReceiver", "FxAccountsMessagesHandler"],
"FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
"FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
"FxAccountsPush.js": ["FxAccountsPushService"],