1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module is used in automation to connect the browser to
7 * a specific FxA account and trigger FX Sync.
9 * To use it, you can call this sequence:
11 * initConfig("https://accounts.stage.mozaws.net");
12 * await Authentication.signIn(username, password);
13 * await Sync.triggerSync();
14 * await Authentication.signOut();
17 * Where username is your FxA e-mail. it will connect your browser
18 * to that account and trigger a Sync (on stage servers.)
20 * You can also use the convenience function that does everything:
22 * await triggerSync(username, password, "https://accounts.stage.mozaws.net");
25 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
29 ChromeUtils.defineESModuleGetters(lazy, {
30 FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs",
31 FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs",
32 Log: "resource://gre/modules/Log.sys.mjs",
33 Svc: "resource://services-sync/util.sys.mjs",
34 Weave: "resource://services-sync/main.sys.mjs",
35 clearTimeout: "resource://gre/modules/Timer.sys.mjs",
36 setTimeout: "resource://gre/modules/Timer.sys.mjs",
39 XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
40 return ChromeUtils.importESModule(
41 "resource://gre/modules/FxAccounts.sys.mjs"
42 ).getFxAccountsSingleton();
45 const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri";
52 function LOG(msg, error) {
56 console.debug(JSON.stringify(error));
57 _LOG.push(JSON.stringify(error));
62 let res = _LOG.join("\n");
67 function promiseObserver(aEventName) {
68 LOG("wait for " + aEventName);
69 return new Promise(resolve => {
71 lazy.Svc.Obs.remove(aEventName, handler);
74 let handlerTimeout = () => {
75 lazy.Svc.Obs.remove(aEventName, handler);
76 LOG("handler timed out " + aEventName);
79 lazy.Svc.Obs.add(aEventName, handler);
80 lazy.setTimeout(handlerTimeout, 3000);
87 * Used to sign in an FxA account, takes care of
88 * the e-mail verification flow.
92 * await Authentication.signIn(username, password);
94 export var Authentication = {
96 return !!(await this.getSignedInUser());
100 let user = await this.getSignedInUser();
102 LOG("current user " + JSON.stringify(user));
104 return user && user.verified;
107 async getSignedInUser() {
109 return await lazy.fxAccounts.getSignedInUser();
111 LOG("getSignedInUser() failed", error);
116 async shortWaitForVerification(ms) {
117 LOG("shortWaitForVerification");
118 let userData = await this.getSignedInUser();
120 LOG("set a timeout");
121 let timeoutPromise = new Promise(resolve => {
122 timeoutID = lazy.setTimeout(() => {
123 LOG(`Warning: no verification after ${ms}ms.`);
127 LOG("set a fxAccounts.whenVerified");
130 .whenVerified(userData)
131 .finally(() => lazy.clearTimeout(timeoutID)),
135 return this.isReady();
138 async _confirmUser(uri) {
139 LOG("Open new tab and load verification page");
140 let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
141 let newtab = mainWindow.gBrowser.addWebTab(uri);
142 let win = mainWindow.gBrowser.getBrowserForTab(newtab);
143 win.addEventListener("load", function (e) {
147 win.addEventListener("loadstart", function (e) {
151 win.addEventListener("error", function (msg, url, lineNo, columnNo, error) {
152 var string = msg.toLowerCase();
153 var substring = "script error";
154 if (string.indexOf(substring) > -1) {
155 LOG("Script Error: See Browser Console for Detail");
161 "Column: " + columnNo,
162 "Error object: " + JSON.stringify(error),
169 LOG("wait for page to load");
170 await new Promise(resolve => {
171 let handlerTimeout = () => {
175 var timer = lazy.setTimeout(handlerTimeout, 10000);
176 win.addEventListener("loadend", function () {
178 lazy.clearTimeout(timer);
182 let didVerify = await this.shortWaitForVerification(10000);
184 mainWindow.gBrowser.removeTab(newtab);
189 * This whole verification process may be bypassed if the
190 * account is allow-listed.
192 async _completeVerification(username) {
193 LOG("Fetching mail (from restmail) for user " + username);
194 let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
197 let triedAlready = new Set();
199 const normalWait = 4000;
200 for (let i = 0; i < tries; ++i) {
201 let resp = await fetch(restmailURI);
202 let messages = await resp.json();
203 // Sort so that the most recent emails are first.
204 messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt));
205 for (let m of messages) {
206 // We look for a link that has a x-link that we haven't yet tried.
207 if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) {
210 if (!m.headers["x-verify-code"]) {
213 let confirmLink = m.headers["x-link"];
214 triedAlready.add(confirmLink);
215 LOG("Trying confirmation link " + confirmLink);
217 if (await this._confirmUser(confirmLink)) {
218 LOG("confirmation done");
221 LOG("confirmation failed");
224 "Warning: Failed to follow confirmation link: " +
225 lazy.Log.exceptionStr(e)
230 // first time through after failing we'll do this.
231 LOG("resendVerificationEmail");
232 await lazy.fxAccounts.resendVerificationEmail();
234 if (await this.shortWaitForVerification(normalWait)) {
239 return this.shortWaitForVerification(normalWait);
242 async signIn(username, password) {
243 LOG("Login user: " + username);
245 // Required here since we don't go through the real login page
246 LOG("Calling FxAccountsConfig.ensureConfigured");
247 await lazy.FxAccountsConfig.ensureConfigured();
248 let client = new lazy.FxAccountsClient();
250 let credentials = await client.signIn(username, password, true);
251 LOG("Signed in, setting up the signed user in fxAccounts");
252 await lazy.fxAccounts._internal.setSignedInUser(credentials);
254 // If the account is not allow-listed for tests, we need to verify it
255 if (!credentials.verified) {
256 LOG("We need to verify the account");
257 await this._completeVerification(username);
259 LOG("Credentials already verified");
263 LOG("signIn() failed", error);
269 if (await Authentication.isLoggedIn()) {
270 // Note: This will clean up the device ID.
271 await lazy.fxAccounts.signOut();
279 * Used to trigger sync.
283 * await Sync.triggerSync();
286 getSyncLogsDirectory() {
287 return PathUtils.join(PathUtils.profileDir, "weave", "logs");
291 lazy.Svc.Obs.add("weave:service:sync:error", this);
292 lazy.Svc.Obs.add("weave:service:setup-complete", this);
293 lazy.Svc.Obs.add("weave:service:tracking-started", this);
294 // Delay the automatic sync operations, so we can trigger it manually
295 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200);
296 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200);
297 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200);
298 lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000);
300 await this.wipeLogs();
303 observe(subject, topic, data) {
304 LOG("Event received " + topic);
307 async configureSync() {
308 // todo, enable all sync engines here
309 // the addon engine requires kinto creds...
310 LOG("configuring sync");
311 console.assert(await Authentication.isReady(), "You are not connected");
312 await lazy.Weave.Service.configure();
313 if (!lazy.Weave.Status.ready) {
314 await promiseObserver("weave:service:ready");
316 if (lazy.Weave.Service.locked) {
317 await promiseObserver("weave:service:resyncs-finished");
322 * triggerSync() runs the whole process of Syncing.
324 * returns 1 on success, 0 on failure.
326 async triggerSync() {
327 if (!(await Authentication.isLoggedIn())) {
328 LOG("Not connected");
334 await this.configureSync();
335 LOG("Triggering a sync");
336 await lazy.Weave.Service.sync();
338 // wait a second for things to settle...
339 await new Promise(resolve => lazy.setTimeout(resolve, 1000));
344 LOG("triggerSync() failed", error);
351 let outputDirectory = this.getSyncLogsDirectory();
352 if (!(await IOUtils.exists(outputDirectory))) {
355 LOG("Wiping existing Sync logs");
357 await IOUtils.remove(outputDirectory, { recursive: true });
359 LOG("wipeLogs() failed", error);
364 let outputDirectory = this.getSyncLogsDirectory();
367 if (await IOUtils.exists(outputDirectory)) {
368 // Iterate through the directory
369 for (const path of await IOUtils.getChildren(outputDirectory)) {
370 const info = await IOUtils.stat(path);
374 name: PathUtils.filename(path),
375 lastModified: info.lastModified,
379 entries.sort(function (a, b) {
380 return b.lastModified - a.lastModified;
384 const promises = entries.map(async entry => {
385 const content = await IOUtils.readUTF8(entry.path);
391 return Promise.all(promises);
395 export function initConfig(autoconfig) {
396 Services.prefs.setCharPref(AUTOCONFIG_PREF, autoconfig);
399 export async function triggerSync(username, password, autoconfig) {
400 initConfig(autoconfig);
401 await Authentication.signIn(username, password);
402 var result = await Sync.triggerSync();
403 await Authentication.signOut();
405 sync: await Sync.getLogs(),