diff options
Diffstat (limited to 'services/automation')
-rw-r--r-- | services/automation/AutomationComponents.manifest | 2 | ||||
-rw-r--r-- | services/automation/ServicesAutomation.sys.mjs | 416 | ||||
-rw-r--r-- | services/automation/moz.build | 14 |
3 files changed, 432 insertions, 0 deletions
diff --git a/services/automation/AutomationComponents.manifest b/services/automation/AutomationComponents.manifest new file mode 100644 index 0000000000..d44fe90a22 --- /dev/null +++ b/services/automation/AutomationComponents.manifest @@ -0,0 +1,2 @@ +# Register resource aliases +resource services-automation resource://gre/modules/services-automation/ diff --git a/services/automation/ServicesAutomation.sys.mjs b/services/automation/ServicesAutomation.sys.mjs new file mode 100644 index 0000000000..24316d8827 --- /dev/null +++ b/services/automation/ServicesAutomation.sys.mjs @@ -0,0 +1,416 @@ +/* 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 module is used in automation to connect the browser to + * a specific FxA account and trigger FX Sync. + * + * To use it, you can call this sequence: + * + * initConfig("https://accounts.stage.mozaws.net"); + * await Authentication.signIn(username, password); + * await Sync.triggerSync(); + * await Authentication.signOut(); + * + * + * Where username is your FxA e-mail. it will connect your browser + * to that account and trigger a Sync (on stage servers.) + * + * You can also use the convenience function that does everything: + * + * await triggerSync(username, password, "https://accounts.stage.mozaws.net"); + * + */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs", + FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs", + Log: "resource://gre/modules/Log.sys.mjs", + Svc: "resource://services-sync/util.sys.mjs", + Weave: "resource://services-sync/main.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); + +const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri"; + +/* + * Log helpers. + */ +var _LOG = []; + +function LOG(msg, error) { + console.debug(msg); + _LOG.push(msg); + if (error) { + console.debug(JSON.stringify(error)); + _LOG.push(JSON.stringify(error)); + } +} + +function dumpLogs() { + let res = _LOG.join("\n"); + _LOG = []; + return res; +} + +function promiseObserver(aEventName) { + LOG("wait for " + aEventName); + return new Promise(resolve => { + let handler = () => { + lazy.Svc.Obs.remove(aEventName, handler); + resolve(); + }; + let handlerTimeout = () => { + lazy.Svc.Obs.remove(aEventName, handler); + LOG("handler timed out " + aEventName); + resolve(); + }; + lazy.Svc.Obs.add(aEventName, handler); + lazy.setTimeout(handlerTimeout, 3000); + }); +} + +/* + * Authentication + * + * Used to sign in an FxA account, takes care of + * the e-mail verification flow. + * + * Usage: + * + * await Authentication.signIn(username, password); + */ +export var Authentication = { + async isLoggedIn() { + return !!(await this.getSignedInUser()); + }, + + async isReady() { + let user = await this.getSignedInUser(); + if (user) { + LOG("current user " + JSON.stringify(user)); + } + return user && user.verified; + }, + + async getSignedInUser() { + try { + return await lazy.fxAccounts.getSignedInUser(); + } catch (error) { + LOG("getSignedInUser() failed", error); + throw error; + } + }, + + async shortWaitForVerification(ms) { + LOG("shortWaitForVerification"); + let userData = await this.getSignedInUser(); + let timeoutID; + LOG("set a timeout"); + let timeoutPromise = new Promise(resolve => { + timeoutID = lazy.setTimeout(() => { + LOG(`Warning: no verification after ${ms}ms.`); + resolve(); + }, ms); + }); + LOG("set a fxAccounts.whenVerified"); + await Promise.race([ + lazy.fxAccounts + .whenVerified(userData) + .finally(() => lazy.clearTimeout(timeoutID)), + timeoutPromise, + ]); + LOG("done"); + return this.isReady(); + }, + + async _confirmUser(uri) { + LOG("Open new tab and load verification page"); + let mainWindow = Services.wm.getMostRecentWindow("navigator:browser"); + let newtab = mainWindow.gBrowser.addWebTab(uri); + let win = mainWindow.gBrowser.getBrowserForTab(newtab); + win.addEventListener("load", function (e) { + LOG("load"); + }); + + win.addEventListener("loadstart", function (e) { + LOG("loadstart"); + }); + + win.addEventListener("error", function (msg, url, lineNo, columnNo, error) { + var string = msg.toLowerCase(); + var substring = "script error"; + if (string.indexOf(substring) > -1) { + LOG("Script Error: See Browser Console for Detail"); + } else { + var message = [ + "Message: " + msg, + "URL: " + url, + "Line: " + lineNo, + "Column: " + columnNo, + "Error object: " + JSON.stringify(error), + ].join(" - "); + + LOG(message); + } + }); + + LOG("wait for page to load"); + await new Promise(resolve => { + let handlerTimeout = () => { + LOG("timed out "); + resolve(); + }; + var timer = lazy.setTimeout(handlerTimeout, 10000); + win.addEventListener("loadend", function () { + resolve(); + lazy.clearTimeout(timer); + }); + }); + LOG("Page Loaded"); + let didVerify = await this.shortWaitForVerification(10000); + LOG("remove tab"); + mainWindow.gBrowser.removeTab(newtab); + return didVerify; + }, + + /* + * This whole verification process may be bypassed if the + * account is allow-listed. + */ + async _completeVerification(username) { + LOG("Fetching mail (from restmail) for user " + username); + let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent( + username + )}`; + let triedAlready = new Set(); + const tries = 10; + const normalWait = 4000; + for (let i = 0; i < tries; ++i) { + let resp = await fetch(restmailURI); + let messages = await resp.json(); + // Sort so that the most recent emails are first. + messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt)); + for (let m of messages) { + // We look for a link that has a x-link that we haven't yet tried. + if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) { + continue; + } + if (!m.headers["x-verify-code"]) { + continue; + } + let confirmLink = m.headers["x-link"]; + triedAlready.add(confirmLink); + LOG("Trying confirmation link " + confirmLink); + try { + if (await this._confirmUser(confirmLink)) { + LOG("confirmation done"); + return true; + } + LOG("confirmation failed"); + } catch (e) { + LOG( + "Warning: Failed to follow confirmation link: " + + lazy.Log.exceptionStr(e) + ); + } + } + if (i === 0) { + // first time through after failing we'll do this. + LOG("resendVerificationEmail"); + await lazy.fxAccounts.resendVerificationEmail(); + } + if (await this.shortWaitForVerification(normalWait)) { + return true; + } + } + // One last try. + return this.shortWaitForVerification(normalWait); + }, + + async signIn(username, password) { + LOG("Login user: " + username); + try { + // Required here since we don't go through the real login page + LOG("Calling FxAccountsConfig.ensureConfigured"); + await lazy.FxAccountsConfig.ensureConfigured(); + let client = new lazy.FxAccountsClient(); + LOG("Signing in"); + let credentials = await client.signIn(username, password, true); + LOG("Signed in, setting up the signed user in fxAccounts"); + await lazy.fxAccounts._internal.setSignedInUser(credentials); + + // If the account is not allow-listed for tests, we need to verify it + if (!credentials.verified) { + LOG("We need to verify the account"); + await this._completeVerification(username); + } else { + LOG("Credentials already verified"); + } + return true; + } catch (error) { + LOG("signIn() failed", error); + throw error; + } + }, + + async signOut() { + if (await Authentication.isLoggedIn()) { + // Note: This will clean up the device ID. + await lazy.fxAccounts.signOut(); + } + }, +}; + +/* + * Sync + * + * Used to trigger sync. + * + * usage: + * + * await Sync.triggerSync(); + */ +export var Sync = { + getSyncLogsDirectory() { + return PathUtils.join(PathUtils.profileDir, "weave", "logs"); + }, + + async init() { + lazy.Svc.Obs.add("weave:service:sync:error", this); + lazy.Svc.Obs.add("weave:service:setup-complete", this); + lazy.Svc.Obs.add("weave:service:tracking-started", this); + // Delay the automatic sync operations, so we can trigger it manually + lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200); + lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200); + lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200); + lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000); + // Wipe all the logs + await this.wipeLogs(); + }, + + observe(subject, topic, data) { + LOG("Event received " + topic); + }, + + async configureSync() { + // todo, enable all sync engines here + // the addon engine requires kinto creds... + LOG("configuring sync"); + console.assert(await Authentication.isReady(), "You are not connected"); + await lazy.Weave.Service.configure(); + if (!lazy.Weave.Status.ready) { + await promiseObserver("weave:service:ready"); + } + if (lazy.Weave.Service.locked) { + await promiseObserver("weave:service:resyncs-finished"); + } + }, + + /* + * triggerSync() runs the whole process of Syncing. + * + * returns 1 on success, 0 on failure. + */ + async triggerSync() { + if (!(await Authentication.isLoggedIn())) { + LOG("Not connected"); + return 1; + } + await this.init(); + let result = 1; + try { + await this.configureSync(); + LOG("Triggering a sync"); + await lazy.Weave.Service.sync(); + + // wait a second for things to settle... + await new Promise(resolve => lazy.setTimeout(resolve, 1000)); + + LOG("Sync done"); + result = 0; + } catch (error) { + LOG("triggerSync() failed", error); + } + + return result; + }, + + async wipeLogs() { + let outputDirectory = this.getSyncLogsDirectory(); + if (!(await IOUtils.exists(outputDirectory))) { + return; + } + LOG("Wiping existing Sync logs"); + try { + await IOUtils.remove(outputDirectory, { recursive: true }); + } catch (error) { + LOG("wipeLogs() failed", error); + } + }, + + async getLogs() { + let outputDirectory = this.getSyncLogsDirectory(); + let entries = []; + + if (await IOUtils.exists(outputDirectory)) { + // Iterate through the directory + for (const path of await IOUtils.getChildren(outputDirectory)) { + const info = await IOUtils.stat(path); + + entries.push({ + path, + name: PathUtils.filename(path), + lastModified: info.lastModified, + }); + } + + entries.sort(function (a, b) { + return b.lastModified - a.lastModified; + }); + } + + const promises = entries.map(async entry => { + const content = await IOUtils.readUTF8(entry.path); + return { + name: entry.name, + content, + }; + }); + return Promise.all(promises); + }, +}; + +export function initConfig(autoconfig) { + Services.prefs.setStringPref(AUTOCONFIG_PREF, autoconfig); +} + +export async function triggerSync(username, password, autoconfig) { + initConfig(autoconfig); + await Authentication.signIn(username, password); + var result = await Sync.triggerSync(); + await Authentication.signOut(); + var logs = { + sync: await Sync.getLogs(), + condprof: [ + { + name: "console.txt", + content: dumpLogs(), + }, + ], + }; + return { + result, + logs, + }; +} diff --git a/services/automation/moz.build b/services/automation/moz.build new file mode 100644 index 0000000000..61b4815f42 --- /dev/null +++ b/services/automation/moz.build @@ -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/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Sync") + +EXTRA_COMPONENTS += [ + "AutomationComponents.manifest", +] + +EXTRA_JS_MODULES["services-automation"] += [ + "ServicesAutomation.sys.mjs", +] |