ServicesAutomation.sys.mjs (10794B)
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/. */ 4 5 /* 6 * This module is used in automation to connect the browser to 7 * a specific FxA account and trigger FX Sync. 8 * 9 * To use it, you can call this sequence: 10 * 11 * initConfig("https://accounts.stage.mozaws.net"); 12 * await Authentication.signIn(username, password); 13 * await Sync.triggerSync(); 14 * await Authentication.signOut(); 15 * 16 * 17 * Where username is your FxA e-mail. it will connect your browser 18 * to that account and trigger a Sync (on stage servers.) 19 * 20 * You can also use the convenience function that does everything: 21 * 22 * await triggerSync(username, password, "https://accounts.stage.mozaws.net"); 23 * 24 */ 25 26 const lazy = {}; 27 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", 36 }); 37 38 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { 39 return ChromeUtils.importESModule( 40 "resource://gre/modules/FxAccounts.sys.mjs" 41 ).getFxAccountsSingleton(); 42 }); 43 44 const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri"; 45 46 /* 47 * Log helpers. 48 */ 49 var _LOG = []; 50 51 function LOG(msg, error) { 52 console.debug(msg); 53 _LOG.push(msg); 54 if (error) { 55 console.debug(JSON.stringify(error)); 56 _LOG.push(JSON.stringify(error)); 57 } 58 } 59 60 function dumpLogs() { 61 let res = _LOG.join("\n"); 62 _LOG = []; 63 return res; 64 } 65 66 function promiseObserver(aEventName) { 67 LOG("wait for " + aEventName); 68 return new Promise(resolve => { 69 let handler = () => { 70 lazy.Svc.Obs.remove(aEventName, handler); 71 resolve(); 72 }; 73 let handlerTimeout = () => { 74 lazy.Svc.Obs.remove(aEventName, handler); 75 LOG("handler timed out " + aEventName); 76 resolve(); 77 }; 78 lazy.Svc.Obs.add(aEventName, handler); 79 lazy.setTimeout(handlerTimeout, 3000); 80 }); 81 } 82 83 /* 84 * Authentication 85 * 86 * Used to sign in an FxA account, takes care of 87 * the e-mail verification flow. 88 * 89 * Usage: 90 * 91 * await Authentication.signIn(username, password); 92 */ 93 export var Authentication = { 94 async isLoggedIn() { 95 return !!(await this.getSignedInUser()); 96 }, 97 98 async isReady() { 99 let user = await this.getSignedInUser(); 100 if (user) { 101 LOG("current user " + JSON.stringify(user)); 102 } 103 return user && user.verified; 104 }, 105 106 async getSignedInUser() { 107 try { 108 return await lazy.fxAccounts.getSignedInUser(); 109 } catch (error) { 110 LOG("getSignedInUser() failed", error); 111 throw error; 112 } 113 }, 114 115 async _confirmUser(uri) { 116 LOG("Open new tab and load verification page"); 117 let mainWindow = Services.wm.getMostRecentWindow("navigator:browser"); 118 let newtab = mainWindow.gBrowser.addWebTab(uri); 119 let win = mainWindow.gBrowser.getBrowserForTab(newtab); 120 win.addEventListener("load", function () { 121 LOG("load"); 122 }); 123 124 win.addEventListener("loadstart", function () { 125 LOG("loadstart"); 126 }); 127 128 win.addEventListener("error", function (msg, url, lineNo, columnNo, error) { 129 var string = msg.toLowerCase(); 130 var substring = "script error"; 131 if (string.indexOf(substring) > -1) { 132 LOG("Script Error: See Browser Console for Detail"); 133 } else { 134 var message = [ 135 "Message: " + msg, 136 "URL: " + url, 137 "Line: " + lineNo, 138 "Column: " + columnNo, 139 "Error object: " + JSON.stringify(error), 140 ].join(" - "); 141 142 LOG(message); 143 } 144 }); 145 146 LOG("wait for page to load"); 147 await new Promise(resolve => { 148 let handlerTimeout = () => { 149 LOG("timed out "); 150 resolve(); 151 }; 152 var timer = lazy.setTimeout(handlerTimeout, 10000); 153 win.addEventListener("loadend", function () { 154 resolve(); 155 lazy.clearTimeout(timer); 156 }); 157 }); 158 LOG("Page Loaded"); 159 let didVerify = false; 160 LOG("remove tab"); 161 mainWindow.gBrowser.removeTab(newtab); 162 return didVerify; 163 }, 164 165 /* 166 * This whole verification process may be bypassed if the 167 * account is allow-listed. 168 */ 169 async _completeVerification(username) { 170 LOG("Fetching mail (from restmail) for user " + username); 171 let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent( 172 username 173 )}`; 174 let triedAlready = new Set(); 175 const tries = 10; 176 for (let i = 0; i < tries; ++i) { 177 let resp = await fetch(restmailURI); 178 let messages = await resp.json(); 179 // Sort so that the most recent emails are first. 180 messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt)); 181 for (let m of messages) { 182 // We look for a link that has a x-link that we haven't yet tried. 183 if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) { 184 continue; 185 } 186 if (!m.headers["x-verify-code"]) { 187 continue; 188 } 189 let confirmLink = m.headers["x-link"]; 190 triedAlready.add(confirmLink); 191 LOG("Trying confirmation link " + confirmLink); 192 try { 193 if (await this._confirmUser(confirmLink)) { 194 LOG("confirmation done"); 195 return true; 196 } 197 LOG("confirmation failed"); 198 } catch (e) { 199 LOG( 200 "Warning: Failed to follow confirmation link: " + 201 lazy.Log.exceptionStr(e) 202 ); 203 } 204 } 205 if (i === 0) { 206 // first time through after failing we'll do this. 207 LOG("resendVerificationEmail"); 208 await lazy.fxAccounts.resendVerificationEmail(); 209 } 210 } 211 // this is all old, we need verification codes now. 212 return false; 213 }, 214 215 async signIn(username, password) { 216 LOG("Login user: " + username); 217 try { 218 // Required here since we don't go through the real login page 219 LOG("Calling FxAccountsConfig.ensureConfigured"); 220 await lazy.FxAccountsConfig.ensureConfigured(); 221 let client = new lazy.FxAccountsClient(); 222 LOG("Signing in"); 223 let credentials = await client.signIn(username, password, true); 224 LOG("Signed in, setting up the signed user in fxAccounts"); 225 await lazy.fxAccounts._internal.setSignedInUser(credentials); 226 227 // If the account is not allow-listed for tests, we need to verify it 228 if (!credentials.verified) { 229 LOG("We need to verify the account"); 230 await this._completeVerification(username); 231 } else { 232 LOG("Credentials already verified"); 233 } 234 return true; 235 } catch (error) { 236 LOG("signIn() failed", error); 237 throw error; 238 } 239 }, 240 241 async signOut() { 242 if (await Authentication.isLoggedIn()) { 243 // Note: This will clean up the device ID. 244 await lazy.fxAccounts.signOut(); 245 } 246 }, 247 }; 248 249 /* 250 * Sync 251 * 252 * Used to trigger sync. 253 * 254 * usage: 255 * 256 * await Sync.triggerSync(); 257 */ 258 export var Sync = { 259 getSyncLogsDirectory() { 260 return PathUtils.join(PathUtils.profileDir, "weave", "logs"); 261 }, 262 263 async init() { 264 lazy.Svc.Obs.add("weave:service:sync:error", this); 265 lazy.Svc.Obs.add("weave:service:setup-complete", this); 266 lazy.Svc.Obs.add("weave:service:tracking-started", this); 267 // Delay the automatic sync operations, so we can trigger it manually 268 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200); 269 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200); 270 lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200); 271 lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000); 272 // Wipe all the logs 273 await this.wipeLogs(); 274 }, 275 276 observe(subject, topic) { 277 LOG("Event received " + topic); 278 }, 279 280 async configureSync() { 281 // todo, enable all sync engines here 282 // the addon engine requires kinto creds... 283 LOG("configuring sync"); 284 console.assert(await Authentication.isReady(), "You are not connected"); 285 await lazy.Weave.Service.configure(); 286 if (!lazy.Weave.Status.ready) { 287 await promiseObserver("weave:service:ready"); 288 } 289 if (lazy.Weave.Service.locked) { 290 await promiseObserver("weave:service:resyncs-finished"); 291 } 292 }, 293 294 /* 295 * triggerSync() runs the whole process of Syncing. 296 * 297 * returns 1 on success, 0 on failure. 298 */ 299 async triggerSync() { 300 if (!(await Authentication.isLoggedIn())) { 301 LOG("Not connected"); 302 return 1; 303 } 304 await this.init(); 305 let result = 1; 306 try { 307 await this.configureSync(); 308 LOG("Triggering a sync"); 309 await lazy.Weave.Service.sync(); 310 311 // wait a second for things to settle... 312 await new Promise(resolve => lazy.setTimeout(resolve, 1000)); 313 314 LOG("Sync done"); 315 result = 0; 316 } catch (error) { 317 LOG("triggerSync() failed", error); 318 } 319 320 return result; 321 }, 322 323 async wipeLogs() { 324 let outputDirectory = this.getSyncLogsDirectory(); 325 if (!(await IOUtils.exists(outputDirectory))) { 326 return; 327 } 328 LOG("Wiping existing Sync logs"); 329 try { 330 await IOUtils.remove(outputDirectory, { recursive: true }); 331 } catch (error) { 332 LOG("wipeLogs() failed", error); 333 } 334 }, 335 336 async getLogs() { 337 let outputDirectory = this.getSyncLogsDirectory(); 338 let entries = []; 339 340 if (await IOUtils.exists(outputDirectory)) { 341 // Iterate through the directory 342 for (const path of await IOUtils.getChildren(outputDirectory)) { 343 const info = await IOUtils.stat(path); 344 345 entries.push({ 346 path, 347 name: PathUtils.filename(path), 348 lastModified: info.lastModified, 349 }); 350 } 351 352 entries.sort(function (a, b) { 353 return b.lastModified - a.lastModified; 354 }); 355 } 356 357 const promises = entries.map(async entry => { 358 const content = await IOUtils.readUTF8(entry.path); 359 return { 360 name: entry.name, 361 content, 362 }; 363 }); 364 return Promise.all(promises); 365 }, 366 }; 367 368 export function initConfig(autoconfig) { 369 Services.prefs.setStringPref(AUTOCONFIG_PREF, autoconfig); 370 } 371 372 export async function triggerSync(username, password, autoconfig) { 373 initConfig(autoconfig); 374 await Authentication.signIn(username, password); 375 var result = await Sync.triggerSync(); 376 await Authentication.signOut(); 377 var logs = { 378 sync: await Sync.getLogs(), 379 condprof: [ 380 { 381 name: "console.txt", 382 content: dumpLogs(), 383 }, 384 ], 385 }; 386 return { 387 result, 388 logs, 389 }; 390 }