tor-browser

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

SyncDisconnect.sys.mjs (9017B)


      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 // This module provides a facility for disconnecting Sync and FxA, optionally
      6 // sanitizing profile data as part of the process.
      7 
      8 const lazy = {};
      9 
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
     12  Log: "resource://gre/modules/Log.sys.mjs",
     13  PREF_LAST_FXA_USER_EMAIL: "resource://gre/modules/FxAccountsCommon.sys.mjs",
     14  PREF_LAST_FXA_USER_UID: "resource://gre/modules/FxAccountsCommon.sys.mjs",
     15  Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
     16  Utils: "resource://services-sync/util.sys.mjs",
     17  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     18 });
     19 
     20 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
     21  return ChromeUtils.importESModule(
     22    "resource://gre/modules/FxAccounts.sys.mjs"
     23  ).getFxAccountsSingleton();
     24 });
     25 
     26 export const SyncDisconnectInternal = {
     27  lockRetryInterval: 1000, // wait 1 seconds before trying for the lock again.
     28  lockRetryCount: 120, // Try 120 times (==2 mins) before giving up in disgust.
     29  promiseDisconnectFinished: null, // If we are sanitizing, a promise for completion.
     30 
     31  // mocked by tests.
     32  getWeave() {
     33    return ChromeUtils.importESModule("resource://services-sync/main.sys.mjs")
     34      .Weave;
     35  },
     36 
     37  // Returns a promise that resolves when we are not syncing, waiting until
     38  // a current Sync completes if necessary. Resolves with true if we
     39  // successfully waited, in which case the sync lock will have been taken to
     40  // ensure future syncs don't state, or resolves with false if we gave up
     41  // waiting for the sync to complete (in which case we didn't take a lock -
     42  // but note that Sync probably remains locked in this case regardless.)
     43  async promiseNotSyncing(abortController) {
     44    let weave = this.getWeave();
     45    let log = lazy.Log.repository.getLogger("Sync.Service");
     46    // We might be syncing - poll for up to 2 minutes waiting for the lock.
     47    // (2 minutes seems extreme, but should be very rare.)
     48    return new Promise(resolve => {
     49      abortController.signal.onabort = () => {
     50        resolve(false);
     51      };
     52 
     53      let attempts = 0;
     54      let checkLock = () => {
     55        if (abortController.signal.aborted) {
     56          // We've already resolved, so don't want a new timer to ever start.
     57          return;
     58        }
     59        if (weave.Service.lock()) {
     60          resolve(true);
     61          return;
     62        }
     63        attempts += 1;
     64        if (attempts >= this.lockRetryCount) {
     65          log.error(
     66            "Gave up waiting for the sync lock - going ahead with sanitize anyway"
     67          );
     68          resolve(false);
     69          return;
     70        }
     71        log.debug("Waiting a couple of seconds to get the sync lock");
     72        lazy.setTimeout(checkLock, this.lockRetryInterval);
     73      };
     74      checkLock();
     75    });
     76  },
     77 
     78  // Sanitize Sync-related data.
     79  async doSanitizeSyncData() {
     80    let weave = this.getWeave();
     81    // Get the sync logger - if stuff goes wrong it can be useful to have that
     82    // recorded in the sync logs.
     83    let log = lazy.Log.repository.getLogger("Sync.Service");
     84    log.info("Starting santitize of Sync data");
     85    try {
     86      // We clobber data for all Sync engines that are enabled.
     87      await weave.Service.promiseInitialized;
     88      weave.Service.enabled = false;
     89 
     90      log.info("starting actual sanitization");
     91      for (let engine of weave.Service.engineManager.getAll()) {
     92        if (engine.enabled) {
     93          try {
     94            log.info("Wiping engine", engine.name);
     95            await engine.wipeClient();
     96          } catch (ex) {
     97            log.error("Failed to wipe engine", ex);
     98          }
     99        }
    100      }
    101      // Reset the pref which is used to show a warning when a different user
    102      // signs in - this is no longer a concern now that we've removed the
    103      // data from the profile.
    104      Services.prefs.clearUserPref(lazy.PREF_LAST_FXA_USER_EMAIL);
    105      Services.prefs.clearUserPref(lazy.PREF_LAST_FXA_USER_UID);
    106 
    107      log.info("Finished wiping sync data");
    108    } catch (ex) {
    109      log.error("Failed to sanitize Sync data", ex);
    110      console.error("Failed to sanitize Sync data", ex);
    111    }
    112    try {
    113      // ensure any logs we wrote are flushed to disk.
    114      await weave.Service.errorHandler.resetFileLog();
    115    } catch (ex) {
    116      console.log("Failed to flush the Sync log", ex);
    117    }
    118  },
    119 
    120  // Sanitize all Browser data.
    121  async doSanitizeBrowserData() {
    122    try {
    123      // sanitize everything other than "open windows" (and we don't do that
    124      // because it may confuse the user - they probably want to see
    125      // about:prefs with the disconnection reflected.
    126      let itemsToClear = Object.keys(lazy.Sanitizer.items).filter(
    127        k => k != "openWindows"
    128      );
    129      await lazy.Sanitizer.sanitize(itemsToClear);
    130    } catch (ex) {
    131      console.error("Failed to sanitize other data", ex);
    132    }
    133  },
    134 
    135  async doSyncAndAccountDisconnect(shouldUnlock) {
    136    // We do a startOver of Sync first - if we do the account first we end
    137    // up with Sync configured but FxA not configured, which causes the browser
    138    // UI to briefly enter a "needs reauth" state.
    139    let Weave = this.getWeave();
    140    await Weave.Service.promiseInitialized;
    141    await Weave.Service.startOver();
    142    await lazy.fxAccounts.signOut();
    143    // Sync may have been disabled if we santized, so re-enable it now or
    144    // else the user will be unable to resync should they sign in before a
    145    // restart.
    146    Weave.Service.enabled = true;
    147 
    148    // and finally, if we managed to get the lock before, we should unlock it
    149    // now.
    150    if (shouldUnlock) {
    151      Weave.Service.unlock();
    152    }
    153  },
    154 
    155  // Start the sanitization process. Returns a promise that resolves when
    156  // the sanitize is complete, and an AbortController which can be used to
    157  // abort the process of waiting for a sync to complete.
    158  async _startDisconnect(abortController, sanitizeData = false) {
    159    // This is a bit convoluted - we want to wait for a sync to finish before
    160    // sanitizing, but want to abort that wait if the browser shuts down while
    161    // we are waiting (in which case we'll charge ahead anyway).
    162    // So we do this by using an AbortController and passing that to the
    163    // function that waits for the sync lock - it will immediately resolve
    164    // if the abort controller is aborted.
    165    let log = lazy.Log.repository.getLogger("Sync.Service");
    166 
    167    // If the master-password is locked then we will fail to fully sanitize,
    168    // so prompt for that now. If canceled, we just abort now.
    169    log.info("checking master-password state");
    170    if (!lazy.Utils.ensureMPUnlocked()) {
    171      log.warn(
    172        "The master-password needs to be unlocked to fully disconnect from sync"
    173      );
    174      return;
    175    }
    176 
    177    log.info("waiting for any existing syncs to complete");
    178    let locked = await this.promiseNotSyncing(abortController);
    179 
    180    if (sanitizeData) {
    181      await this.doSanitizeSyncData();
    182 
    183      // We disconnect before sanitizing the browser data - in a worst-case
    184      // scenario where the sanitize takes so long that even the shutdown
    185      // blocker doesn't allow it to finish, we should still at least be in
    186      // a disconnected state on the next startup.
    187      log.info("disconnecting account");
    188      await this.doSyncAndAccountDisconnect(locked);
    189 
    190      await this.doSanitizeBrowserData();
    191    } else {
    192      log.info("disconnecting account");
    193      await this.doSyncAndAccountDisconnect(locked);
    194    }
    195  },
    196 
    197  async disconnect(sanitizeData) {
    198    if (this.promiseDisconnectFinished) {
    199      throw new Error("A disconnect is already in progress");
    200    }
    201    let abortController = new AbortController();
    202    let promiseDisconnectFinished = this._startDisconnect(
    203      abortController,
    204      sanitizeData
    205    );
    206    this.promiseDisconnectFinished = promiseDisconnectFinished;
    207    let shutdownBlocker = () => {
    208      // oh dear - we are sanitizing (probably stuck waiting for a sync to
    209      // complete) and the browser is shutting down. Let's avoid the wait
    210      // for sync to complete and continue the process anyway.
    211      abortController.abort();
    212      return promiseDisconnectFinished;
    213    };
    214    lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
    215      "SyncDisconnect: removing requested data",
    216      shutdownBlocker
    217    );
    218 
    219    // wait for it to finish - hopefully without the blocker being called.
    220    await promiseDisconnectFinished;
    221    this.promiseDisconnectFinished = null;
    222 
    223    // sanitize worked so remove our blocker - it's a noop if the blocker
    224    // did call us.
    225    lazy.AsyncShutdown.appShutdownConfirmed.removeBlocker(shutdownBlocker);
    226  },
    227 };
    228 
    229 export const SyncDisconnect = {
    230  get promiseDisconnectFinished() {
    231    return SyncDisconnectInternal.promiseDisconnectFinished;
    232  },
    233 
    234  disconnect(sanitizeData) {
    235    return SyncDisconnectInternal.disconnect(sanitizeData);
    236  },
    237 };