tor-browser

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

enginesync.sys.mjs (14017B)


      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 file contains code for synchronizing engines.
      7 */
      8 
      9 import { Log } from "resource://gre/modules/Log.sys.mjs";
     10 
     11 import {
     12  ABORT_SYNC_COMMAND,
     13  LOGIN_FAILED_NETWORK_ERROR,
     14  NO_SYNC_NODE_FOUND,
     15  STATUS_OK,
     16  SYNC_FAILED_PARTIAL,
     17  SYNC_SUCCEEDED,
     18  WEAVE_VERSION,
     19  kSyncNetworkOffline,
     20 } from "resource://services-sync/constants.sys.mjs";
     21 
     22 import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
     23 
     24 import { Async } from "resource://services-common/async.sys.mjs";
     25 
     26 const lazy = {};
     27 ChromeUtils.defineESModuleGetters(lazy, {
     28  Doctor: "resource://services-sync/doctor.sys.mjs",
     29 });
     30 
     31 /**
     32 * Perform synchronization of engines.
     33 *
     34 * This was originally split out of service.js. The API needs lots of love.
     35 */
     36 export function EngineSynchronizer(service) {
     37  this._log = Log.repository.getLogger("Sync.Synchronizer");
     38  this._log.manageLevelFromPref("services.sync.log.logger.synchronizer");
     39 
     40  this.service = service;
     41 }
     42 
     43 EngineSynchronizer.prototype = {
     44  async sync(engineNamesToSync, why) {
     45    let fastSync = why && why == "sleep";
     46    let startTime = Date.now();
     47 
     48    this.service.status.resetSync();
     49 
     50    // Make sure we should sync or record why we shouldn't.
     51    let reason = this.service._checkSync();
     52    if (reason) {
     53      if (reason == kSyncNetworkOffline) {
     54        this.service.status.sync = LOGIN_FAILED_NETWORK_ERROR;
     55      }
     56 
     57      // this is a purposeful abort rather than a failure, so don't set
     58      // any status bits
     59      reason = "Can't sync: " + reason;
     60      throw new Error(reason);
     61    }
     62 
     63    // If we don't have a node, get one. If that fails, retry in 10 minutes.
     64    if (
     65      !this.service.clusterURL &&
     66      !(await this.service.identity.setCluster())
     67    ) {
     68      this.service.status.sync = NO_SYNC_NODE_FOUND;
     69      this._log.info("No cluster URL found. Cannot sync.");
     70      return;
     71    }
     72 
     73    // Ping the server with a special info request once a day.
     74    let infoURL = this.service.infoURL;
     75    let now = Math.floor(Date.now() / 1000);
     76    let lastPing = Svc.PrefBranch.getIntPref("lastPing", 0);
     77    if (now - lastPing > 86400) {
     78      // 60 * 60 * 24
     79      infoURL += "?v=" + WEAVE_VERSION;
     80      Svc.PrefBranch.setIntPref("lastPing", now);
     81    }
     82 
     83    let engineManager = this.service.engineManager;
     84 
     85    // Figure out what the last modified time is for each collection
     86    let info = await this.service._fetchInfo(infoURL);
     87 
     88    // Convert the response to an object and read out the modified times
     89    for (let engine of [this.service.clientsEngine].concat(
     90      engineManager.getAll()
     91    )) {
     92      engine.lastModified = info.obj[engine.name] || 0;
     93    }
     94 
     95    if (!(await this.service._remoteSetup(info, !fastSync))) {
     96      throw new Error("Aborting sync, remote setup failed");
     97    }
     98 
     99    if (!fastSync) {
    100      // Make sure we have an up-to-date list of clients before sending commands
    101      this._log.debug("Refreshing client list.");
    102      if (!(await this._syncEngine(this.service.clientsEngine))) {
    103        // Clients is an engine like any other; it can fail with a 401,
    104        // and we can elect to abort the sync.
    105        this._log.warn("Client engine sync failed. Aborting.");
    106        return;
    107      }
    108    }
    109 
    110    // We only honor the "hint" of what engines to Sync if this isn't
    111    // a first sync.
    112    let allowEnginesHint = false;
    113    // Wipe data in the desired direction if necessary
    114    switch (Svc.PrefBranch.getStringPref("firstSync", null)) {
    115      case "resetClient":
    116        await this.service.resetClient(engineManager.enabledEngineNames);
    117        break;
    118      case "wipeClient":
    119        await this.service.wipeClient(engineManager.enabledEngineNames);
    120        break;
    121      case "wipeRemote":
    122        await this.service.wipeRemote(engineManager.enabledEngineNames);
    123        break;
    124      default:
    125        allowEnginesHint = true;
    126        break;
    127    }
    128 
    129    if (!fastSync && this.service.clientsEngine.localCommands) {
    130      try {
    131        if (!(await this.service.clientsEngine.processIncomingCommands())) {
    132          this.service.status.sync = ABORT_SYNC_COMMAND;
    133          throw new Error("Processed command aborted sync.");
    134        }
    135 
    136        // Repeat remoteSetup in-case the commands forced us to reset
    137        if (!(await this.service._remoteSetup(info))) {
    138          throw new Error("Remote setup failed after processing commands.");
    139        }
    140      } finally {
    141        // Always immediately attempt to push back the local client (now
    142        // without commands).
    143        // Note that we don't abort here; if there's a 401 because we've
    144        // been reassigned, we'll handle it around another engine.
    145        await this._syncEngine(this.service.clientsEngine);
    146      }
    147    }
    148 
    149    // Update engines because it might change what we sync.
    150    try {
    151      await this._updateEnabledEngines();
    152    } catch (ex) {
    153      this._log.debug("Updating enabled engines failed", ex);
    154      this.service.errorHandler.checkServerError(ex);
    155      throw ex;
    156    }
    157 
    158    await this.service.engineManager.switchAlternatives();
    159 
    160    // If the engines to sync has been specified, we sync in the order specified.
    161    let enginesToSync;
    162    if (allowEnginesHint && engineNamesToSync) {
    163      this._log.info("Syncing specified engines", engineNamesToSync);
    164      enginesToSync = engineManager
    165        .get(engineNamesToSync)
    166        .filter(e => e.enabled);
    167    } else {
    168      this._log.info("Syncing all enabled engines.");
    169      enginesToSync = engineManager.getEnabled();
    170    }
    171    try {
    172      // We don't bother validating engines that failed to sync.
    173      let enginesToValidate = [];
    174      for (let engine of enginesToSync) {
    175        if (engine.shouldSkipSync(why)) {
    176          this._log.info(`Engine ${engine.name} asked to be skipped`);
    177          continue;
    178        }
    179        // If there's any problems with syncing the engine, report the failure
    180        if (
    181          !(await this._syncEngine(engine)) ||
    182          this.service.status.enforceBackoff
    183        ) {
    184          this._log.info("Aborting sync for failure in " + engine.name);
    185          break;
    186        }
    187        enginesToValidate.push(engine);
    188      }
    189 
    190      // If _syncEngine fails for a 401, we might not have a cluster URL here.
    191      // If that's the case, break out of this immediately, rather than
    192      // throwing an exception when trying to fetch metaURL.
    193      if (!this.service.clusterURL) {
    194        this._log.debug(
    195          "Aborting sync, no cluster URL: not uploading new meta/global."
    196        );
    197        return;
    198      }
    199 
    200      // Upload meta/global if any engines changed anything.
    201      let meta = await this.service.recordManager.get(this.service.metaURL);
    202      if (meta.isNew || meta.changed) {
    203        this._log.info("meta/global changed locally: reuploading.");
    204        try {
    205          await this.service.uploadMetaGlobal(meta);
    206          delete meta.isNew;
    207          delete meta.changed;
    208        } catch (error) {
    209          this._log.error(
    210            "Unable to upload meta/global. Leaving marked as new."
    211          );
    212        }
    213      }
    214 
    215      if (!fastSync) {
    216        await lazy.Doctor.consult(enginesToValidate);
    217      }
    218 
    219      // If there were no sync engine failures
    220      if (this.service.status.service != SYNC_FAILED_PARTIAL) {
    221        this.service.status.sync = SYNC_SUCCEEDED;
    222      }
    223 
    224      // Even if there were engine failures, bump lastSync even on partial since
    225      // it's reflected in the UI (bug 1439777).
    226      if (
    227        this.service.status.service == SYNC_FAILED_PARTIAL ||
    228        this.service.status.service == STATUS_OK
    229      ) {
    230        Svc.PrefBranch.setStringPref("lastSync", new Date().toString());
    231      }
    232    } finally {
    233      Svc.PrefBranch.clearUserPref("firstSync");
    234 
    235      let syncTime = ((Date.now() - startTime) / 1000).toFixed(2);
    236      let dateStr = Utils.formatTimestamp(new Date());
    237      this._log.info(
    238        "Sync completed at " + dateStr + " after " + syncTime + " secs."
    239      );
    240    }
    241  },
    242 
    243  // Returns true if sync should proceed.
    244  // false / no return value means sync should be aborted.
    245  async _syncEngine(engine) {
    246    try {
    247      await engine.sync();
    248    } catch (e) {
    249      if (e.status == 401) {
    250        // Maybe a 401, cluster update perhaps needed?
    251        // We rely on ErrorHandler observing the sync failure notification to
    252        // schedule another sync and clear node assignment values.
    253        // Here we simply want to muffle the exception and return an
    254        // appropriate value.
    255        return false;
    256      }
    257      // Note that policies.js has already logged info about the exception...
    258      if (Async.isShutdownException(e)) {
    259        // Failure due to a shutdown exception should prevent other engines
    260        // trying to start and immediately failing.
    261        this._log.info(
    262          `${engine.name} was interrupted by shutdown; no other engines will sync`
    263        );
    264        return false;
    265      }
    266    }
    267 
    268    return true;
    269  },
    270 
    271  async _updateEnabledFromMeta(
    272    meta,
    273    numClients,
    274    engineManager = this.service.engineManager
    275  ) {
    276    this._log.info("Updating enabled engines: " + numClients + " clients.");
    277 
    278    if (meta.isNew || !meta.payload.engines) {
    279      this._log.debug(
    280        "meta/global isn't new, or is missing engines. Not updating enabled state."
    281      );
    282      return;
    283    }
    284 
    285    // If we're the only client and the server has no data for us
    286    // (neither enabled *nor* declined engines), just keep our local state.
    287    // Belt-and-suspenders approach to Bug 615926.
    288    let hasEnabledEngines = false;
    289    for (let e in meta.payload.engines) {
    290      if (e != "clients") {
    291        hasEnabledEngines = true;
    292        break;
    293      }
    294    }
    295 
    296    let hasDeclinedEngines =
    297      Array.isArray(meta.payload.declined) && meta.payload.declined.length;
    298 
    299    if (numClients <= 1 && !hasEnabledEngines && !hasDeclinedEngines) {
    300      this._log.info(
    301        "One client and neither enabled nor declined engines on server: " +
    302          "not touching local engine status."
    303      );
    304      return;
    305    }
    306 
    307    this.service._ignorePrefObserver = true;
    308 
    309    let enabled = engineManager.enabledEngineNames;
    310 
    311    let toDecline = new Set();
    312    let toUndecline = new Set();
    313 
    314    for (let engineName in meta.payload.engines) {
    315      if (engineName == "clients") {
    316        // Clients is special.
    317        continue;
    318      }
    319      let index = enabled.indexOf(engineName);
    320      if (index != -1) {
    321        // The engine is enabled locally. Nothing to do.
    322        enabled.splice(index, 1);
    323        continue;
    324      }
    325      let engine = engineManager.get(engineName);
    326      if (!engine) {
    327        // The engine doesn't exist locally. Nothing to do.
    328        continue;
    329      }
    330 
    331      let attemptedEnable = false;
    332      // If the engine was enabled remotely, enable it locally.
    333      if (
    334        !Svc.PrefBranch.getBoolPref(
    335          "engineStatusChanged." + engine.prefName,
    336          false
    337        )
    338      ) {
    339        this._log.trace(
    340          "Engine " + engineName + " was enabled. Marking as non-declined."
    341        );
    342        toUndecline.add(engineName);
    343        this._log.trace(engineName + " engine was enabled remotely.");
    344        engine.enabled = true;
    345        // Note that setting engine.enabled to true might not have worked for
    346        // the password engine if a master-password is enabled.  However, it's
    347        // still OK that we added it to undeclined - the user *tried* to enable
    348        // it remotely - so it still winds up as not being flagged as declined
    349        // even though it's disabled remotely.
    350        attemptedEnable = true;
    351      }
    352 
    353      // If either the engine was disabled locally or enabling the engine
    354      // failed (see above re master-password) then wipe server data and
    355      // disable it everywhere.
    356      if (!engine.enabled) {
    357        this._log.trace("Wiping data for " + engineName + " engine.");
    358        await engine.wipeServer();
    359        delete meta.payload.engines[engineName];
    360        meta.changed = true; // the new enabled state must propagate
    361        // We also here mark the engine as declined, because the pref
    362        // was explicitly changed to false - unless we tried, and failed,
    363        // to enable it - in which case we leave the declined state alone.
    364        if (!attemptedEnable) {
    365          // This will be reflected in meta/global in the next stage.
    366          this._log.trace(
    367            "Engine " +
    368              engineName +
    369              " was disabled locally. Marking as declined."
    370          );
    371          toDecline.add(engineName);
    372        }
    373      }
    374    }
    375 
    376    // Any remaining engines were either enabled locally or disabled remotely.
    377    for (let engineName of enabled) {
    378      let engine = engineManager.get(engineName);
    379      if (
    380        Svc.PrefBranch.getBoolPref(
    381          "engineStatusChanged." + engine.prefName,
    382          false
    383        )
    384      ) {
    385        this._log.trace("The " + engineName + " engine was enabled locally.");
    386        toUndecline.add(engineName);
    387      } else {
    388        this._log.trace("The " + engineName + " engine was disabled remotely.");
    389 
    390        // Don't automatically mark it as declined!
    391        try {
    392          engine.enabled = false;
    393        } catch (e) {
    394          this._log.trace("Failed to disable engine " + engineName);
    395        }
    396      }
    397    }
    398 
    399    engineManager.decline(toDecline);
    400    engineManager.undecline(toUndecline);
    401 
    402    for (const pref of Svc.PrefBranch.getChildList("engineStatusChanged.")) {
    403      Svc.PrefBranch.clearUserPref(pref);
    404    }
    405    this.service._ignorePrefObserver = false;
    406  },
    407 
    408  async _updateEnabledEngines() {
    409    let meta = await this.service.recordManager.get(this.service.metaURL);
    410    let numClients = this.service.scheduler.numClients;
    411    let engineManager = this.service.engineManager;
    412 
    413    await this._updateEnabledFromMeta(meta, numClients, engineManager);
    414  },
    415 };
    416 Object.freeze(EngineSynchronizer.prototype);