tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }