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");
28 ChromeUtils.defineESModuleGetters(lazy, {
29 FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs",
30 FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs",
31 Log: "resource://gre/modules/Log.sys.mjs",
32 Svc: "resource://services-sync/util.sys.mjs",
33 Weave: "resource://services-sync/main.sys.mjs",
34 clearTimeout: "resource://gre/modules/Timer.sys.mjs",
35 setTimeout: "resource://gre/modules/Timer.sys.mjs",
38 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
39 return ChromeUtils.importESModule(
40 "resource://gre/modules/FxAccounts.sys.mjs"
41 ).getFxAccountsSingleton();
44 const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri";
51 function LOG(msg, error) {
55 console.debug(JSON.stringify(error));
56 _LOG.push(JSON.stringify(error));
61 let res = _LOG.join("\n");
66 function promiseObserver(aEventName) {
67 LOG("wait for " + aEventName);
68 return new Promise(resolve => {
70 lazy.Svc.Obs.remove(aEventName, handler);
73 let handlerTimeout = () => {
74 lazy.Svc.Obs.remove(aEventName, handler);
75 LOG("handler timed out " + aEventName);
78 lazy.Svc.Obs.add(aEventName, handler);
79 lazy.setTimeout(handlerTimeout, 3000);
86 * Used to sign in an FxA account, takes care of
87 * the e-mail verification flow.
91 * await Authentication.signIn(username, password);
93 export var Authentication = {
95 return !!(await this.getSignedInUser());
99 let user = await this.getSignedInUser();
101 LOG("current user " + JSON.stringify(user));
103 return user && user.verified;
106 async getSignedInUser() {
108 return await lazy.fxAccounts.getSignedInUser();
110 LOG("getSignedInUser() failed", error);
115 async shortWaitForVerification(ms) {
116 LOG("shortWaitForVerification");
117 let userData = await this.getSignedInUser();
119 LOG("set a timeout");
120 let timeoutPromise = new Promise(resolve => {
121 timeoutID = lazy.setTimeout(() => {
122 LOG(`Warning: no verification after ${ms}ms.`);
126 LOG("set a fxAccounts.whenVerified");
129 .whenVerified(userData)
130 .finally(() => lazy.clearTimeout(timeoutID)),
134 return this.isReady();
137 async _confirmUser(uri) {
138 LOG("Open new tab and load verification page");
139 let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
140 let newtab = mainWindow.gBrowser.addWebTab(uri);
141 let win = mainWindow.gBrowser.getBrowserForTab(newtab);
142 win.addEventListener("load", function () {
146 win.addEventListener("loadstart", function () {
150 win.addEventListener("error", function (msg, url, lineNo, columnNo, error) {
151 var string = msg.toLowerCase();
152 var substring = "script error";
153 if (string.indexOf(substring) > -1) {
154 LOG("Script Error: See Browser Console for Detail");
160 "Column: " + columnNo,
161 "Error object: " + JSON.stringify(error),
168 LOG("wait for page to load");
169 await new Promise(resolve => {
170 let handlerTimeout = () => {
174 var timer = lazy.setTimeout(handlerTimeout, 10000);
175 win.addEventListener("loadend", function () {
177 lazy.clearTimeout(timer);
181 let didVerify = await this.shortWaitForVerification(10000);
183 mainWindow.gBrowser.removeTab(newtab);
188 * This whole verification process may be bypassed if the
189 * account is allow-listed.
191 async _completeVerification(username) {
192 LOG("Fetching mail (from restmail) for user " + username);
193 let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
196 let triedAlready = new Set();
198 const normalWait = 4000;
199 for (let i = 0; i < tries; ++i) {
200 let resp = await fetch(restmailURI);
201 let messages = await resp.json();
202 // Sort so that the most recent emails are first.
203 messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt));
204 for (let m of messages) {
205 // We look for a link that has a x-link that we haven't yet tried.
206 if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) {
209 if (!m.headers["x-verify-code"]) {
212 let confirmLink = m.headers["x-link"];
213 triedAlready.add(confirmLink);
214 LOG("Trying confirmation link " + confirmLink);
216 if (await this._confirmUser(confirmLink)) {
217 LOG("confirmation done");
220 LOG("confirmation failed");
223 "Warning: Failed to follow confirmation link: " +
224 lazy.Log.exceptionStr(e)
229 // first time through after failing we'll do this.
230 LOG("resendVerificationEmail");
231 await lazy.fxAccounts.resendVerificationEmail();
233 if (await this.shortWaitForVerification(normalWait)) {
238 return this.shortWaitForVerification(normalWait);
241 async signIn(username, password) {
242 LOG("Login user: " + username);
244 // Required here since we don't go through the real login page
245 LOG("Calling FxAccountsConfig.ensureConfigured");
246 await lazy.FxAccountsConfig.ensureConfigured();
247 let client = new lazy.FxAccountsClient();
249 let credentials = await client.signIn(username, password, true);
250 LOG("Signed in, setting up the signed user in fxAccounts");
251 await lazy.fxAccounts._internal.setSignedInUser(credentials);
253 // If the account is not allow-listed for tests, we need to verify it
254 if (!credentials.verified) {
255 LOG("We need to verify the account");
256 await this._completeVerification(username);
258 LOG("Credentials already verified");
262 LOG("signIn() failed", error);
268 if (await Authentication.isLoggedIn()) {
269 // Note: This will clean up the device ID.
270 await lazy.fxAccounts.signOut();
278 * Used to trigger sync.
282 * await Sync.triggerSync();
285 getSyncLogsDirectory() {
286 return PathUtils.join(PathUtils.profileDir, "weave", "logs");
290 lazy.Svc.Obs.add("weave:service:sync:error", this);
291 lazy.Svc.Obs.add("weave:service:setup-complete", this);
292 lazy.Svc.Obs.add("weave:service:tracking-started", this);
293 // Delay the automatic sync operations, so we can trigger it manually
294 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200);
295 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200);
296 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200);
297 lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000);
299 await this.wipeLogs();
302 observe(subject, topic) {
303 LOG("Event received " + topic);
306 async configureSync() {
307 // todo, enable all sync engines here
308 // the addon engine requires kinto creds...
309 LOG("configuring sync");
310 console.assert(await Authentication.isReady(), "You are not connected");
311 await lazy.Weave.Service.configure();
312 if (!lazy.Weave.Status.ready) {
313 await promiseObserver("weave:service:ready");
315 if (lazy.Weave.Service.locked) {
316 await promiseObserver("weave:service:resyncs-finished");
321 * triggerSync() runs the whole process of Syncing.
323 * returns 1 on success, 0 on failure.
325 async triggerSync() {
326 if (!(await Authentication.isLoggedIn())) {
327 LOG("Not connected");
333 await this.configureSync();
334 LOG("Triggering a sync");
335 await lazy.Weave.Service.sync();
337 // wait a second for things to settle...
338 await new Promise(resolve => lazy.setTimeout(resolve, 1000));
343 LOG("triggerSync() failed", error);
350 let outputDirectory = this.getSyncLogsDirectory();
351 if (!(await IOUtils.exists(outputDirectory))) {
354 LOG("Wiping existing Sync logs");
356 await IOUtils.remove(outputDirectory, { recursive: true });
358 LOG("wipeLogs() failed", error);
363 let outputDirectory = this.getSyncLogsDirectory();
366 if (await IOUtils.exists(outputDirectory)) {
367 // Iterate through the directory
368 for (const path of await IOUtils.getChildren(outputDirectory)) {
369 const info = await IOUtils.stat(path);
373 name: PathUtils.filename(path),
374 lastModified: info.lastModified,
378 entries.sort(function (a, b) {
379 return b.lastModified - a.lastModified;
383 const promises = entries.map(async entry => {
384 const content = await IOUtils.readUTF8(entry.path);
390 return Promise.all(promises);
394 export function initConfig(autoconfig) {
395 Services.prefs.setStringPref(AUTOCONFIG_PREF, autoconfig);
398 export async function triggerSync(username, password, autoconfig) {
399 initConfig(autoconfig);
400 await Authentication.signIn(username, password);
401 var result = await Sync.triggerSync();
402 await Authentication.signOut();
404 sync: await Sync.getLogs(),