tor-browser

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

service.sys.mjs (54517B)


      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 const CRYPTO_COLLECTION = "crypto";
      6 const KEYS_WBO = "keys";
      7 
      8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      9 import { Log } from "resource://gre/modules/Log.sys.mjs";
     10 
     11 import { Async } from "resource://services-common/async.sys.mjs";
     12 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
     13 
     14 import {
     15  CLIENT_NOT_CONFIGURED,
     16  CREDENTIALS_CHANGED,
     17  HMAC_EVENT_INTERVAL,
     18  LOGIN_FAILED,
     19  LOGIN_FAILED_INVALID_PASSPHRASE,
     20  LOGIN_FAILED_NETWORK_ERROR,
     21  LOGIN_FAILED_NO_PASSPHRASE,
     22  LOGIN_FAILED_NO_USERNAME,
     23  LOGIN_FAILED_SERVER_ERROR,
     24  LOGIN_SUCCEEDED,
     25  MASTER_PASSWORD_LOCKED,
     26  METARECORD_DOWNLOAD_FAIL,
     27  NO_SYNC_NODE_FOUND,
     28  PREFS_BRANCH,
     29  STATUS_DISABLED,
     30  STATUS_OK,
     31  STORAGE_VERSION,
     32  VERSION_OUT_OF_DATE,
     33  WEAVE_VERSION,
     34  kFirefoxShuttingDown,
     35  kFirstSyncChoiceNotMade,
     36  kSyncBackoffNotMet,
     37  kSyncMasterPasswordLocked,
     38  kSyncNetworkOffline,
     39  kSyncNotConfigured,
     40  kSyncWeaveDisabled,
     41 } from "resource://services-sync/constants.sys.mjs";
     42 
     43 import { EngineManager } from "resource://services-sync/engines.sys.mjs";
     44 import { ClientEngine } from "resource://services-sync/engines/clients.sys.mjs";
     45 import { Weave } from "resource://services-sync/main.sys.mjs";
     46 import {
     47  ErrorHandler,
     48  SyncScheduler,
     49 } from "resource://services-sync/policies.sys.mjs";
     50 import {
     51  CollectionKeyManager,
     52  CryptoWrapper,
     53  RecordManager,
     54  WBORecord,
     55 } from "resource://services-sync/record.sys.mjs";
     56 import { Resource } from "resource://services-sync/resource.sys.mjs";
     57 import { EngineSynchronizer } from "resource://services-sync/stages/enginesync.sys.mjs";
     58 import { DeclinedEngines } from "resource://services-sync/stages/declined.sys.mjs";
     59 import { Status } from "resource://services-sync/status.sys.mjs";
     60 
     61 ChromeUtils.importESModule("resource://services-sync/telemetry.sys.mjs");
     62 import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
     63 
     64 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
     65 import { SCOPE_APP_SYNC } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     66 
     67 const fxAccounts = getFxAccountsSingleton();
     68 
     69 function getEngineModules() {
     70  let result = {
     71    Addons: { module: "addons.sys.mjs", symbol: "AddonsEngine" },
     72    Password: { module: "passwords.sys.mjs", symbol: "PasswordEngine" },
     73    Prefs: { module: "prefs.sys.mjs", symbol: "PrefsEngine" },
     74  };
     75  if (AppConstants.MOZ_APP_NAME != "thunderbird") {
     76    result.Bookmarks = {
     77      module: "bookmarks.sys.mjs",
     78      symbol: "BookmarksEngine",
     79    };
     80    result.Form = { module: "forms.sys.mjs", symbol: "FormEngine" };
     81    result.History = { module: "history.sys.mjs", symbol: "HistoryEngine" };
     82    result.Tab = { module: "tabs.sys.mjs", symbol: "TabEngine" };
     83  }
     84  if (Svc.PrefBranch.getBoolPref("engine.addresses.available", false)) {
     85    result.Addresses = {
     86      module: "resource://autofill/FormAutofillSync.sys.mjs",
     87      symbol: "AddressesEngine",
     88    };
     89  }
     90  if (Svc.PrefBranch.getBoolPref("engine.creditcards.available", false)) {
     91    result.CreditCards = {
     92      module: "resource://autofill/FormAutofillSync.sys.mjs",
     93      symbol: "CreditCardsEngine",
     94    };
     95  }
     96  result["Extension-Storage"] = {
     97    module: "extension-storage.sys.mjs",
     98    controllingPref: "webextensions.storage.sync.kinto",
     99    whenTrue: "ExtensionStorageEngineKinto",
    100    whenFalse: "ExtensionStorageEngineBridge",
    101  };
    102  return result;
    103 }
    104 
    105 const lazy = {};
    106 
    107 // A unique identifier for this browser session. Used for logging so
    108 // we can easily see whether 2 logs are in the same browser session or
    109 // after the browser restarted.
    110 ChromeUtils.defineLazyGetter(lazy, "browserSessionID", Utils.makeGUID);
    111 
    112 function Sync11Service() {
    113  this._notify = Utils.notify("weave:service:");
    114  Utils.defineLazyIDProperty(this, "syncID", "services.sync.client.syncID");
    115 }
    116 Sync11Service.prototype = {
    117  _lock: Utils.lock,
    118  _locked: false,
    119  _loggedIn: false,
    120  // There are some scenarios where we want to kick off another sync immediately
    121  // after the current sync
    122  _queuedSyncReason: null,
    123 
    124  infoURL: null,
    125  storageURL: null,
    126  metaURL: null,
    127  cryptoKeyURL: null,
    128  // The cluster URL comes via the identity object, which in the FxA
    129  // world is ebbedded in the token returned from the token server.
    130  _clusterURL: null,
    131 
    132  get clusterURL() {
    133    return this._clusterURL || "";
    134  },
    135  set clusterURL(value) {
    136    if (value != null && typeof value != "string") {
    137      throw new Error("cluster must be a string, got " + typeof value);
    138    }
    139    this._clusterURL = value;
    140    this._updateCachedURLs();
    141  },
    142 
    143  get isLoggedIn() {
    144    return this._loggedIn;
    145  },
    146 
    147  get locked() {
    148    return this._locked;
    149  },
    150  lock: function lock() {
    151    if (this._locked) {
    152      return false;
    153    }
    154    this._locked = true;
    155    return true;
    156  },
    157  unlock: function unlock() {
    158    this._locked = false;
    159  },
    160 
    161  // A specialized variant of Utils.catch.
    162  // This provides a more informative error message when we're already syncing:
    163  // see Bug 616568.
    164  _catch(func) {
    165    function lockExceptions(ex) {
    166      if (Utils.isLockException(ex)) {
    167        // This only happens if we're syncing already.
    168        this._log.info("Cannot start sync: already syncing?");
    169      }
    170    }
    171 
    172    return Utils.catch.call(this, func, lockExceptions);
    173  },
    174 
    175  get userBaseURL() {
    176    // The user URL is the cluster URL.
    177    return this.clusterURL;
    178  },
    179 
    180  _updateCachedURLs: function _updateCachedURLs() {
    181    // Nothing to cache yet if we don't have the building blocks
    182    if (!this.clusterURL) {
    183      // Also reset all other URLs used by Sync to ensure we aren't accidentally
    184      // using one cached earlier - if there's no cluster URL any cached ones
    185      // are invalid.
    186      this.infoURL = undefined;
    187      this.storageURL = undefined;
    188      this.metaURL = undefined;
    189      this.cryptoKeysURL = undefined;
    190      return;
    191    }
    192 
    193    this._log.debug(
    194      "Caching URLs under storage user base: " + this.userBaseURL
    195    );
    196 
    197    // Generate and cache various URLs under the storage API for this user
    198    this.infoURL = this.userBaseURL + "info/collections";
    199    this.storageURL = this.userBaseURL + "storage/";
    200    this.metaURL = this.storageURL + "meta/global";
    201    this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO;
    202  },
    203 
    204  _checkCrypto: function _checkCrypto() {
    205    let ok = false;
    206 
    207    try {
    208      let iv = Weave.Crypto.generateRandomIV();
    209      if (iv.length == 24) {
    210        ok = true;
    211      }
    212    } catch (e) {
    213      this._log.debug("Crypto check failed: " + e);
    214    }
    215 
    216    return ok;
    217  },
    218 
    219  /**
    220   * Here is a disgusting yet reasonable way of handling HMAC errors deep in
    221   * the guts of Sync. The astute reader will note that this is a hacky way of
    222   * implementing something like continuable conditions.
    223   *
    224   * A handler function is glued to each engine. If the engine discovers an
    225   * HMAC failure, we fetch keys from the server and update our keys, just as
    226   * we would on startup.
    227   *
    228   * If our key collection changed, we signal to the engine (via our return
    229   * value) that it should retry decryption.
    230   *
    231   * If our key collection did not change, it means that we already had the
    232   * correct keys... and thus a different client has the wrong ones. Reupload
    233   * the bundle that we fetched, which will bump the modified time on the
    234   * server and (we hope) prompt a broken client to fix itself.
    235   *
    236   * We keep track of the time at which we last applied this reasoning, because
    237   * thrashing doesn't solve anything. We keep a reasonable interval between
    238   * these remedial actions.
    239   */
    240  lastHMACEvent: 0,
    241 
    242  /*
    243   * Returns whether to try again.
    244   */
    245  async handleHMACEvent() {
    246    let now = Date.now();
    247 
    248    // Leave a sizable delay between HMAC recovery attempts. This gives us
    249    // time for another client to fix themselves if we touch the record.
    250    if (now - this.lastHMACEvent < HMAC_EVENT_INTERVAL) {
    251      return false;
    252    }
    253 
    254    this._log.info(
    255      "Bad HMAC event detected. Attempting recovery " +
    256        "or signaling to other clients."
    257    );
    258 
    259    // Set the last handled time so that we don't act again.
    260    this.lastHMACEvent = now;
    261 
    262    // Fetch keys.
    263    let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
    264    try {
    265      let cryptoResp = (
    266        await cryptoKeys.fetch(this.resource(this.cryptoKeysURL))
    267      ).response;
    268 
    269      // Save out the ciphertext for when we reupload. If there's a bug in
    270      // CollectionKeyManager, this will prevent us from uploading junk.
    271      let cipherText = cryptoKeys.ciphertext;
    272 
    273      if (!cryptoResp.success) {
    274        this._log.warn("Failed to download keys.");
    275        return false;
    276      }
    277 
    278      let keysChanged = await this.handleFetchedKeys(
    279        this.identity.syncKeyBundle,
    280        cryptoKeys,
    281        true
    282      );
    283      if (keysChanged) {
    284        // Did they change? If so, carry on.
    285        this._log.info("Suggesting retry.");
    286        return true; // Try again.
    287      }
    288 
    289      // If not, reupload them and continue the current sync.
    290      cryptoKeys.ciphertext = cipherText;
    291      cryptoKeys.cleartext = null;
    292 
    293      let uploadResp = await this._uploadCryptoKeys(
    294        cryptoKeys,
    295        cryptoResp.obj.modified
    296      );
    297      if (uploadResp.success) {
    298        this._log.info("Successfully re-uploaded keys. Continuing sync.");
    299      } else {
    300        this._log.warn(
    301          "Got error response re-uploading keys. " +
    302            "Continuing sync; let's try again later."
    303        );
    304      }
    305 
    306      return false; // Don't try again: same keys.
    307    } catch (ex) {
    308      this._log.warn(
    309        "Got exception fetching and handling crypto keys. " +
    310          "Will try again later.",
    311        ex
    312      );
    313      return false;
    314    }
    315  },
    316 
    317  async handleFetchedKeys(syncKey, cryptoKeys, skipReset) {
    318    // Don't want to wipe if we're just starting up!
    319    let wasBlank = this.collectionKeys.isClear;
    320    let keysChanged = await this.collectionKeys.updateContents(
    321      syncKey,
    322      cryptoKeys
    323    );
    324 
    325    if (keysChanged && !wasBlank) {
    326      this._log.debug("Keys changed: " + JSON.stringify(keysChanged));
    327 
    328      if (!skipReset) {
    329        this._log.info("Resetting client to reflect key change.");
    330 
    331        if (keysChanged.length) {
    332          // Collection keys only. Reset individual engines.
    333          await this.resetClient(keysChanged);
    334        } else {
    335          // Default key changed: wipe it all.
    336          await this.resetClient();
    337        }
    338 
    339        this._log.info("Downloaded new keys, client reset. Proceeding.");
    340      }
    341      return true;
    342    }
    343    return false;
    344  },
    345 
    346  /**
    347   * Prepare to initialize the rest of Weave after waiting a little bit
    348   */
    349  async onStartup() {
    350    this.status = Status;
    351    this.identity = Status._authManager;
    352    this.collectionKeys = new CollectionKeyManager();
    353 
    354    this.scheduler = new SyncScheduler(this);
    355    this.errorHandler = new ErrorHandler(this);
    356 
    357    this._log = Log.repository.getLogger("Sync.Service");
    358    this._log.manageLevelFromPref("services.sync.log.logger.service.main");
    359 
    360    this._log.info("Loading Weave " + WEAVE_VERSION);
    361 
    362    this.recordManager = new RecordManager(this);
    363 
    364    this.enabled = true;
    365 
    366    await this._registerEngines();
    367 
    368    let ua = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
    369      Ci.nsIHttpProtocolHandler
    370    ).userAgent;
    371    this._log.info(ua);
    372 
    373    if (!this._checkCrypto()) {
    374      this.enabled = false;
    375      this._log.info(
    376        "Could not load the Weave crypto component. Disabling " +
    377          "Weave, since it will not work correctly."
    378      );
    379    }
    380 
    381    Svc.Obs.add("weave:service:setup-complete", this);
    382    Svc.Obs.add("weave:service:sync:finish", this);
    383    Svc.Obs.add("sync:collection_changed", this); // Pulled from FxAccountsCommon
    384    Svc.Obs.add("fxaccounts:device_disconnected", this);
    385    Services.prefs.addObserver(PREFS_BRANCH + "engine.", this);
    386 
    387    if (!this.enabled) {
    388      this._log.info("Firefox Sync disabled.");
    389    }
    390 
    391    this._updateCachedURLs();
    392 
    393    let status = this._checkSetup();
    394    if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
    395      this._startTracking();
    396    }
    397 
    398    // Send an event now that Weave service is ready.  We don't do this
    399    // synchronously so that observers can import this module before
    400    // registering an observer.
    401    CommonUtils.nextTick(() => {
    402      this.status.ready = true;
    403 
    404      // UI code uses the flag on the XPCOM service so it doesn't have
    405      // to load a bunch of modules.
    406      let xps = Cc["@mozilla.org/weave/service;1"].getService(
    407        Ci.nsISupports
    408      ).wrappedJSObject;
    409      xps.ready = true;
    410 
    411      Svc.Obs.notify("weave:service:ready");
    412    });
    413  },
    414 
    415  _checkSetup: function _checkSetup() {
    416    if (!this.enabled) {
    417      return (this.status.service = STATUS_DISABLED);
    418    }
    419    return this.status.checkSetup();
    420  },
    421 
    422  /**
    423   * Register the built-in engines for certain applications
    424   */
    425  async _registerEngines() {
    426    this.engineManager = new EngineManager(this);
    427 
    428    let engineModules = getEngineModules();
    429 
    430    let engines = [];
    431    // We allow a pref, which has no default value, to limit the engines
    432    // which are registered. We expect only tests will use this.
    433    if (
    434      Svc.PrefBranch.getPrefType("registerEngines") !=
    435      Ci.nsIPrefBranch.PREF_INVALID
    436    ) {
    437      engines = Svc.PrefBranch.getStringPref("registerEngines").split(",");
    438      this._log.info("Registering custom set of engines", engines);
    439    } else {
    440      // default is all engines.
    441      engines = Object.keys(engineModules);
    442    }
    443 
    444    let declined = [];
    445    let pref = Svc.PrefBranch.getStringPref("declinedEngines", null);
    446    if (pref) {
    447      declined = pref.split(",");
    448    }
    449 
    450    let clientsEngine = new ClientEngine(this);
    451    // Ideally clientsEngine should not exist
    452    // (or be a promise that calls initialize() before returning the engine)
    453    await clientsEngine.initialize();
    454    this.clientsEngine = clientsEngine;
    455 
    456    for (let name of engines) {
    457      if (!(name in engineModules)) {
    458        this._log.info("Do not know about engine: " + name);
    459        continue;
    460      }
    461      let modInfo = engineModules[name];
    462      if (!modInfo.module.includes(":")) {
    463        modInfo.module = "resource://services-sync/engines/" + modInfo.module;
    464      }
    465      try {
    466        let ns = ChromeUtils.importESModule(modInfo.module);
    467        if (modInfo.symbol) {
    468          let symbol = modInfo.symbol;
    469          if (!(symbol in ns)) {
    470            this._log.warn(
    471              "Could not find exported engine instance: " + symbol
    472            );
    473            continue;
    474          }
    475          await this.engineManager.register(ns[symbol]);
    476        } else {
    477          let { whenTrue, whenFalse, controllingPref } = modInfo;
    478          if (!(whenTrue in ns) || !(whenFalse in ns)) {
    479            this._log.warn("Could not find all exported engine instances", {
    480              whenTrue,
    481              whenFalse,
    482            });
    483            continue;
    484          }
    485          await this.engineManager.registerAlternatives(
    486            name.toLowerCase(),
    487            controllingPref,
    488            ns[whenTrue],
    489            ns[whenFalse]
    490          );
    491        }
    492      } catch (ex) {
    493        this._log.warn("Could not register engine " + name, ex);
    494      }
    495    }
    496 
    497    this.engineManager.setDeclined(declined);
    498  },
    499 
    500  /**
    501   * This method updates the local engines state from an existing meta/global
    502   * when Sync is disabled.
    503   * Running this code if sync is enabled would end up in very weird results
    504   * (but we're nice and we check before doing anything!).
    505   */
    506  async updateLocalEnginesState() {
    507    await this.promiseInitialized;
    508 
    509    // Sanity check, this method is not meant to be run if Sync is enabled!
    510    if (Svc.PrefBranch.getStringPref("username", "")) {
    511      throw new Error("Sync is enabled!");
    512    }
    513 
    514    // For historical reasons the behaviour of setCluster() is bizarre,
    515    // so just check what we care about - the meta URL.
    516    if (!this.metaURL) {
    517      await this.identity.setCluster();
    518      if (!this.metaURL) {
    519        this._log.warn("Could not find a cluster.");
    520        return;
    521      }
    522    }
    523    // Clear the cache so we always fetch the latest meta/global.
    524    this.recordManager.clearCache();
    525    let meta = await this.recordManager.get(this.metaURL);
    526    if (!meta) {
    527      this._log.info("Meta record is null, aborting engine state update.");
    528      return;
    529    }
    530    const declinedEngines = meta.payload.declined;
    531    const allEngines = this.engineManager.getAll().map(e => e.name);
    532    // We don't want our observer of the enabled prefs to treat the change as
    533    // a user-change, otherwise we will do the wrong thing with declined etc.
    534    this._ignorePrefObserver = true;
    535    try {
    536      for (const engine of allEngines) {
    537        Svc.PrefBranch.setBoolPref(
    538          `engine.${engine}`,
    539          !declinedEngines.includes(engine)
    540        );
    541      }
    542    } finally {
    543      this._ignorePrefObserver = false;
    544    }
    545  },
    546 
    547  QueryInterface: ChromeUtils.generateQI([
    548    "nsIObserver",
    549    "nsISupportsWeakReference",
    550  ]),
    551 
    552  observe(subject, topic, data) {
    553    switch (topic) {
    554      // Ideally this observer should be in the SyncScheduler, but it would require
    555      // some work to know about the sync specific engines. We should move this there once it does.
    556      case "sync:collection_changed":
    557        // We check if we're running TPS here to avoid TPS failing because it
    558        // couldn't get to get the sync lock, due to us currently syncing the
    559        // clients engine.
    560        if (
    561          data.includes("clients") &&
    562          !Svc.PrefBranch.getBoolPref("testing.tps", false)
    563        ) {
    564          // Sync in the background (it's fine not to wait on the returned promise
    565          // because sync() has a lock).
    566          // [] = clients collection only
    567          this.sync({ why: "collection_changed", engines: [] }).catch(e => {
    568            this._log.error(e);
    569          });
    570        }
    571        break;
    572      case "fxaccounts:device_disconnected":
    573        data = JSON.parse(data);
    574        if (!data.isLocalDevice) {
    575          // Refresh the known stale clients list in the background.
    576          this.clientsEngine.updateKnownStaleClients().catch(e => {
    577            this._log.error(e);
    578          });
    579        }
    580        break;
    581      case "weave:service:setup-complete": {
    582        let status = this._checkSetup();
    583        if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
    584          this._startTracking();
    585        }
    586        break;
    587      }
    588      case "nsPref:changed": {
    589        if (this._ignorePrefObserver) {
    590          return;
    591        }
    592        const engine = data.slice((PREFS_BRANCH + "engine.").length);
    593        if (engine.includes(".")) {
    594          // A sub-preference of the engine was changed. For example
    595          // `services.sync.engine.bookmarks.validation.percentageChance`.
    596          return;
    597        }
    598        this._handleEngineStatusChanged(engine);
    599        break;
    600      }
    601      case "weave:service:sync:finish":
    602        if (this._queuedSyncReason) {
    603          this.sync({ why: this._queuedSyncReason });
    604          this._queuedSyncReason = null;
    605        }
    606        break;
    607    }
    608  },
    609 
    610  _handleEngineStatusChanged(engine) {
    611    this._log.trace("Status for " + engine + " engine changed.");
    612    if (Svc.PrefBranch.getBoolPref("engineStatusChanged." + engine, false)) {
    613      // The enabled status being changed back to what it was before.
    614      Svc.PrefBranch.clearUserPref("engineStatusChanged." + engine);
    615    } else {
    616      // Remember that the engine status changed locally until the next sync.
    617      Svc.PrefBranch.setBoolPref("engineStatusChanged." + engine, true);
    618    }
    619  },
    620 
    621  _startTracking() {
    622    const engines = [this.clientsEngine, ...this.engineManager.getAll()];
    623    for (let engine of engines) {
    624      try {
    625        engine.startTracking();
    626      } catch (e) {
    627        this._log.error(`Could not start ${engine.name} engine tracker`, e);
    628      }
    629    }
    630    // This is for TPS. We should try to do better.
    631    Svc.Obs.notify("weave:service:tracking-started");
    632  },
    633 
    634  async _stopTracking() {
    635    const engines = [this.clientsEngine, ...this.engineManager.getAll()];
    636    for (let engine of engines) {
    637      try {
    638        await engine.stopTracking();
    639      } catch (e) {
    640        this._log.error(`Could not stop ${engine.name} engine tracker`, e);
    641      }
    642    }
    643    Svc.Obs.notify("weave:service:tracking-stopped");
    644  },
    645 
    646  /**
    647   * Obtain a Resource instance with authentication credentials.
    648   */
    649  resource: function resource(url) {
    650    let res = new Resource(url);
    651    res.authenticator = this.identity.getResourceAuthenticator();
    652 
    653    return res;
    654  },
    655 
    656  /**
    657   * Perform the info fetch as part of a login or key fetch, or
    658   * inside engine sync.
    659   */
    660  async _fetchInfo(url) {
    661    let infoURL = url || this.infoURL;
    662 
    663    this._log.trace("In _fetchInfo: " + infoURL);
    664    let info;
    665    try {
    666      info = await this.resource(infoURL).get();
    667    } catch (ex) {
    668      this.errorHandler.checkServerError(ex);
    669      throw ex;
    670    }
    671 
    672    // Always check for errors.
    673    this.errorHandler.checkServerError(info);
    674    if (!info.success) {
    675      this._log.error("Aborting sync: failed to get collections.");
    676      throw info;
    677    }
    678    return info;
    679  },
    680 
    681  async verifyAndFetchSymmetricKeys(infoResponse) {
    682    this._log.debug(
    683      "Fetching and verifying -- or generating -- symmetric keys."
    684    );
    685 
    686    let syncKeyBundle = this.identity.syncKeyBundle;
    687    if (!syncKeyBundle) {
    688      this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
    689      this.status.sync = CREDENTIALS_CHANGED;
    690      return false;
    691    }
    692 
    693    try {
    694      if (!infoResponse) {
    695        infoResponse = await this._fetchInfo(); // Will throw an exception on failure.
    696      }
    697 
    698      // This only applies when the server is already at version 4.
    699      if (infoResponse.status != 200) {
    700        this._log.warn(
    701          "info/collections returned non-200 response. Failing key fetch."
    702        );
    703        this.status.login = LOGIN_FAILED_SERVER_ERROR;
    704        this.errorHandler.checkServerError(infoResponse);
    705        return false;
    706      }
    707 
    708      let infoCollections = infoResponse.obj;
    709 
    710      this._log.info(
    711        "Testing info/collections: " + JSON.stringify(infoCollections)
    712      );
    713 
    714      if (this.collectionKeys.updateNeeded(infoCollections)) {
    715        this._log.info("collection keys reports that a key update is needed.");
    716 
    717        // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this.
    718 
    719        // Fetch storage/crypto/keys.
    720        let cryptoKeys;
    721 
    722        if (infoCollections && CRYPTO_COLLECTION in infoCollections) {
    723          try {
    724            cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
    725            let cryptoResp = (
    726              await cryptoKeys.fetch(this.resource(this.cryptoKeysURL))
    727            ).response;
    728 
    729            if (cryptoResp.success) {
    730              await this.handleFetchedKeys(syncKeyBundle, cryptoKeys);
    731              return true;
    732            } else if (cryptoResp.status == 404) {
    733              // On failure, ask to generate new keys and upload them.
    734              // Fall through to the behavior below.
    735              this._log.warn(
    736                "Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating."
    737              );
    738              cryptoKeys = null;
    739            } else {
    740              // Some other problem.
    741              this.status.login = LOGIN_FAILED_SERVER_ERROR;
    742              this.errorHandler.checkServerError(cryptoResp);
    743              this._log.warn(
    744                "Got status " + cryptoResp.status + " fetching crypto keys."
    745              );
    746              return false;
    747            }
    748          } catch (ex) {
    749            this._log.warn("Got exception fetching cryptoKeys.", ex);
    750            // TODO: Um, what exceptions might we get here? Should we re-throw any?
    751 
    752            // One kind of exception: HMAC failure.
    753            if (Utils.isHMACMismatch(ex)) {
    754              this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
    755              this.status.sync = CREDENTIALS_CHANGED;
    756            } else {
    757              // In the absence of further disambiguation or more precise
    758              // failure constants, just report failure.
    759              this.status.login = LOGIN_FAILED;
    760            }
    761            return false;
    762          }
    763        } else {
    764          this._log.info(
    765            "... 'crypto' is not a reported collection. Generating new keys."
    766          );
    767        }
    768 
    769        if (!cryptoKeys) {
    770          this._log.info("No keys! Generating new ones.");
    771 
    772          // Better make some and upload them, and wipe the server to ensure
    773          // consistency. This is all achieved via _freshStart.
    774          // If _freshStart fails to clear the server or upload keys, it will
    775          // throw.
    776          await this._freshStart();
    777          return true;
    778        }
    779 
    780        // Last-ditch case.
    781        return false;
    782      }
    783      // No update needed: we're good!
    784      return true;
    785    } catch (ex) {
    786      // This means no keys are present, or there's a network error.
    787      this._log.debug("Failed to fetch and verify keys", ex);
    788      this.errorHandler.checkServerError(ex);
    789      return false;
    790    }
    791  },
    792 
    793  getMaxRecordPayloadSize() {
    794    let config = this.serverConfiguration;
    795    if (!config || !config.max_record_payload_bytes) {
    796      this._log.warn(
    797        "No config or incomplete config in getMaxRecordPayloadSize." +
    798          " Are we running tests?"
    799      );
    800      // should stay in sync with MAX_PAYLOAD_SIZE in the Rust tabs engine.
    801      return 2 * 1024 * 1024;
    802    }
    803    let payloadMax = config.max_record_payload_bytes;
    804    if (config.max_post_bytes && payloadMax <= config.max_post_bytes) {
    805      return config.max_post_bytes - 4096;
    806    }
    807    return payloadMax;
    808  },
    809 
    810  getMemcacheMaxRecordPayloadSize() {
    811    // Collections stored in memcached ("tabs", "clients" or "meta") have a
    812    // different max size than ones stored in the normal storage server db.
    813    // In practice, the real limit here is 1M (bug 1300451 comment 40), but
    814    // there's overhead involved that is hard to calculate on the client, so we
    815    // use 512k to be safe (at the recommendation of the server team). Note
    816    // that if the server reports a lower limit (via info/configuration), we
    817    // respect that limit instead. See also bug 1403052.
    818    return Math.min(512 * 1024, this.getMaxRecordPayloadSize());
    819  },
    820 
    821  async verifyLogin(allow40XRecovery = true) {
    822    // Attaching auth credentials to a request requires access to
    823    // passwords, which means that Resource.get can throw MP-related
    824    // exceptions!
    825    // So we ask the identity to verify the login state after unlocking the
    826    // master password (ie, this call is expected to prompt for MP unlock
    827    // if necessary) while we still have control.
    828    this.status.login = await this.identity.unlockAndVerifyAuthState();
    829    this._log.debug(
    830      "Fetching unlocked auth state returned " + this.status.login
    831    );
    832    if (this.status.login != STATUS_OK) {
    833      return false;
    834    }
    835 
    836    try {
    837      // Make sure we have a cluster to verify against.
    838      // This is a little weird, if we don't get a node we pretend
    839      // to succeed, since that probably means we just don't have storage.
    840      if (this.clusterURL == "" && !(await this.identity.setCluster())) {
    841        this.status.sync = NO_SYNC_NODE_FOUND;
    842        return true;
    843      }
    844 
    845      // Fetch collection info on every startup.
    846      let test = await this.resource(this.infoURL).get();
    847 
    848      switch (test.status) {
    849        case 200:
    850          // The user is authenticated.
    851 
    852          // We have no way of verifying the passphrase right now,
    853          // so wait until remoteSetup to do so.
    854          // Just make the most trivial checks.
    855          if (!this.identity.syncKeyBundle) {
    856            this._log.warn("No passphrase in verifyLogin.");
    857            this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
    858            return false;
    859          }
    860 
    861          // Go ahead and do remote setup, so that we can determine
    862          // conclusively that our passphrase is correct.
    863          if (await this._remoteSetup(test)) {
    864            // Username/password verified.
    865            this.status.login = LOGIN_SUCCEEDED;
    866            return true;
    867          }
    868 
    869          this._log.warn("Remote setup failed.");
    870          // Remote setup must have failed.
    871          return false;
    872 
    873        case 401:
    874          this._log.warn("401: login failed.");
    875        // Fall through to the 404 case.
    876 
    877        case 404:
    878          // Check that we're verifying with the correct cluster
    879          if (allow40XRecovery && (await this.identity.setCluster())) {
    880            return await this.verifyLogin(false);
    881          }
    882 
    883          // We must have the right cluster, but the server doesn't expect us.
    884          // For FxA this almost certainly means "transient error fetching token".
    885          this.status.login = LOGIN_FAILED_NETWORK_ERROR;
    886          return false;
    887 
    888        default:
    889          // Server didn't respond with something that we expected
    890          this.status.login = LOGIN_FAILED_SERVER_ERROR;
    891          this.errorHandler.checkServerError(test);
    892          return false;
    893      }
    894    } catch (ex) {
    895      // Must have failed on some network issue
    896      this._log.debug("verifyLogin failed", ex);
    897      this.status.login = LOGIN_FAILED_NETWORK_ERROR;
    898      this.errorHandler.checkServerError(ex);
    899      return false;
    900    }
    901  },
    902 
    903  async generateNewSymmetricKeys() {
    904    this._log.info("Generating new keys WBO...");
    905    let wbo = await this.collectionKeys.generateNewKeysWBO();
    906    this._log.info("Encrypting new key bundle.");
    907    await wbo.encrypt(this.identity.syncKeyBundle);
    908 
    909    let uploadRes = await this._uploadCryptoKeys(wbo, 0);
    910    if (uploadRes.status != 200) {
    911      this._log.warn(
    912        "Got status " +
    913          uploadRes.status +
    914          " uploading new keys. What to do? Throw!"
    915      );
    916      this.errorHandler.checkServerError(uploadRes);
    917      throw new Error("Unable to upload symmetric keys.");
    918    }
    919    this._log.info("Got status " + uploadRes.status + " uploading keys.");
    920    let serverModified = uploadRes.obj; // Modified timestamp according to server.
    921    this._log.debug("Server reports crypto modified: " + serverModified);
    922 
    923    // Now verify that info/collections shows them!
    924    this._log.debug("Verifying server collection records.");
    925    let info = await this._fetchInfo();
    926    this._log.debug("info/collections is: " + info.data);
    927 
    928    if (info.status != 200) {
    929      this._log.warn("Non-200 info/collections response. Aborting.");
    930      throw new Error("Unable to upload symmetric keys.");
    931    }
    932 
    933    info = info.obj;
    934    if (!(CRYPTO_COLLECTION in info)) {
    935      this._log.error(
    936        "Consistency failure: info/collections excludes " +
    937          "crypto after successful upload."
    938      );
    939      throw new Error("Symmetric key upload failed.");
    940    }
    941 
    942    // Can't check against local modified: clock drift.
    943    if (info[CRYPTO_COLLECTION] < serverModified) {
    944      this._log.error(
    945        "Consistency failure: info/collections crypto entry " +
    946          "is stale after successful upload."
    947      );
    948      throw new Error("Symmetric key upload failed.");
    949    }
    950 
    951    // Doesn't matter if the timestamp is ahead.
    952 
    953    // Download and install them.
    954    let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
    955    let cryptoResp = (await cryptoKeys.fetch(this.resource(this.cryptoKeysURL)))
    956      .response;
    957    if (cryptoResp.status != 200) {
    958      this._log.warn("Failed to download keys.");
    959      throw new Error("Symmetric key download failed.");
    960    }
    961    let keysChanged = await this.handleFetchedKeys(
    962      this.identity.syncKeyBundle,
    963      cryptoKeys,
    964      true
    965    );
    966    if (keysChanged) {
    967      this._log.info("Downloaded keys differed, as expected.");
    968    }
    969  },
    970 
    971  // configures/enabled/turns-on sync. There must be an FxA user signed in.
    972  async configure() {
    973    // We don't, and must not, throw if sync is already configured, because we
    974    // might end up being called as part of a "reconnect" flow. We also want to
    975    // avoid checking the FxA user is the same as the pref because the email
    976    // address for the FxA account can change - we'd need to use the uid.
    977    let user = await fxAccounts.getSignedInUser();
    978    if (!user) {
    979      throw new Error("No FxA user is signed in");
    980    }
    981    // Check if the user has sync keys. With OAuth-based authentication,
    982    // keys cannot be fetched on demand - they must exist locally.
    983    let hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
    984    if (!hasKeys) {
    985      throw new Error("User does not have sync keys");
    986    }
    987    this._log.info("Configuring sync with current FxA user");
    988    Svc.PrefBranch.setStringPref("username", user.email);
    989    Svc.Obs.notify("weave:connected");
    990  },
    991 
    992  // resets/turns-off sync.
    993  async startOver() {
    994    this._log.trace("Invoking Service.startOver.");
    995    await this._stopTracking();
    996    this.status.resetSync();
    997 
    998    // Deletion doesn't make sense if we aren't set up yet!
    999    if (this.clusterURL != "") {
   1000      // Clear client-specific data from the server, including disabled engines.
   1001      const engines = [this.clientsEngine, ...this.engineManager.getAll()];
   1002      for (let engine of engines) {
   1003        try {
   1004          await engine.removeClientData();
   1005        } catch (ex) {
   1006          this._log.warn(`Deleting client data for ${engine.name} failed`, ex);
   1007        }
   1008      }
   1009      this._log.debug("Finished deleting client data.");
   1010    } else {
   1011      this._log.debug("Skipping client data removal: no cluster URL.");
   1012    }
   1013 
   1014    this.identity.resetCredentials();
   1015    this.status.login = LOGIN_FAILED_NO_USERNAME;
   1016    this.logout();
   1017    Svc.Obs.notify("weave:service:start-over");
   1018 
   1019    // Reset all engines and clear keys.
   1020    await this.resetClient();
   1021    this.collectionKeys.clear();
   1022    this.status.resetBackoff();
   1023 
   1024    // Reset Weave prefs.
   1025    this._ignorePrefObserver = true;
   1026    for (const pref of Svc.PrefBranch.getChildList("")) {
   1027      Svc.PrefBranch.clearUserPref(pref);
   1028    }
   1029    this._ignorePrefObserver = false;
   1030    this.clusterURL = null;
   1031 
   1032    Svc.PrefBranch.setStringPref("lastversion", WEAVE_VERSION);
   1033 
   1034    try {
   1035      this.identity.finalize();
   1036      this.status.__authManager = null;
   1037      this.identity = Status._authManager;
   1038      Svc.Obs.notify("weave:service:start-over:finish");
   1039    } catch (err) {
   1040      this._log.error(
   1041        "startOver failed to re-initialize the identity manager",
   1042        err
   1043      );
   1044      // Still send the observer notification so the current state is
   1045      // reflected in the UI.
   1046      Svc.Obs.notify("weave:service:start-over:finish");
   1047    }
   1048  },
   1049 
   1050  async login() {
   1051    async function onNotify() {
   1052      this._loggedIn = false;
   1053      if (this.scheduler.offline) {
   1054        this.status.login = LOGIN_FAILED_NETWORK_ERROR;
   1055        throw new Error("Application is offline, login should not be called");
   1056      }
   1057 
   1058      this._log.info("User logged in successfully - verifying login.");
   1059      if (!(await this.verifyLogin())) {
   1060        // verifyLogin sets the failure states here.
   1061        throw new Error(`Login failed: ${this.status.login}`);
   1062      }
   1063 
   1064      this._updateCachedURLs();
   1065 
   1066      this._loggedIn = true;
   1067 
   1068      return true;
   1069    }
   1070 
   1071    let notifier = this._notify("login", "", onNotify.bind(this));
   1072    return this._catch(this._lock("service.js: login", notifier))();
   1073  },
   1074 
   1075  logout: function logout() {
   1076    // If we failed during login, we aren't going to have this._loggedIn set,
   1077    // but we still want to ask the identity to logout, so it doesn't try and
   1078    // reuse any old credentials next time we sync.
   1079    this._log.info("Logging out");
   1080    this.identity.logout();
   1081    this._loggedIn = false;
   1082 
   1083    Svc.Obs.notify("weave:service:logout:finish");
   1084  },
   1085 
   1086  // Note: returns false if we failed for a reason other than the server not yet
   1087  // supporting the api.
   1088  async _fetchServerConfiguration() {
   1089    // This is similar to _fetchInfo, but with different error handling.
   1090 
   1091    let infoURL = this.userBaseURL + "info/configuration";
   1092    this._log.debug("Fetching server configuration", infoURL);
   1093    let configResponse;
   1094    try {
   1095      configResponse = await this.resource(infoURL).get();
   1096    } catch (ex) {
   1097      // This is probably a network or similar error.
   1098      this._log.warn("Failed to fetch info/configuration", ex);
   1099      this.errorHandler.checkServerError(ex);
   1100      return false;
   1101    }
   1102 
   1103    if (configResponse.status == 404) {
   1104      // This server doesn't support the URL yet - that's OK.
   1105      this._log.debug(
   1106        "info/configuration returned 404 - using default upload semantics"
   1107      );
   1108    } else if (configResponse.status != 200) {
   1109      this._log.warn(
   1110        `info/configuration returned ${configResponse.status} - using default configuration`
   1111      );
   1112      this.errorHandler.checkServerError(configResponse);
   1113      return false;
   1114    } else {
   1115      this.serverConfiguration = configResponse.obj;
   1116    }
   1117    this._log.trace(
   1118      "info/configuration for this server",
   1119      this.serverConfiguration
   1120    );
   1121    return true;
   1122  },
   1123 
   1124  // Stuff we need to do after login, before we can really do
   1125  // anything (e.g. key setup).
   1126  async _remoteSetup(infoResponse, fetchConfig = true) {
   1127    if (fetchConfig && !(await this._fetchServerConfiguration())) {
   1128      return false;
   1129    }
   1130 
   1131    this._log.debug("Fetching global metadata record");
   1132    let meta = await this.recordManager.get(this.metaURL);
   1133 
   1134    // Checking modified time of the meta record.
   1135    if (
   1136      infoResponse &&
   1137      infoResponse.obj.meta != this.metaModified &&
   1138      (!meta || !meta.isNew)
   1139    ) {
   1140      // Delete the cached meta record...
   1141      this._log.debug(
   1142        "Clearing cached meta record. metaModified is " +
   1143          JSON.stringify(this.metaModified) +
   1144          ", setting to " +
   1145          JSON.stringify(infoResponse.obj.meta)
   1146      );
   1147 
   1148      this.recordManager.del(this.metaURL);
   1149 
   1150      // ... fetch the current record from the server, and COPY THE FLAGS.
   1151      let newMeta = await this.recordManager.get(this.metaURL);
   1152 
   1153      // If we got a 401, we do not want to create a new meta/global - we
   1154      // should be able to get the existing meta after we get a new node.
   1155      if (this.recordManager.response.status == 401) {
   1156        this._log.debug(
   1157          "Fetching meta/global record on the server returned 401."
   1158        );
   1159        this.errorHandler.checkServerError(this.recordManager.response);
   1160        return false;
   1161      }
   1162 
   1163      if (this.recordManager.response.status == 404) {
   1164        this._log.debug("No meta/global record on the server. Creating one.");
   1165        try {
   1166          await this._uploadNewMetaGlobal();
   1167        } catch (uploadRes) {
   1168          this._log.warn(
   1169            "Unable to upload new meta/global. Failing remote setup."
   1170          );
   1171          this.errorHandler.checkServerError(uploadRes);
   1172          return false;
   1173        }
   1174      } else if (!newMeta) {
   1175        this._log.warn("Unable to get meta/global. Failing remote setup.");
   1176        this.errorHandler.checkServerError(this.recordManager.response);
   1177        return false;
   1178      } else {
   1179        // If newMeta, then it stands to reason that meta != null.
   1180        newMeta.isNew = meta.isNew;
   1181        newMeta.changed = meta.changed;
   1182      }
   1183 
   1184      // Switch in the new meta object and record the new time.
   1185      meta = newMeta;
   1186      this.metaModified = infoResponse.obj.meta;
   1187    }
   1188 
   1189    let remoteVersion =
   1190      meta && meta.payload.storageVersion ? meta.payload.storageVersion : "";
   1191 
   1192    this._log.debug(
   1193      [
   1194        "Weave Version:",
   1195        WEAVE_VERSION,
   1196        "Local Storage:",
   1197        STORAGE_VERSION,
   1198        "Remote Storage:",
   1199        remoteVersion,
   1200      ].join(" ")
   1201    );
   1202 
   1203    // Check for cases that require a fresh start. When comparing remoteVersion,
   1204    // we need to convert it to a number as older clients used it as a string.
   1205    if (
   1206      !meta ||
   1207      !meta.payload.storageVersion ||
   1208      !meta.payload.syncID ||
   1209      STORAGE_VERSION > parseFloat(remoteVersion)
   1210    ) {
   1211      this._log.info(
   1212        "One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed."
   1213      );
   1214 
   1215      // abort the server wipe if the GET status was anything other than 404 or 200
   1216      let status = this.recordManager.response.status;
   1217      if (status != 200 && status != 404) {
   1218        this.status.sync = METARECORD_DOWNLOAD_FAIL;
   1219        this.errorHandler.checkServerError(this.recordManager.response);
   1220        this._log.warn(
   1221          "Unknown error while downloading metadata record. Aborting sync."
   1222        );
   1223        return false;
   1224      }
   1225 
   1226      if (!meta) {
   1227        this._log.info("No metadata record, server wipe needed");
   1228      }
   1229      if (meta && !meta.payload.syncID) {
   1230        this._log.warn("No sync id, server wipe needed");
   1231      }
   1232 
   1233      this._log.info("Wiping server data");
   1234      await this._freshStart();
   1235 
   1236      if (status == 404) {
   1237        this._log.info(
   1238          "Metadata record not found, server was wiped to ensure " +
   1239            "consistency."
   1240        );
   1241      } else {
   1242        // 200
   1243        this._log.info("Wiped server; incompatible metadata: " + remoteVersion);
   1244      }
   1245      return true;
   1246    } else if (remoteVersion > STORAGE_VERSION) {
   1247      this.status.sync = VERSION_OUT_OF_DATE;
   1248      this._log.warn("Upgrade required to access newer storage version.");
   1249      return false;
   1250    } else if (meta.payload.syncID != this.syncID) {
   1251      this._log.info(
   1252        "Sync IDs differ. Local is " +
   1253          this.syncID +
   1254          ", remote is " +
   1255          meta.payload.syncID
   1256      );
   1257      await this.resetClient();
   1258      this.collectionKeys.clear();
   1259      this.syncID = meta.payload.syncID;
   1260      this._log.debug("Clear cached values and take syncId: " + this.syncID);
   1261 
   1262      if (!(await this.verifyAndFetchSymmetricKeys(infoResponse))) {
   1263        this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
   1264        return false;
   1265      }
   1266 
   1267      // bug 545725 - re-verify creds and fail sanely
   1268      if (!(await this.verifyLogin())) {
   1269        this.status.sync = CREDENTIALS_CHANGED;
   1270        this._log.info(
   1271          "Credentials have changed, aborting sync and forcing re-login."
   1272        );
   1273        return false;
   1274      }
   1275 
   1276      return true;
   1277    }
   1278    if (!(await this.verifyAndFetchSymmetricKeys(infoResponse))) {
   1279      this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
   1280      return false;
   1281    }
   1282 
   1283    return true;
   1284  },
   1285 
   1286  /**
   1287   * Return whether we should attempt login at the start of a sync.
   1288   *
   1289   * Note that this function has strong ties to _checkSync: callers
   1290   * of this function should typically use _checkSync to verify that
   1291   * any necessary login took place.
   1292   */
   1293  _shouldLogin: function _shouldLogin() {
   1294    return (
   1295      this.enabled &&
   1296      !this.scheduler.offline &&
   1297      !this.isLoggedIn &&
   1298      Async.isAppReady()
   1299    );
   1300  },
   1301 
   1302  /**
   1303   * Determine if a sync should run.
   1304   *
   1305   * @param ignore [optional]
   1306   *        array of reasons to ignore when checking
   1307   *
   1308   * @return Reason for not syncing; not-truthy if sync should run
   1309   */
   1310  _checkSync: function _checkSync(ignore) {
   1311    let reason = "";
   1312    // Ideally we'd call _checkSetup() here but that has too many side-effects.
   1313    if (Status.service == CLIENT_NOT_CONFIGURED) {
   1314      reason = kSyncNotConfigured;
   1315    } else if (Status.service == STATUS_DISABLED || !this.enabled) {
   1316      reason = kSyncWeaveDisabled;
   1317    } else if (this.scheduler.offline) {
   1318      reason = kSyncNetworkOffline;
   1319    } else if (this.status.minimumNextSync > Date.now()) {
   1320      reason = kSyncBackoffNotMet;
   1321    } else if (
   1322      this.status.login == MASTER_PASSWORD_LOCKED &&
   1323      Utils.mpLocked()
   1324    ) {
   1325      reason = kSyncMasterPasswordLocked;
   1326    } else if (Svc.PrefBranch.getStringPref("firstSync", null) == "notReady") {
   1327      reason = kFirstSyncChoiceNotMade;
   1328    } else if (!Async.isAppReady()) {
   1329      reason = kFirefoxShuttingDown;
   1330    }
   1331 
   1332    if (ignore && ignore.includes(reason)) {
   1333      return "";
   1334    }
   1335 
   1336    return reason;
   1337  },
   1338 
   1339  /**
   1340   * Perform a full sync (or of the given engines). While a sync is in progress,
   1341   * this call is ignored; to guarantee a follow-up you must call queueSync().
   1342   *
   1343   * @param {object} options
   1344   * @param {Array<string>} [options.engines] — names of engines to sync
   1345   * @param {string} [options.why] — reason for the sync
   1346   * @returns {Promise<void>}
   1347   */
   1348  async sync({ engines, why } = {}) {
   1349    let dateStr = Utils.formatTimestamp(new Date());
   1350    this._log.debug("User-Agent: " + Utils.userAgent);
   1351    await this.promiseInitialized;
   1352    this._log.info(
   1353      `Starting sync at ${dateStr} in browser session ${lazy.browserSessionID}`
   1354    );
   1355    return this._catch(async function () {
   1356      // Make sure we're logged in.
   1357      if (this._shouldLogin()) {
   1358        this._log.debug("In sync: should login.");
   1359        if (!(await this.login())) {
   1360          this._log.debug("Not syncing: login returned false.");
   1361          return;
   1362        }
   1363      } else {
   1364        this._log.trace("In sync: no need to login.");
   1365      }
   1366      await this._lockedSync(engines, why);
   1367    })();
   1368  },
   1369 
   1370  /**
   1371   * Sync up engines with the server.
   1372   */
   1373  async _lockedSync(engineNamesToSync, why) {
   1374    return this._lock(
   1375      "service.js: sync",
   1376      this._notify("sync", JSON.stringify({ why }), async function onNotify() {
   1377        let synchronizer = new EngineSynchronizer(this);
   1378        await synchronizer.sync(engineNamesToSync, why); // Might throw!
   1379 
   1380        // We successfully synchronized.
   1381        // Check if the identity wants to pre-fetch a migration sentinel from
   1382        // the server.
   1383        // If we have no clusterURL, we are probably doing a node reassignment
   1384        // so don't attempt to get it in that case.
   1385        if (this.clusterURL) {
   1386          this.identity.prefetchMigrationSentinel(this);
   1387        }
   1388 
   1389        // Now let's update our declined engines
   1390        await this._maybeUpdateDeclined();
   1391      })
   1392    )();
   1393  },
   1394 
   1395  /**
   1396   * Kick off a sync after the current one finishes, or immediately if idle.
   1397   *
   1398   * @param {string} why — reason for calling the sync
   1399   */
   1400  queueSync(why) {
   1401    if (this._locked) {
   1402      // A sync is already in flight; queue a follow-up.
   1403      this._queuedSyncReason = why;
   1404    } else {
   1405      // No sync right now, go ahead immediately.
   1406      this.sync({ why });
   1407    }
   1408  },
   1409 
   1410  /**
   1411   * Update the "declined" information in meta/global if necessary.
   1412   */
   1413  async _maybeUpdateDeclined() {
   1414    // if Sync failed due to no node we will not have a meta URL, so can't
   1415    // update anything.
   1416    if (!this.metaURL) {
   1417      return;
   1418    }
   1419    let meta = await this.recordManager.get(this.metaURL);
   1420    if (!meta) {
   1421      this._log.warn("No meta/global; can't update declined state.");
   1422      return;
   1423    }
   1424 
   1425    let declinedEngines = new DeclinedEngines(this);
   1426    let didChange = declinedEngines.updateDeclined(meta, this.engineManager);
   1427    if (!didChange) {
   1428      this._log.info(
   1429        "No change to declined engines. Not reuploading meta/global."
   1430      );
   1431      return;
   1432    }
   1433 
   1434    await this.uploadMetaGlobal(meta);
   1435  },
   1436 
   1437  /**
   1438   * Upload a fresh meta/global record
   1439   *
   1440   * @throws the response object if the upload request was not a success
   1441   */
   1442  async _uploadNewMetaGlobal() {
   1443    let meta = new WBORecord("meta", "global");
   1444    meta.payload.syncID = this.syncID;
   1445    meta.payload.storageVersion = STORAGE_VERSION;
   1446    meta.payload.declined = this.engineManager.getDeclined();
   1447    meta.modified = 0;
   1448    meta.isNew = true;
   1449 
   1450    await this.uploadMetaGlobal(meta);
   1451  },
   1452 
   1453  /**
   1454   * Upload meta/global, throwing the response on failure
   1455   *
   1456   * @param {WBORecord} meta meta/global record
   1457   * @throws the response object if the request was not a success
   1458   */
   1459  async uploadMetaGlobal(meta) {
   1460    this._log.debug("Uploading meta/global", meta);
   1461    let res = this.resource(this.metaURL);
   1462    res.setHeader("X-If-Unmodified-Since", meta.modified);
   1463    let response = await res.put(meta);
   1464    if (!response.success) {
   1465      throw response;
   1466    }
   1467    // From https://docs.services.mozilla.com/storage/apis-1.5.html:
   1468    // "Successful responses will return the new last-modified time for the collection."
   1469    meta.modified = response.obj;
   1470    this.recordManager.set(this.metaURL, meta);
   1471  },
   1472 
   1473  /**
   1474   * Upload crypto/keys
   1475   *
   1476   * @param {WBORecord} cryptoKeys crypto/keys record
   1477   * @param {number} lastModified known last modified timestamp (in decimal seconds),
   1478   *                 will be used to set the X-If-Unmodified-Since header
   1479   */
   1480  async _uploadCryptoKeys(cryptoKeys, lastModified) {
   1481    this._log.debug(`Uploading crypto/keys (lastModified: ${lastModified})`);
   1482    let res = this.resource(this.cryptoKeysURL);
   1483    res.setHeader("X-If-Unmodified-Since", lastModified);
   1484    return res.put(cryptoKeys);
   1485  },
   1486 
   1487  async _freshStart() {
   1488    this._log.info("Fresh start. Resetting client.");
   1489    await this.resetClient();
   1490    this.collectionKeys.clear();
   1491 
   1492    // Wipe the server.
   1493    await this.wipeServer();
   1494 
   1495    // Upload a new meta/global record.
   1496    // _uploadNewMetaGlobal throws on failure -- including race conditions.
   1497    // If we got into a race condition, we'll abort the sync this way, too.
   1498    // That's fine. We'll just wait till the next sync. The client that we're
   1499    // racing is probably busy uploading stuff right now anyway.
   1500    await this._uploadNewMetaGlobal();
   1501 
   1502    // Wipe everything we know about except meta because we just uploaded it
   1503    // TODO: there's a bug here. We should be calling resetClient, no?
   1504 
   1505    // Generate, upload, and download new keys. Do this last so we don't wipe
   1506    // them...
   1507    await this.generateNewSymmetricKeys();
   1508  },
   1509 
   1510  /**
   1511   * Wipe user data from the server.
   1512   *
   1513   * @param collections [optional]
   1514   *        Array of collections to wipe. If not given, all collections are
   1515   *        wiped by issuing a DELETE request for `storageURL`.
   1516   *
   1517   * @return the server's timestamp of the (last) DELETE.
   1518   */
   1519  async wipeServer(collections) {
   1520    let response;
   1521    if (!collections) {
   1522      // Strip the trailing slash.
   1523      let res = this.resource(this.storageURL.slice(0, -1));
   1524      res.setHeader("X-Confirm-Delete", "1");
   1525      try {
   1526        response = await res.delete();
   1527      } catch (ex) {
   1528        this._log.debug("Failed to wipe server", ex);
   1529        throw ex;
   1530      }
   1531      if (response.status != 200 && response.status != 404) {
   1532        this._log.debug(
   1533          "Aborting wipeServer. Server responded with " +
   1534            response.status +
   1535            " response for " +
   1536            this.storageURL
   1537        );
   1538        throw response;
   1539      }
   1540      return response.headers["x-weave-timestamp"];
   1541    }
   1542 
   1543    let timestamp;
   1544    for (let name of collections) {
   1545      let url = this.storageURL + name;
   1546      try {
   1547        response = await this.resource(url).delete();
   1548      } catch (ex) {
   1549        this._log.debug("Failed to wipe '" + name + "' collection", ex);
   1550        throw ex;
   1551      }
   1552 
   1553      if (response.status != 200 && response.status != 404) {
   1554        this._log.debug(
   1555          "Aborting wipeServer. Server responded with " +
   1556            response.status +
   1557            " response for " +
   1558            url
   1559        );
   1560        throw response;
   1561      }
   1562 
   1563      if ("x-weave-timestamp" in response.headers) {
   1564        timestamp = response.headers["x-weave-timestamp"];
   1565      }
   1566    }
   1567    return timestamp;
   1568  },
   1569 
   1570  /**
   1571   * Wipe all local user data.
   1572   *
   1573   * @param engines [optional]
   1574   *        Array of engine names to wipe. If not given, all engines are used.
   1575   */
   1576  async wipeClient(engines) {
   1577    // If we don't have any engines, reset the service and wipe all engines
   1578    if (!engines) {
   1579      // Clear out any service data
   1580      await this.resetService();
   1581 
   1582      engines = [this.clientsEngine, ...this.engineManager.getAll()];
   1583    } else {
   1584      // Convert the array of names into engines
   1585      engines = this.engineManager.get(engines);
   1586    }
   1587 
   1588    // Fully wipe each engine if it's able to decrypt data
   1589    for (let engine of engines) {
   1590      if (await engine.canDecrypt()) {
   1591        await engine.wipeClient();
   1592      }
   1593    }
   1594  },
   1595 
   1596  /**
   1597   * Wipe all remote user data by wiping the server then telling each remote
   1598   * client to wipe itself.
   1599   *
   1600   * @param engines
   1601   *        Array of engine names to wipe.
   1602   */
   1603  async wipeRemote(engines) {
   1604    try {
   1605      // Make sure stuff gets uploaded.
   1606      await this.resetClient(engines);
   1607 
   1608      // Clear out any server data.
   1609      await this.wipeServer(engines);
   1610 
   1611      // Only wipe the engines provided.
   1612      let extra = { reason: "wipe-remote" };
   1613      for (const e of engines) {
   1614        await this.clientsEngine.sendCommand("wipeEngine", [e], null, extra);
   1615      }
   1616 
   1617      // Make sure the changed clients get updated.
   1618      await this.clientsEngine.sync();
   1619    } catch (ex) {
   1620      this.errorHandler.checkServerError(ex);
   1621      throw ex;
   1622    }
   1623  },
   1624 
   1625  /**
   1626   * Reset local service information like logs, sync times, caches.
   1627   */
   1628  async resetService() {
   1629    return this._catch(async function reset() {
   1630      this._log.info("Service reset.");
   1631 
   1632      // Pretend we've never synced to the server and drop cached data
   1633      this.syncID = "";
   1634      this.recordManager.clearCache();
   1635    })();
   1636  },
   1637 
   1638  /**
   1639   * Reset the client by getting rid of any local server data and client data.
   1640   *
   1641   * @param engines [optional]
   1642   *        Array of engine names to reset. If not given, all engines are used.
   1643   */
   1644  async resetClient(engines) {
   1645    return this._catch(async function doResetClient() {
   1646      // If we don't have any engines, reset everything including the service
   1647      if (!engines) {
   1648        // Clear out any service data
   1649        await this.resetService();
   1650 
   1651        engines = [this.clientsEngine, ...this.engineManager.getAll()];
   1652      } else {
   1653        // Convert the array of names into engines
   1654        engines = this.engineManager.get(engines);
   1655      }
   1656 
   1657      // Have each engine drop any temporary meta data
   1658      for (let engine of engines) {
   1659        await engine.resetClient();
   1660      }
   1661    })();
   1662  },
   1663 
   1664  recordTelemetryEvent(object, method, value, extra = undefined) {
   1665    Svc.Obs.notify("weave:telemetry:event", { object, method, value, extra });
   1666  },
   1667 };
   1668 
   1669 export var Service = new Sync11Service();
   1670 Service.promiseInitialized = new Promise(resolve => {
   1671  Service.onStartup().then(resolve);
   1672 });