services/sync/modules/service.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/modules/service.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1575 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["Service"];
     1.9 +
    1.10 +const Cc = Components.classes;
    1.11 +const Ci = Components.interfaces;
    1.12 +const Cr = Components.results;
    1.13 +const Cu = Components.utils;
    1.14 +
    1.15 +// How long before refreshing the cluster
    1.16 +const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes
    1.17 +
    1.18 +// How long a key to generate from an old passphrase.
    1.19 +const PBKDF2_KEY_BYTES = 16;
    1.20 +
    1.21 +const CRYPTO_COLLECTION = "crypto";
    1.22 +const KEYS_WBO = "keys";
    1.23 +
    1.24 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.25 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.26 +Cu.import("resource://gre/modules/Log.jsm");
    1.27 +Cu.import("resource://services-common/utils.js");
    1.28 +Cu.import("resource://services-sync/constants.js");
    1.29 +Cu.import("resource://services-sync/engines.js");
    1.30 +Cu.import("resource://services-sync/engines/clients.js");
    1.31 +Cu.import("resource://services-sync/identity.js");
    1.32 +Cu.import("resource://services-sync/policies.js");
    1.33 +Cu.import("resource://services-sync/record.js");
    1.34 +Cu.import("resource://services-sync/resource.js");
    1.35 +Cu.import("resource://services-sync/rest.js");
    1.36 +Cu.import("resource://services-sync/stages/enginesync.js");
    1.37 +Cu.import("resource://services-sync/stages/declined.js");
    1.38 +Cu.import("resource://services-sync/status.js");
    1.39 +Cu.import("resource://services-sync/userapi.js");
    1.40 +Cu.import("resource://services-sync/util.js");
    1.41 +
    1.42 +const ENGINE_MODULES = {
    1.43 +  Addons: "addons.js",
    1.44 +  Bookmarks: "bookmarks.js",
    1.45 +  Form: "forms.js",
    1.46 +  History: "history.js",
    1.47 +  Password: "passwords.js",
    1.48 +  Prefs: "prefs.js",
    1.49 +  Tab: "tabs.js",
    1.50 +};
    1.51 +
    1.52 +const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
    1.53 +                            INFO_COLLECTION_USAGE,
    1.54 +                            INFO_COLLECTION_COUNTS,
    1.55 +                            INFO_QUOTA];
    1.56 +
    1.57 +
    1.58 +function Sync11Service() {
    1.59 +  this._notify = Utils.notify("weave:service:");
    1.60 +}
    1.61 +Sync11Service.prototype = {
    1.62 +
    1.63 +  _lock: Utils.lock,
    1.64 +  _locked: false,
    1.65 +  _loggedIn: false,
    1.66 +
    1.67 +  infoURL: null,
    1.68 +  storageURL: null,
    1.69 +  metaURL: null,
    1.70 +  cryptoKeyURL: null,
    1.71 +
    1.72 +  get serverURL() Svc.Prefs.get("serverURL"),
    1.73 +  set serverURL(value) {
    1.74 +    if (!value.endsWith("/")) {
    1.75 +      value += "/";
    1.76 +    }
    1.77 +
    1.78 +    // Only do work if it's actually changing
    1.79 +    if (value == this.serverURL)
    1.80 +      return;
    1.81 +
    1.82 +    // A new server most likely uses a different cluster, so clear that
    1.83 +    Svc.Prefs.set("serverURL", value);
    1.84 +    Svc.Prefs.reset("clusterURL");
    1.85 +  },
    1.86 +
    1.87 +  get clusterURL() Svc.Prefs.get("clusterURL", ""),
    1.88 +  set clusterURL(value) {
    1.89 +    Svc.Prefs.set("clusterURL", value);
    1.90 +    this._updateCachedURLs();
    1.91 +  },
    1.92 +
    1.93 +  get miscAPI() {
    1.94 +    // Append to the serverURL if it's a relative fragment
    1.95 +    let misc = Svc.Prefs.get("miscURL");
    1.96 +    if (misc.indexOf(":") == -1)
    1.97 +      misc = this.serverURL + misc;
    1.98 +    return misc + MISC_API_VERSION + "/";
    1.99 +  },
   1.100 +
   1.101 +  /**
   1.102 +   * The URI of the User API service.
   1.103 +   *
   1.104 +   * This is the base URI of the service as applicable to all users up to
   1.105 +   * and including the server version path component, complete with trailing
   1.106 +   * forward slash.
   1.107 +   */
   1.108 +  get userAPIURI() {
   1.109 +    // Append to the serverURL if it's a relative fragment.
   1.110 +    let url = Svc.Prefs.get("userURL");
   1.111 +    if (!url.contains(":")) {
   1.112 +      url = this.serverURL + url;
   1.113 +    }
   1.114 +
   1.115 +    return url + USER_API_VERSION + "/";
   1.116 +  },
   1.117 +
   1.118 +  get pwResetURL() {
   1.119 +    return this.serverURL + "weave-password-reset";
   1.120 +  },
   1.121 +
   1.122 +  get syncID() {
   1.123 +    // Generate a random syncID id we don't have one
   1.124 +    let syncID = Svc.Prefs.get("client.syncID", "");
   1.125 +    return syncID == "" ? this.syncID = Utils.makeGUID() : syncID;
   1.126 +  },
   1.127 +  set syncID(value) {
   1.128 +    Svc.Prefs.set("client.syncID", value);
   1.129 +  },
   1.130 +
   1.131 +  get isLoggedIn() { return this._loggedIn; },
   1.132 +
   1.133 +  get locked() { return this._locked; },
   1.134 +  lock: function lock() {
   1.135 +    if (this._locked)
   1.136 +      return false;
   1.137 +    this._locked = true;
   1.138 +    return true;
   1.139 +  },
   1.140 +  unlock: function unlock() {
   1.141 +    this._locked = false;
   1.142 +  },
   1.143 +
   1.144 +  // A specialized variant of Utils.catch.
   1.145 +  // This provides a more informative error message when we're already syncing:
   1.146 +  // see Bug 616568.
   1.147 +  _catch: function _catch(func) {
   1.148 +    function lockExceptions(ex) {
   1.149 +      if (Utils.isLockException(ex)) {
   1.150 +        // This only happens if we're syncing already.
   1.151 +        this._log.info("Cannot start sync: already syncing?");
   1.152 +      }
   1.153 +    }
   1.154 +
   1.155 +    return Utils.catch.call(this, func, lockExceptions);
   1.156 +  },
   1.157 +
   1.158 +  get userBaseURL() {
   1.159 +    if (!this._clusterManager) {
   1.160 +      return null;
   1.161 +    }
   1.162 +    return this._clusterManager.getUserBaseURL();
   1.163 +  },
   1.164 +
   1.165 +  _updateCachedURLs: function _updateCachedURLs() {
   1.166 +    // Nothing to cache yet if we don't have the building blocks
   1.167 +    if (!this.clusterURL || !this.identity.username)
   1.168 +      return;
   1.169 +
   1.170 +    this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
   1.171 +
   1.172 +    // Generate and cache various URLs under the storage API for this user
   1.173 +    this.infoURL = this.userBaseURL + "info/collections";
   1.174 +    this.storageURL = this.userBaseURL + "storage/";
   1.175 +    this.metaURL = this.storageURL + "meta/global";
   1.176 +    this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO;
   1.177 +  },
   1.178 +
   1.179 +  _checkCrypto: function _checkCrypto() {
   1.180 +    let ok = false;
   1.181 +
   1.182 +    try {
   1.183 +      let iv = Svc.Crypto.generateRandomIV();
   1.184 +      if (iv.length == 24)
   1.185 +        ok = true;
   1.186 +
   1.187 +    } catch (e) {
   1.188 +      this._log.debug("Crypto check failed: " + e);
   1.189 +    }
   1.190 +
   1.191 +    return ok;
   1.192 +  },
   1.193 +
   1.194 +  /**
   1.195 +   * Here is a disgusting yet reasonable way of handling HMAC errors deep in
   1.196 +   * the guts of Sync. The astute reader will note that this is a hacky way of
   1.197 +   * implementing something like continuable conditions.
   1.198 +   *
   1.199 +   * A handler function is glued to each engine. If the engine discovers an
   1.200 +   * HMAC failure, we fetch keys from the server and update our keys, just as
   1.201 +   * we would on startup.
   1.202 +   *
   1.203 +   * If our key collection changed, we signal to the engine (via our return
   1.204 +   * value) that it should retry decryption.
   1.205 +   *
   1.206 +   * If our key collection did not change, it means that we already had the
   1.207 +   * correct keys... and thus a different client has the wrong ones. Reupload
   1.208 +   * the bundle that we fetched, which will bump the modified time on the
   1.209 +   * server and (we hope) prompt a broken client to fix itself.
   1.210 +   *
   1.211 +   * We keep track of the time at which we last applied this reasoning, because
   1.212 +   * thrashing doesn't solve anything. We keep a reasonable interval between
   1.213 +   * these remedial actions.
   1.214 +   */
   1.215 +  lastHMACEvent: 0,
   1.216 +
   1.217 +  /*
   1.218 +   * Returns whether to try again.
   1.219 +   */
   1.220 +  handleHMACEvent: function handleHMACEvent() {
   1.221 +    let now = Date.now();
   1.222 +
   1.223 +    // Leave a sizable delay between HMAC recovery attempts. This gives us
   1.224 +    // time for another client to fix themselves if we touch the record.
   1.225 +    if ((now - this.lastHMACEvent) < HMAC_EVENT_INTERVAL)
   1.226 +      return false;
   1.227 +
   1.228 +    this._log.info("Bad HMAC event detected. Attempting recovery " +
   1.229 +                   "or signaling to other clients.");
   1.230 +
   1.231 +    // Set the last handled time so that we don't act again.
   1.232 +    this.lastHMACEvent = now;
   1.233 +
   1.234 +    // Fetch keys.
   1.235 +    let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
   1.236 +    try {
   1.237 +      let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
   1.238 +
   1.239 +      // Save out the ciphertext for when we reupload. If there's a bug in
   1.240 +      // CollectionKeyManager, this will prevent us from uploading junk.
   1.241 +      let cipherText = cryptoKeys.ciphertext;
   1.242 +
   1.243 +      if (!cryptoResp.success) {
   1.244 +        this._log.warn("Failed to download keys.");
   1.245 +        return false;
   1.246 +      }
   1.247 +
   1.248 +      let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
   1.249 +                                               cryptoKeys, true);
   1.250 +      if (keysChanged) {
   1.251 +        // Did they change? If so, carry on.
   1.252 +        this._log.info("Suggesting retry.");
   1.253 +        return true;              // Try again.
   1.254 +      }
   1.255 +
   1.256 +      // If not, reupload them and continue the current sync.
   1.257 +      cryptoKeys.ciphertext = cipherText;
   1.258 +      cryptoKeys.cleartext  = null;
   1.259 +
   1.260 +      let uploadResp = cryptoKeys.upload(this.resource(this.cryptoKeysURL));
   1.261 +      if (uploadResp.success)
   1.262 +        this._log.info("Successfully re-uploaded keys. Continuing sync.");
   1.263 +      else
   1.264 +        this._log.warn("Got error response re-uploading keys. " +
   1.265 +                       "Continuing sync; let's try again later.");
   1.266 +
   1.267 +      return false;            // Don't try again: same keys.
   1.268 +
   1.269 +    } catch (ex) {
   1.270 +      this._log.warn("Got exception \"" + ex + "\" fetching and handling " +
   1.271 +                     "crypto keys. Will try again later.");
   1.272 +      return false;
   1.273 +    }
   1.274 +  },
   1.275 +
   1.276 +  handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) {
   1.277 +    // Don't want to wipe if we're just starting up!
   1.278 +    let wasBlank = this.collectionKeys.isClear;
   1.279 +    let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys);
   1.280 +
   1.281 +    if (keysChanged && !wasBlank) {
   1.282 +      this._log.debug("Keys changed: " + JSON.stringify(keysChanged));
   1.283 +
   1.284 +      if (!skipReset) {
   1.285 +        this._log.info("Resetting client to reflect key change.");
   1.286 +
   1.287 +        if (keysChanged.length) {
   1.288 +          // Collection keys only. Reset individual engines.
   1.289 +          this.resetClient(keysChanged);
   1.290 +        }
   1.291 +        else {
   1.292 +          // Default key changed: wipe it all.
   1.293 +          this.resetClient();
   1.294 +        }
   1.295 +
   1.296 +        this._log.info("Downloaded new keys, client reset. Proceeding.");
   1.297 +      }
   1.298 +      return true;
   1.299 +    }
   1.300 +    return false;
   1.301 +  },
   1.302 +
   1.303 +  /**
   1.304 +   * Prepare to initialize the rest of Weave after waiting a little bit
   1.305 +   */
   1.306 +  onStartup: function onStartup() {
   1.307 +    this._migratePrefs();
   1.308 +
   1.309 +    // Status is instantiated before us and is the first to grab an instance of
   1.310 +    // the IdentityManager. We use that instance because IdentityManager really
   1.311 +    // needs to be a singleton. Ideally, the longer-lived object would spawn
   1.312 +    // this service instance.
   1.313 +    if (!Status || !Status._authManager) {
   1.314 +      throw new Error("Status or Status._authManager not initialized.");
   1.315 +    }
   1.316 +
   1.317 +    this.status = Status;
   1.318 +    this.identity = Status._authManager;
   1.319 +    this.collectionKeys = new CollectionKeyManager();
   1.320 +
   1.321 +    this.errorHandler = new ErrorHandler(this);
   1.322 +
   1.323 +    this._log = Log.repository.getLogger("Sync.Service");
   1.324 +    this._log.level =
   1.325 +      Log.Level[Svc.Prefs.get("log.logger.service.main")];
   1.326 +
   1.327 +    this._log.info("Loading Weave " + WEAVE_VERSION);
   1.328 +
   1.329 +    this._clusterManager = this.identity.createClusterManager(this);
   1.330 +    this.recordManager = new RecordManager(this);
   1.331 +
   1.332 +    this.enabled = true;
   1.333 +
   1.334 +    this._registerEngines();
   1.335 +
   1.336 +    let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
   1.337 +      getService(Ci.nsIHttpProtocolHandler).userAgent;
   1.338 +    this._log.info(ua);
   1.339 +
   1.340 +    if (!this._checkCrypto()) {
   1.341 +      this.enabled = false;
   1.342 +      this._log.info("Could not load the Weave crypto component. Disabling " +
   1.343 +                      "Weave, since it will not work correctly.");
   1.344 +    }
   1.345 +
   1.346 +    Svc.Obs.add("weave:service:setup-complete", this);
   1.347 +    Svc.Prefs.observe("engine.", this);
   1.348 +
   1.349 +    this.scheduler = new SyncScheduler(this);
   1.350 +
   1.351 +    if (!this.enabled) {
   1.352 +      this._log.info("Firefox Sync disabled.");
   1.353 +    }
   1.354 +
   1.355 +    this._updateCachedURLs();
   1.356 +
   1.357 +    let status = this._checkSetup();
   1.358 +    if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
   1.359 +      Svc.Obs.notify("weave:engine:start-tracking");
   1.360 +    }
   1.361 +
   1.362 +    // Send an event now that Weave service is ready.  We don't do this
   1.363 +    // synchronously so that observers can import this module before
   1.364 +    // registering an observer.
   1.365 +    Utils.nextTick(function onNextTick() {
   1.366 +      this.status.ready = true;
   1.367 +
   1.368 +      // UI code uses the flag on the XPCOM service so it doesn't have
   1.369 +      // to load a bunch of modules.
   1.370 +      let xps = Cc["@mozilla.org/weave/service;1"]
   1.371 +                  .getService(Ci.nsISupports)
   1.372 +                  .wrappedJSObject;
   1.373 +      xps.ready = true;
   1.374 +
   1.375 +      Svc.Obs.notify("weave:service:ready");
   1.376 +    }.bind(this));
   1.377 +  },
   1.378 +
   1.379 +  _checkSetup: function _checkSetup() {
   1.380 +    if (!this.enabled) {
   1.381 +      return this.status.service = STATUS_DISABLED;
   1.382 +    }
   1.383 +    return this.status.checkSetup();
   1.384 +  },
   1.385 +
   1.386 +  _migratePrefs: function _migratePrefs() {
   1.387 +    // Migrate old debugLog prefs.
   1.388 +    let logLevel = Svc.Prefs.get("log.appender.debugLog");
   1.389 +    if (logLevel) {
   1.390 +      Svc.Prefs.set("log.appender.file.level", logLevel);
   1.391 +      Svc.Prefs.reset("log.appender.debugLog");
   1.392 +    }
   1.393 +    if (Svc.Prefs.get("log.appender.debugLog.enabled")) {
   1.394 +      Svc.Prefs.set("log.appender.file.logOnSuccess", true);
   1.395 +      Svc.Prefs.reset("log.appender.debugLog.enabled");
   1.396 +    }
   1.397 +
   1.398 +    // Migrate old extensions.weave.* prefs if we haven't already tried.
   1.399 +    if (Svc.Prefs.get("migrated", false))
   1.400 +      return;
   1.401 +
   1.402 +    // Grab the list of old pref names
   1.403 +    let oldPrefBranch = "extensions.weave.";
   1.404 +    let oldPrefNames = Cc["@mozilla.org/preferences-service;1"].
   1.405 +                       getService(Ci.nsIPrefService).
   1.406 +                       getBranch(oldPrefBranch).
   1.407 +                       getChildList("", {});
   1.408 +
   1.409 +    // Map each old pref to the current pref branch
   1.410 +    let oldPref = new Preferences(oldPrefBranch);
   1.411 +    for each (let pref in oldPrefNames)
   1.412 +      Svc.Prefs.set(pref, oldPref.get(pref));
   1.413 +
   1.414 +    // Remove all the old prefs and remember that we've migrated
   1.415 +    oldPref.resetBranch("");
   1.416 +    Svc.Prefs.set("migrated", true);
   1.417 +  },
   1.418 +
   1.419 +  /**
   1.420 +   * Register the built-in engines for certain applications
   1.421 +   */
   1.422 +  _registerEngines: function _registerEngines() {
   1.423 +    this.engineManager = new EngineManager(this);
   1.424 +
   1.425 +    let engines = [];
   1.426 +    // Applications can provide this preference (comma-separated list)
   1.427 +    // to specify which engines should be registered on startup.
   1.428 +    let pref = Svc.Prefs.get("registerEngines");
   1.429 +    if (pref) {
   1.430 +      engines = pref.split(",");
   1.431 +    }
   1.432 +
   1.433 +    let declined = [];
   1.434 +    pref = Svc.Prefs.get("declinedEngines");
   1.435 +    if (pref) {
   1.436 +      declined = pref.split(",");
   1.437 +    }
   1.438 +
   1.439 +    this.clientsEngine = new ClientEngine(this);
   1.440 +
   1.441 +    for (let name of engines) {
   1.442 +      if (!name in ENGINE_MODULES) {
   1.443 +        this._log.info("Do not know about engine: " + name);
   1.444 +        continue;
   1.445 +      }
   1.446 +
   1.447 +      let ns = {};
   1.448 +      try {
   1.449 +        Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns);
   1.450 +
   1.451 +        let engineName = name + "Engine";
   1.452 +        if (!(engineName in ns)) {
   1.453 +          this._log.warn("Could not find exported engine instance: " + engineName);
   1.454 +          continue;
   1.455 +        }
   1.456 +
   1.457 +        this.engineManager.register(ns[engineName]);
   1.458 +      } catch (ex) {
   1.459 +        this._log.warn("Could not register engine " + name + ": " +
   1.460 +                       CommonUtils.exceptionStr(ex));
   1.461 +      }
   1.462 +    }
   1.463 +
   1.464 +    this.engineManager.setDeclined(declined);
   1.465 +  },
   1.466 +
   1.467 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   1.468 +                                         Ci.nsISupportsWeakReference]),
   1.469 +
   1.470 +  // nsIObserver
   1.471 +
   1.472 +  observe: function observe(subject, topic, data) {
   1.473 +    switch (topic) {
   1.474 +      case "weave:service:setup-complete":
   1.475 +        let status = this._checkSetup();
   1.476 +        if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED)
   1.477 +            Svc.Obs.notify("weave:engine:start-tracking");
   1.478 +        break;
   1.479 +      case "nsPref:changed":
   1.480 +        if (this._ignorePrefObserver)
   1.481 +          return;
   1.482 +        let engine = data.slice((PREFS_BRANCH + "engine.").length);
   1.483 +        this._handleEngineStatusChanged(engine);
   1.484 +        break;
   1.485 +    }
   1.486 +  },
   1.487 +
   1.488 +  _handleEngineStatusChanged: function handleEngineDisabled(engine) {
   1.489 +    this._log.trace("Status for " + engine + " engine changed.");
   1.490 +    if (Svc.Prefs.get("engineStatusChanged." + engine, false)) {
   1.491 +      // The enabled status being changed back to what it was before.
   1.492 +      Svc.Prefs.reset("engineStatusChanged." + engine);
   1.493 +    } else {
   1.494 +      // Remember that the engine status changed locally until the next sync.
   1.495 +      Svc.Prefs.set("engineStatusChanged." + engine, true);
   1.496 +    }
   1.497 +  },
   1.498 +
   1.499 +  /**
   1.500 +   * Obtain a Resource instance with authentication credentials.
   1.501 +   */
   1.502 +  resource: function resource(url) {
   1.503 +    let res = new Resource(url);
   1.504 +    res.authenticator = this.identity.getResourceAuthenticator();
   1.505 +
   1.506 +    return res;
   1.507 +  },
   1.508 +
   1.509 +  /**
   1.510 +   * Obtain a SyncStorageRequest instance with authentication credentials.
   1.511 +   */
   1.512 +  getStorageRequest: function getStorageRequest(url) {
   1.513 +    let request = new SyncStorageRequest(url);
   1.514 +    request.authenticator = this.identity.getRESTRequestAuthenticator();
   1.515 +
   1.516 +    return request;
   1.517 +  },
   1.518 +
   1.519 +  /**
   1.520 +   * Perform the info fetch as part of a login or key fetch, or
   1.521 +   * inside engine sync.
   1.522 +   */
   1.523 +  _fetchInfo: function (url) {
   1.524 +    let infoURL = url || this.infoURL;
   1.525 +
   1.526 +    this._log.trace("In _fetchInfo: " + infoURL);
   1.527 +    let info;
   1.528 +    try {
   1.529 +      info = this.resource(infoURL).get();
   1.530 +    } catch (ex) {
   1.531 +      this.errorHandler.checkServerError(ex);
   1.532 +      throw ex;
   1.533 +    }
   1.534 +
   1.535 +    // Always check for errors; this is also where we look for X-Weave-Alert.
   1.536 +    this.errorHandler.checkServerError(info);
   1.537 +    if (!info.success) {
   1.538 +      throw "Aborting sync: failed to get collections.";
   1.539 +    }
   1.540 +    return info;
   1.541 +  },
   1.542 +
   1.543 +  verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) {
   1.544 +
   1.545 +    this._log.debug("Fetching and verifying -- or generating -- symmetric keys.");
   1.546 +
   1.547 +    // Don't allow empty/missing passphrase.
   1.548 +    // Furthermore, we assume that our sync key is already upgraded,
   1.549 +    // and fail if that assumption is invalidated.
   1.550 +
   1.551 +    if (!this.identity.syncKey) {
   1.552 +      this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
   1.553 +      this.status.sync = CREDENTIALS_CHANGED;
   1.554 +      return false;
   1.555 +    }
   1.556 +
   1.557 +    let syncKeyBundle = this.identity.syncKeyBundle;
   1.558 +    if (!syncKeyBundle) {
   1.559 +      this._log.error("Sync Key Bundle not set. Invalid Sync Key?");
   1.560 +
   1.561 +      this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
   1.562 +      this.status.sync = CREDENTIALS_CHANGED;
   1.563 +      return false;
   1.564 +    }
   1.565 +
   1.566 +    try {
   1.567 +      if (!infoResponse)
   1.568 +        infoResponse = this._fetchInfo();    // Will throw an exception on failure.
   1.569 +
   1.570 +      // This only applies when the server is already at version 4.
   1.571 +      if (infoResponse.status != 200) {
   1.572 +        this._log.warn("info/collections returned non-200 response. Failing key fetch.");
   1.573 +        this.status.login = LOGIN_FAILED_SERVER_ERROR;
   1.574 +        this.errorHandler.checkServerError(infoResponse);
   1.575 +        return false;
   1.576 +      }
   1.577 +
   1.578 +      let infoCollections = infoResponse.obj;
   1.579 +
   1.580 +      this._log.info("Testing info/collections: " + JSON.stringify(infoCollections));
   1.581 +
   1.582 +      if (this.collectionKeys.updateNeeded(infoCollections)) {
   1.583 +        this._log.info("collection keys reports that a key update is needed.");
   1.584 +
   1.585 +        // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this.
   1.586 +
   1.587 +        // Fetch storage/crypto/keys.
   1.588 +        let cryptoKeys;
   1.589 +
   1.590 +        if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) {
   1.591 +          try {
   1.592 +            cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
   1.593 +            let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
   1.594 +
   1.595 +            if (cryptoResp.success) {
   1.596 +              let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys);
   1.597 +              return true;
   1.598 +            }
   1.599 +            else if (cryptoResp.status == 404) {
   1.600 +              // On failure, ask to generate new keys and upload them.
   1.601 +              // Fall through to the behavior below.
   1.602 +              this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating.");
   1.603 +              cryptoKeys = null;
   1.604 +            }
   1.605 +            else {
   1.606 +              // Some other problem.
   1.607 +              this.status.login = LOGIN_FAILED_SERVER_ERROR;
   1.608 +              this.errorHandler.checkServerError(cryptoResp);
   1.609 +              this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys.");
   1.610 +              return false;
   1.611 +            }
   1.612 +          }
   1.613 +          catch (ex) {
   1.614 +            this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys.");
   1.615 +            // TODO: Um, what exceptions might we get here? Should we re-throw any?
   1.616 +
   1.617 +            // One kind of exception: HMAC failure.
   1.618 +            if (Utils.isHMACMismatch(ex)) {
   1.619 +              this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
   1.620 +              this.status.sync = CREDENTIALS_CHANGED;
   1.621 +            }
   1.622 +            else {
   1.623 +              // In the absence of further disambiguation or more precise
   1.624 +              // failure constants, just report failure.
   1.625 +              this.status.login = LOGIN_FAILED;
   1.626 +            }
   1.627 +            return false;
   1.628 +          }
   1.629 +        }
   1.630 +        else {
   1.631 +          this._log.info("... 'crypto' is not a reported collection. Generating new keys.");
   1.632 +        }
   1.633 +
   1.634 +        if (!cryptoKeys) {
   1.635 +          this._log.info("No keys! Generating new ones.");
   1.636 +
   1.637 +          // Better make some and upload them, and wipe the server to ensure
   1.638 +          // consistency. This is all achieved via _freshStart.
   1.639 +          // If _freshStart fails to clear the server or upload keys, it will
   1.640 +          // throw.
   1.641 +          this._freshStart();
   1.642 +          return true;
   1.643 +        }
   1.644 +
   1.645 +        // Last-ditch case.
   1.646 +        return false;
   1.647 +      }
   1.648 +      else {
   1.649 +        // No update needed: we're good!
   1.650 +        return true;
   1.651 +      }
   1.652 +
   1.653 +    } catch (ex) {
   1.654 +      // This means no keys are present, or there's a network error.
   1.655 +      this._log.debug("Failed to fetch and verify keys: "
   1.656 +                      + Utils.exceptionStr(ex));
   1.657 +      this.errorHandler.checkServerError(ex);
   1.658 +      return false;
   1.659 +    }
   1.660 +  },
   1.661 +
   1.662 +  verifyLogin: function verifyLogin(allow40XRecovery = true) {
   1.663 +    // If the identity isn't ready it  might not know the username...
   1.664 +    if (!this.identity.readyToAuthenticate) {
   1.665 +      this._log.info("Not ready to authenticate in verifyLogin.");
   1.666 +      this.status.login = LOGIN_FAILED_NOT_READY;
   1.667 +      return false;
   1.668 +    }
   1.669 +
   1.670 +    if (!this.identity.username) {
   1.671 +      this._log.warn("No username in verifyLogin.");
   1.672 +      this.status.login = LOGIN_FAILED_NO_USERNAME;
   1.673 +      return false;
   1.674 +    }
   1.675 +
   1.676 +    // Unlock master password, or return.
   1.677 +    // Attaching auth credentials to a request requires access to
   1.678 +    // passwords, which means that Resource.get can throw MP-related
   1.679 +    // exceptions!
   1.680 +    // Try to fetch the passphrase first, while we still have control.
   1.681 +    try {
   1.682 +      this.identity.syncKey;
   1.683 +    } catch (ex) {
   1.684 +      this._log.debug("Fetching passphrase threw " + ex +
   1.685 +                      "; assuming master password locked.");
   1.686 +      this.status.login = MASTER_PASSWORD_LOCKED;
   1.687 +      return false;
   1.688 +    }
   1.689 +
   1.690 +    try {
   1.691 +      // Make sure we have a cluster to verify against.
   1.692 +      // This is a little weird, if we don't get a node we pretend
   1.693 +      // to succeed, since that probably means we just don't have storage.
   1.694 +      if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
   1.695 +        this.status.sync = NO_SYNC_NODE_FOUND;
   1.696 +        return true;
   1.697 +      }
   1.698 +
   1.699 +      // Fetch collection info on every startup.
   1.700 +      let test = this.resource(this.infoURL).get();
   1.701 +
   1.702 +      switch (test.status) {
   1.703 +        case 200:
   1.704 +          // The user is authenticated.
   1.705 +
   1.706 +          // We have no way of verifying the passphrase right now,
   1.707 +          // so wait until remoteSetup to do so.
   1.708 +          // Just make the most trivial checks.
   1.709 +          if (!this.identity.syncKey) {
   1.710 +            this._log.warn("No passphrase in verifyLogin.");
   1.711 +            this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
   1.712 +            return false;
   1.713 +          }
   1.714 +
   1.715 +          // Go ahead and do remote setup, so that we can determine
   1.716 +          // conclusively that our passphrase is correct.
   1.717 +          if (this._remoteSetup()) {
   1.718 +            // Username/password verified.
   1.719 +            this.status.login = LOGIN_SUCCEEDED;
   1.720 +            return true;
   1.721 +          }
   1.722 +
   1.723 +          this._log.warn("Remote setup failed.");
   1.724 +          // Remote setup must have failed.
   1.725 +          return false;
   1.726 +
   1.727 +        case 401:
   1.728 +          this._log.warn("401: login failed.");
   1.729 +          // Fall through to the 404 case.
   1.730 +
   1.731 +        case 404:
   1.732 +          // Check that we're verifying with the correct cluster
   1.733 +          if (allow40XRecovery && this._clusterManager.setCluster()) {
   1.734 +            return this.verifyLogin(false);
   1.735 +          }
   1.736 +
   1.737 +          // We must have the right cluster, but the server doesn't expect us
   1.738 +          this.status.login = LOGIN_FAILED_LOGIN_REJECTED;
   1.739 +          return false;
   1.740 +
   1.741 +        default:
   1.742 +          // Server didn't respond with something that we expected
   1.743 +          this.status.login = LOGIN_FAILED_SERVER_ERROR;
   1.744 +          this.errorHandler.checkServerError(test);
   1.745 +          return false;
   1.746 +      }
   1.747 +    } catch (ex) {
   1.748 +      // Must have failed on some network issue
   1.749 +      this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
   1.750 +      this.status.login = LOGIN_FAILED_NETWORK_ERROR;
   1.751 +      this.errorHandler.checkServerError(ex);
   1.752 +      return false;
   1.753 +    }
   1.754 +  },
   1.755 +
   1.756 +  generateNewSymmetricKeys: function generateNewSymmetricKeys() {
   1.757 +    this._log.info("Generating new keys WBO...");
   1.758 +    let wbo = this.collectionKeys.generateNewKeysWBO();
   1.759 +    this._log.info("Encrypting new key bundle.");
   1.760 +    wbo.encrypt(this.identity.syncKeyBundle);
   1.761 +
   1.762 +    this._log.info("Uploading...");
   1.763 +    let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL));
   1.764 +    if (uploadRes.status != 200) {
   1.765 +      this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!");
   1.766 +      this.errorHandler.checkServerError(uploadRes);
   1.767 +      throw new Error("Unable to upload symmetric keys.");
   1.768 +    }
   1.769 +    this._log.info("Got status " + uploadRes.status + " uploading keys.");
   1.770 +    let serverModified = uploadRes.obj;   // Modified timestamp according to server.
   1.771 +    this._log.debug("Server reports crypto modified: " + serverModified);
   1.772 +
   1.773 +    // Now verify that info/collections shows them!
   1.774 +    this._log.debug("Verifying server collection records.");
   1.775 +    let info = this._fetchInfo();
   1.776 +    this._log.debug("info/collections is: " + info);
   1.777 +
   1.778 +    if (info.status != 200) {
   1.779 +      this._log.warn("Non-200 info/collections response. Aborting.");
   1.780 +      throw new Error("Unable to upload symmetric keys.");
   1.781 +    }
   1.782 +
   1.783 +    info = info.obj;
   1.784 +    if (!(CRYPTO_COLLECTION in info)) {
   1.785 +      this._log.error("Consistency failure: info/collections excludes " +
   1.786 +                      "crypto after successful upload.");
   1.787 +      throw new Error("Symmetric key upload failed.");
   1.788 +    }
   1.789 +
   1.790 +    // Can't check against local modified: clock drift.
   1.791 +    if (info[CRYPTO_COLLECTION] < serverModified) {
   1.792 +      this._log.error("Consistency failure: info/collections crypto entry " +
   1.793 +                      "is stale after successful upload.");
   1.794 +      throw new Error("Symmetric key upload failed.");
   1.795 +    }
   1.796 +
   1.797 +    // Doesn't matter if the timestamp is ahead.
   1.798 +
   1.799 +    // Download and install them.
   1.800 +    let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
   1.801 +    let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
   1.802 +    if (cryptoResp.status != 200) {
   1.803 +      this._log.warn("Failed to download keys.");
   1.804 +      throw new Error("Symmetric key download failed.");
   1.805 +    }
   1.806 +    let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
   1.807 +                                             cryptoKeys, true);
   1.808 +    if (keysChanged) {
   1.809 +      this._log.info("Downloaded keys differed, as expected.");
   1.810 +    }
   1.811 +  },
   1.812 +
   1.813 +  changePassword: function changePassword(newPassword) {
   1.814 +    let client = new UserAPI10Client(this.userAPIURI);
   1.815 +    let cb = Async.makeSpinningCallback();
   1.816 +    client.changePassword(this.identity.username,
   1.817 +                          this.identity.basicPassword, newPassword, cb);
   1.818 +
   1.819 +    try {
   1.820 +      cb.wait();
   1.821 +    } catch (ex) {
   1.822 +      this._log.debug("Password change failed: " +
   1.823 +                      CommonUtils.exceptionStr(ex));
   1.824 +      return false;
   1.825 +    }
   1.826 +
   1.827 +    // Save the new password for requests and login manager.
   1.828 +    this.identity.basicPassword = newPassword;
   1.829 +    this.persistLogin();
   1.830 +    return true;
   1.831 +  },
   1.832 +
   1.833 +  changePassphrase: function changePassphrase(newphrase) {
   1.834 +    return this._catch(function doChangePasphrase() {
   1.835 +      /* Wipe. */
   1.836 +      this.wipeServer();
   1.837 +
   1.838 +      this.logout();
   1.839 +
   1.840 +      /* Set this so UI is updated on next run. */
   1.841 +      this.identity.syncKey = newphrase;
   1.842 +      this.persistLogin();
   1.843 +
   1.844 +      /* We need to re-encrypt everything, so reset. */
   1.845 +      this.resetClient();
   1.846 +      this.collectionKeys.clear();
   1.847 +
   1.848 +      /* Login and sync. This also generates new keys. */
   1.849 +      this.sync();
   1.850 +
   1.851 +      Svc.Obs.notify("weave:service:change-passphrase", true);
   1.852 +
   1.853 +      return true;
   1.854 +    })();
   1.855 +  },
   1.856 +
   1.857 +  startOver: function startOver() {
   1.858 +    this._log.trace("Invoking Service.startOver.");
   1.859 +    Svc.Obs.notify("weave:engine:stop-tracking");
   1.860 +    this.status.resetSync();
   1.861 +
   1.862 +    // Deletion doesn't make sense if we aren't set up yet!
   1.863 +    if (this.clusterURL != "") {
   1.864 +      // Clear client-specific data from the server, including disabled engines.
   1.865 +      for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) {
   1.866 +        try {
   1.867 +          engine.removeClientData();
   1.868 +        } catch(ex) {
   1.869 +          this._log.warn("Deleting client data for " + engine.name + " failed:"
   1.870 +                         + Utils.exceptionStr(ex));
   1.871 +        }
   1.872 +      }
   1.873 +      this._log.debug("Finished deleting client data.");
   1.874 +    } else {
   1.875 +      this._log.debug("Skipping client data removal: no cluster URL.");
   1.876 +    }
   1.877 +
   1.878 +    // We want let UI consumers of the following notification know as soon as
   1.879 +    // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
   1.880 +    // by emptying the passphrase (we still need the password).
   1.881 +    this._log.info("Service.startOver dropping sync key and logging out.");
   1.882 +    this.identity.resetSyncKey();
   1.883 +    this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
   1.884 +    this.logout();
   1.885 +    Svc.Obs.notify("weave:service:start-over");
   1.886 +
   1.887 +    // Reset all engines and clear keys.
   1.888 +    this.resetClient();
   1.889 +    this.collectionKeys.clear();
   1.890 +    this.status.resetBackoff();
   1.891 +
   1.892 +    // Reset Weave prefs.
   1.893 +    this._ignorePrefObserver = true;
   1.894 +    Svc.Prefs.resetBranch("");
   1.895 +    this._ignorePrefObserver = false;
   1.896 +
   1.897 +    Svc.Prefs.set("lastversion", WEAVE_VERSION);
   1.898 +
   1.899 +    this.identity.deleteSyncCredentials();
   1.900 +
   1.901 +    // If necessary, reset the identity manager, then re-initialize it so the
   1.902 +    // FxA manager is used.  This is configurable via a pref - mainly for tests.
   1.903 +    let keepIdentity = false;
   1.904 +    try {
   1.905 +      keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
   1.906 +    } catch (_) { /* no such pref */ }
   1.907 +    if (keepIdentity) {
   1.908 +      Svc.Obs.notify("weave:service:start-over:finish");
   1.909 +      return;
   1.910 +    }
   1.911 +
   1.912 +    this.identity.finalize().then(
   1.913 +      () => {
   1.914 +        this.identity.username = "";
   1.915 +        this.status.__authManager = null;
   1.916 +        this.identity = Status._authManager;
   1.917 +        this._clusterManager = this.identity.createClusterManager(this);
   1.918 +        Svc.Obs.notify("weave:service:start-over:finish");
   1.919 +      }
   1.920 +    ).then(null,
   1.921 +      err => {
   1.922 +        this._log.error("startOver failed to re-initialize the identity manager: " + err);
   1.923 +        // Still send the observer notification so the current state is
   1.924 +        // reflected in the UI.
   1.925 +        Svc.Obs.notify("weave:service:start-over:finish");
   1.926 +      }
   1.927 +    );
   1.928 +  },
   1.929 +
   1.930 +  persistLogin: function persistLogin() {
   1.931 +    try {
   1.932 +      this.identity.persistCredentials(true);
   1.933 +    } catch (ex) {
   1.934 +      this._log.info("Unable to persist credentials: " + ex);
   1.935 +    }
   1.936 +  },
   1.937 +
   1.938 +  login: function login(username, password, passphrase) {
   1.939 +    function onNotify() {
   1.940 +      this._loggedIn = false;
   1.941 +      if (Services.io.offline) {
   1.942 +        this.status.login = LOGIN_FAILED_NETWORK_ERROR;
   1.943 +        throw "Application is offline, login should not be called";
   1.944 +      }
   1.945 +
   1.946 +      let initialStatus = this._checkSetup();
   1.947 +      if (username) {
   1.948 +        this.identity.username = username;
   1.949 +      }
   1.950 +      if (password) {
   1.951 +        this.identity.basicPassword = password;
   1.952 +      }
   1.953 +      if (passphrase) {
   1.954 +        this.identity.syncKey = passphrase;
   1.955 +      }
   1.956 +
   1.957 +      if (this._checkSetup() == CLIENT_NOT_CONFIGURED) {
   1.958 +        throw "Aborting login, client not configured.";
   1.959 +      }
   1.960 +
   1.961 +      // Ask the identity manager to explicitly login now.
   1.962 +      let cb = Async.makeSpinningCallback();
   1.963 +      this.identity.ensureLoggedIn().then(cb, cb);
   1.964 +
   1.965 +      // Just let any errors bubble up - they've more context than we do!
   1.966 +      cb.wait();
   1.967 +
   1.968 +      // Calling login() with parameters when the client was
   1.969 +      // previously not configured means setup was completed.
   1.970 +      if (initialStatus == CLIENT_NOT_CONFIGURED
   1.971 +          && (username || password || passphrase)) {
   1.972 +        Svc.Obs.notify("weave:service:setup-complete");
   1.973 +      }
   1.974 +      this._log.info("Logging in the user.");
   1.975 +      this._updateCachedURLs();
   1.976 +
   1.977 +      if (!this.verifyLogin()) {
   1.978 +        // verifyLogin sets the failure states here.
   1.979 +        throw "Login failed: " + this.status.login;
   1.980 +      }
   1.981 +
   1.982 +      this._loggedIn = true;
   1.983 +
   1.984 +      return true;
   1.985 +    }
   1.986 +
   1.987 +    let notifier = this._notify("login", "", onNotify.bind(this));
   1.988 +    return this._catch(this._lock("service.js: login", notifier))();
   1.989 +  },
   1.990 +
   1.991 +  logout: function logout() {
   1.992 +    // If we failed during login, we aren't going to have this._loggedIn set,
   1.993 +    // but we still want to ask the identity to logout, so it doesn't try and
   1.994 +    // reuse any old credentials next time we sync.
   1.995 +    this._log.info("Logging out");
   1.996 +    this.identity.logout();
   1.997 +    this._loggedIn = false;
   1.998 +
   1.999 +    Svc.Obs.notify("weave:service:logout:finish");
  1.1000 +  },
  1.1001 +
  1.1002 +  checkAccount: function checkAccount(account) {
  1.1003 +    let client = new UserAPI10Client(this.userAPIURI);
  1.1004 +    let cb = Async.makeSpinningCallback();
  1.1005 +
  1.1006 +    let username = this.identity.usernameFromAccount(account);
  1.1007 +    client.usernameExists(username, cb);
  1.1008 +
  1.1009 +    try {
  1.1010 +      let exists = cb.wait();
  1.1011 +      return exists ? "notAvailable" : "available";
  1.1012 +    } catch (ex) {
  1.1013 +      // TODO fix API convention.
  1.1014 +      return this.errorHandler.errorStr(ex);
  1.1015 +    }
  1.1016 +  },
  1.1017 +
  1.1018 +  createAccount: function createAccount(email, password,
  1.1019 +                                        captchaChallenge, captchaResponse) {
  1.1020 +    let client = new UserAPI10Client(this.userAPIURI);
  1.1021 +
  1.1022 +    // Hint to server to allow scripted user creation or otherwise
  1.1023 +    // ignore captcha.
  1.1024 +    if (Svc.Prefs.isSet("admin-secret")) {
  1.1025 +      client.adminSecret = Svc.Prefs.get("admin-secret", "");
  1.1026 +    }
  1.1027 +
  1.1028 +    let cb = Async.makeSpinningCallback();
  1.1029 +
  1.1030 +    client.createAccount(email, password, captchaChallenge, captchaResponse,
  1.1031 +                         cb);
  1.1032 +
  1.1033 +    try {
  1.1034 +      cb.wait();
  1.1035 +      return null;
  1.1036 +    } catch (ex) {
  1.1037 +      return this.errorHandler.errorStr(ex.body);
  1.1038 +    }
  1.1039 +  },
  1.1040 +
  1.1041 +  // Stuff we need to do after login, before we can really do
  1.1042 +  // anything (e.g. key setup).
  1.1043 +  _remoteSetup: function _remoteSetup(infoResponse) {
  1.1044 +    let reset = false;
  1.1045 +
  1.1046 +    this._log.debug("Fetching global metadata record");
  1.1047 +    let meta = this.recordManager.get(this.metaURL);
  1.1048 +
  1.1049 +    // Checking modified time of the meta record.
  1.1050 +    if (infoResponse &&
  1.1051 +        (infoResponse.obj.meta != this.metaModified) &&
  1.1052 +        (!meta || !meta.isNew)) {
  1.1053 +
  1.1054 +      // Delete the cached meta record...
  1.1055 +      this._log.debug("Clearing cached meta record. metaModified is " +
  1.1056 +          JSON.stringify(this.metaModified) + ", setting to " +
  1.1057 +          JSON.stringify(infoResponse.obj.meta));
  1.1058 +
  1.1059 +      this.recordManager.del(this.metaURL);
  1.1060 +
  1.1061 +      // ... fetch the current record from the server, and COPY THE FLAGS.
  1.1062 +      let newMeta = this.recordManager.get(this.metaURL);
  1.1063 +
  1.1064 +      // If we got a 401, we do not want to create a new meta/global - we
  1.1065 +      // should be able to get the existing meta after we get a new node.
  1.1066 +      if (this.recordManager.response.status == 401) {
  1.1067 +        this._log.debug("Fetching meta/global record on the server returned 401.");
  1.1068 +        this.errorHandler.checkServerError(this.recordManager.response);
  1.1069 +        return false;
  1.1070 +      }
  1.1071 +
  1.1072 +      if (!this.recordManager.response.success || !newMeta) {
  1.1073 +        this._log.debug("No meta/global record on the server. Creating one.");
  1.1074 +        newMeta = new WBORecord("meta", "global");
  1.1075 +        newMeta.payload.syncID = this.syncID;
  1.1076 +        newMeta.payload.storageVersion = STORAGE_VERSION;
  1.1077 +        newMeta.payload.declined = this.engineManager.getDeclined();
  1.1078 +
  1.1079 +        newMeta.isNew = true;
  1.1080 +
  1.1081 +        this.recordManager.set(this.metaURL, newMeta);
  1.1082 +        if (!newMeta.upload(this.resource(this.metaURL)).success) {
  1.1083 +          this._log.warn("Unable to upload new meta/global. Failing remote setup.");
  1.1084 +          return false;
  1.1085 +        }
  1.1086 +      } else {
  1.1087 +        // If newMeta, then it stands to reason that meta != null.
  1.1088 +        newMeta.isNew   = meta.isNew;
  1.1089 +        newMeta.changed = meta.changed;
  1.1090 +      }
  1.1091 +
  1.1092 +      // Switch in the new meta object and record the new time.
  1.1093 +      meta              = newMeta;
  1.1094 +      this.metaModified = infoResponse.obj.meta;
  1.1095 +    }
  1.1096 +
  1.1097 +    let remoteVersion = (meta && meta.payload.storageVersion)?
  1.1098 +      meta.payload.storageVersion : "";
  1.1099 +
  1.1100 +    this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:",
  1.1101 +      STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" "));
  1.1102 +
  1.1103 +    // Check for cases that require a fresh start. When comparing remoteVersion,
  1.1104 +    // we need to convert it to a number as older clients used it as a string.
  1.1105 +    if (!meta || !meta.payload.storageVersion || !meta.payload.syncID ||
  1.1106 +        STORAGE_VERSION > parseFloat(remoteVersion)) {
  1.1107 +
  1.1108 +      this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed.");
  1.1109 +
  1.1110 +      // abort the server wipe if the GET status was anything other than 404 or 200
  1.1111 +      let status = this.recordManager.response.status;
  1.1112 +      if (status != 200 && status != 404) {
  1.1113 +        this.status.sync = METARECORD_DOWNLOAD_FAIL;
  1.1114 +        this.errorHandler.checkServerError(this.recordManager.response);
  1.1115 +        this._log.warn("Unknown error while downloading metadata record. " +
  1.1116 +                       "Aborting sync.");
  1.1117 +        return false;
  1.1118 +      }
  1.1119 +
  1.1120 +      if (!meta)
  1.1121 +        this._log.info("No metadata record, server wipe needed");
  1.1122 +      if (meta && !meta.payload.syncID)
  1.1123 +        this._log.warn("No sync id, server wipe needed");
  1.1124 +
  1.1125 +      reset = true;
  1.1126 +
  1.1127 +      this._log.info("Wiping server data");
  1.1128 +      this._freshStart();
  1.1129 +
  1.1130 +      if (status == 404)
  1.1131 +        this._log.info("Metadata record not found, server was wiped to ensure " +
  1.1132 +                       "consistency.");
  1.1133 +      else // 200
  1.1134 +        this._log.info("Wiped server; incompatible metadata: " + remoteVersion);
  1.1135 +
  1.1136 +      return true;
  1.1137 +    }
  1.1138 +    else if (remoteVersion > STORAGE_VERSION) {
  1.1139 +      this.status.sync = VERSION_OUT_OF_DATE;
  1.1140 +      this._log.warn("Upgrade required to access newer storage version.");
  1.1141 +      return false;
  1.1142 +    }
  1.1143 +    else if (meta.payload.syncID != this.syncID) {
  1.1144 +
  1.1145 +      this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID);
  1.1146 +      this.resetClient();
  1.1147 +      this.collectionKeys.clear();
  1.1148 +      this.syncID = meta.payload.syncID;
  1.1149 +      this._log.debug("Clear cached values and take syncId: " + this.syncID);
  1.1150 +
  1.1151 +      if (!this.upgradeSyncKey(meta.payload.syncID)) {
  1.1152 +        this._log.warn("Failed to upgrade sync key. Failing remote setup.");
  1.1153 +        return false;
  1.1154 +      }
  1.1155 +
  1.1156 +      if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
  1.1157 +        this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
  1.1158 +        return false;
  1.1159 +      }
  1.1160 +
  1.1161 +      // bug 545725 - re-verify creds and fail sanely
  1.1162 +      if (!this.verifyLogin()) {
  1.1163 +        this.status.sync = CREDENTIALS_CHANGED;
  1.1164 +        this._log.info("Credentials have changed, aborting sync and forcing re-login.");
  1.1165 +        return false;
  1.1166 +      }
  1.1167 +
  1.1168 +      return true;
  1.1169 +    }
  1.1170 +    else {
  1.1171 +      if (!this.upgradeSyncKey(meta.payload.syncID)) {
  1.1172 +        this._log.warn("Failed to upgrade sync key. Failing remote setup.");
  1.1173 +        return false;
  1.1174 +      }
  1.1175 +
  1.1176 +      if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
  1.1177 +        this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
  1.1178 +        return false;
  1.1179 +      }
  1.1180 +
  1.1181 +      return true;
  1.1182 +    }
  1.1183 +  },
  1.1184 +
  1.1185 +  /**
  1.1186 +   * Return whether we should attempt login at the start of a sync.
  1.1187 +   *
  1.1188 +   * Note that this function has strong ties to _checkSync: callers
  1.1189 +   * of this function should typically use _checkSync to verify that
  1.1190 +   * any necessary login took place.
  1.1191 +   */
  1.1192 +  _shouldLogin: function _shouldLogin() {
  1.1193 +    return this.enabled &&
  1.1194 +           !Services.io.offline &&
  1.1195 +           !this.isLoggedIn;
  1.1196 +  },
  1.1197 +
  1.1198 +  /**
  1.1199 +   * Determine if a sync should run.
  1.1200 +   *
  1.1201 +   * @param ignore [optional]
  1.1202 +   *        array of reasons to ignore when checking
  1.1203 +   *
  1.1204 +   * @return Reason for not syncing; not-truthy if sync should run
  1.1205 +   */
  1.1206 +  _checkSync: function _checkSync(ignore) {
  1.1207 +    let reason = "";
  1.1208 +    if (!this.enabled)
  1.1209 +      reason = kSyncWeaveDisabled;
  1.1210 +    else if (Services.io.offline)
  1.1211 +      reason = kSyncNetworkOffline;
  1.1212 +    else if (this.status.minimumNextSync > Date.now())
  1.1213 +      reason = kSyncBackoffNotMet;
  1.1214 +    else if ((this.status.login == MASTER_PASSWORD_LOCKED) &&
  1.1215 +             Utils.mpLocked())
  1.1216 +      reason = kSyncMasterPasswordLocked;
  1.1217 +    else if (Svc.Prefs.get("firstSync") == "notReady")
  1.1218 +      reason = kFirstSyncChoiceNotMade;
  1.1219 +
  1.1220 +    if (ignore && ignore.indexOf(reason) != -1)
  1.1221 +      return "";
  1.1222 +
  1.1223 +    return reason;
  1.1224 +  },
  1.1225 +
  1.1226 +  sync: function sync() {
  1.1227 +    let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
  1.1228 +    this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent);
  1.1229 +    this._log.info("Starting sync at " + dateStr);
  1.1230 +    this._catch(function () {
  1.1231 +      // Make sure we're logged in.
  1.1232 +      if (this._shouldLogin()) {
  1.1233 +        this._log.debug("In sync: should login.");
  1.1234 +        if (!this.login()) {
  1.1235 +          this._log.debug("Not syncing: login returned false.");
  1.1236 +          return;
  1.1237 +        }
  1.1238 +      }
  1.1239 +      else {
  1.1240 +        this._log.trace("In sync: no need to login.");
  1.1241 +      }
  1.1242 +      return this._lockedSync.apply(this, arguments);
  1.1243 +    })();
  1.1244 +  },
  1.1245 +
  1.1246 +  /**
  1.1247 +   * Sync up engines with the server.
  1.1248 +   */
  1.1249 +  _lockedSync: function _lockedSync() {
  1.1250 +    return this._lock("service.js: sync",
  1.1251 +                      this._notify("sync", "", function onNotify() {
  1.1252 +
  1.1253 +      let synchronizer = new EngineSynchronizer(this);
  1.1254 +      let cb = Async.makeSpinningCallback();
  1.1255 +      synchronizer.onComplete = cb;
  1.1256 +
  1.1257 +      synchronizer.sync();
  1.1258 +      // wait() throws if the first argument is truthy, which is exactly what
  1.1259 +      // we want.
  1.1260 +      let result = cb.wait();
  1.1261 +
  1.1262 +      // We successfully synchronized. Now let's update our declined engines.
  1.1263 +      let meta = this.recordManager.get(this.metaURL);
  1.1264 +      if (!meta) {
  1.1265 +        this._log.warn("No meta/global; can't update declined state.");
  1.1266 +        return;
  1.1267 +      }
  1.1268 +
  1.1269 +      let declinedEngines = new DeclinedEngines(this);
  1.1270 +      let didChange = declinedEngines.updateDeclined(meta, this.engineManager);
  1.1271 +      if (!didChange) {
  1.1272 +        this._log.info("No change to declined engines. Not reuploading meta/global.");
  1.1273 +        return;
  1.1274 +      }
  1.1275 +
  1.1276 +      this.uploadMetaGlobal(meta);
  1.1277 +    }))();
  1.1278 +  },
  1.1279 +
  1.1280 +  /**
  1.1281 +   * Upload meta/global, throwing the response on failure.
  1.1282 +   */
  1.1283 +  uploadMetaGlobal: function (meta) {
  1.1284 +    this._log.debug("Uploading meta/global: " + JSON.stringify(meta));
  1.1285 +
  1.1286 +    // It would be good to set the X-If-Unmodified-Since header to `timestamp`
  1.1287 +    // for this PUT to ensure at least some level of transactionality.
  1.1288 +    // Unfortunately, the servers don't support it after a wipe right now
  1.1289 +    // (bug 693893), so we're going to defer this until bug 692700.
  1.1290 +    let res = this.resource(this.metaURL);
  1.1291 +    let response = res.put(meta);
  1.1292 +    if (!response.success) {
  1.1293 +      throw response;
  1.1294 +    }
  1.1295 +    this.recordManager.set(this.metaURL, meta);
  1.1296 +  },
  1.1297 +
  1.1298 +  /**
  1.1299 +   * If we have a passphrase, rather than a 25-alphadigit sync key,
  1.1300 +   * use the provided sync ID to bootstrap it using PBKDF2.
  1.1301 +   *
  1.1302 +   * Store the new 'passphrase' back into the identity manager.
  1.1303 +   *
  1.1304 +   * We can check this as often as we want, because once it's done the
  1.1305 +   * check will no longer succeed. It only matters that it happens after
  1.1306 +   * we decide to bump the server storage version.
  1.1307 +   */
  1.1308 +  upgradeSyncKey: function upgradeSyncKey(syncID) {
  1.1309 +    let p = this.identity.syncKey;
  1.1310 +
  1.1311 +    if (!p) {
  1.1312 +      return false;
  1.1313 +    }
  1.1314 +
  1.1315 +    // Check whether it's already a key that we generated.
  1.1316 +    if (Utils.isPassphrase(p)) {
  1.1317 +      this._log.info("Sync key is up-to-date: no need to upgrade.");
  1.1318 +      return true;
  1.1319 +    }
  1.1320 +
  1.1321 +    // Otherwise, let's upgrade it.
  1.1322 +    // N.B., we persist the sync key without testing it first...
  1.1323 +
  1.1324 +    let s = btoa(syncID);        // It's what WeaveCrypto expects. *sigh*
  1.1325 +    let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES);   // Base 32.
  1.1326 +
  1.1327 +    if (!k) {
  1.1328 +      this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade.");
  1.1329 +      return false;
  1.1330 +    }
  1.1331 +
  1.1332 +    this._log.info("Upgrading sync key...");
  1.1333 +    this.identity.syncKey = k;
  1.1334 +    this._log.info("Saving upgraded sync key...");
  1.1335 +    this.persistLogin();
  1.1336 +    this._log.info("Done saving.");
  1.1337 +    return true;
  1.1338 +  },
  1.1339 +
  1.1340 +  _freshStart: function _freshStart() {
  1.1341 +    this._log.info("Fresh start. Resetting client and considering key upgrade.");
  1.1342 +    this.resetClient();
  1.1343 +    this.collectionKeys.clear();
  1.1344 +    this.upgradeSyncKey(this.syncID);
  1.1345 +
  1.1346 +    // Wipe the server.
  1.1347 +    let wipeTimestamp = this.wipeServer();
  1.1348 +
  1.1349 +    // Upload a new meta/global record.
  1.1350 +    let meta = new WBORecord("meta", "global");
  1.1351 +    meta.payload.syncID = this.syncID;
  1.1352 +    meta.payload.storageVersion = STORAGE_VERSION;
  1.1353 +    meta.payload.declined = this.engineManager.getDeclined();
  1.1354 +    meta.isNew = true;
  1.1355 +
  1.1356 +    // uploadMetaGlobal throws on failure -- including race conditions.
  1.1357 +    // If we got into a race condition, we'll abort the sync this way, too.
  1.1358 +    // That's fine. We'll just wait till the next sync. The client that we're
  1.1359 +    // racing is probably busy uploading stuff right now anyway.
  1.1360 +    this.uploadMetaGlobal(meta);
  1.1361 +
  1.1362 +    // Wipe everything we know about except meta because we just uploaded it
  1.1363 +    let engines = [this.clientsEngine].concat(this.engineManager.getAll());
  1.1364 +    let collections = [engine.name for each (engine in engines)];
  1.1365 +    // TODO: there's a bug here. We should be calling resetClient, no?
  1.1366 +
  1.1367 +    // Generate, upload, and download new keys. Do this last so we don't wipe
  1.1368 +    // them...
  1.1369 +    this.generateNewSymmetricKeys();
  1.1370 +  },
  1.1371 +
  1.1372 +  /**
  1.1373 +   * Wipe user data from the server.
  1.1374 +   *
  1.1375 +   * @param collections [optional]
  1.1376 +   *        Array of collections to wipe. If not given, all collections are
  1.1377 +   *        wiped by issuing a DELETE request for `storageURL`.
  1.1378 +   *
  1.1379 +   * @return the server's timestamp of the (last) DELETE.
  1.1380 +   */
  1.1381 +  wipeServer: function wipeServer(collections) {
  1.1382 +    let response;
  1.1383 +    if (!collections) {
  1.1384 +      // Strip the trailing slash.
  1.1385 +      let res = this.resource(this.storageURL.slice(0, -1));
  1.1386 +      res.setHeader("X-Confirm-Delete", "1");
  1.1387 +      try {
  1.1388 +        response = res.delete();
  1.1389 +      } catch (ex) {
  1.1390 +        this._log.debug("Failed to wipe server: " + CommonUtils.exceptionStr(ex));
  1.1391 +        throw ex;
  1.1392 +      }
  1.1393 +      if (response.status != 200 && response.status != 404) {
  1.1394 +        this._log.debug("Aborting wipeServer. Server responded with " +
  1.1395 +                        response.status + " response for " + this.storageURL);
  1.1396 +        throw response;
  1.1397 +      }
  1.1398 +      return response.headers["x-weave-timestamp"];
  1.1399 +    }
  1.1400 +
  1.1401 +    let timestamp;
  1.1402 +    for (let name of collections) {
  1.1403 +      let url = this.storageURL + name;
  1.1404 +      try {
  1.1405 +        response = this.resource(url).delete();
  1.1406 +      } catch (ex) {
  1.1407 +        this._log.debug("Failed to wipe '" + name + "' collection: " +
  1.1408 +                        Utils.exceptionStr(ex));
  1.1409 +        throw ex;
  1.1410 +      }
  1.1411 +
  1.1412 +      if (response.status != 200 && response.status != 404) {
  1.1413 +        this._log.debug("Aborting wipeServer. Server responded with " +
  1.1414 +                        response.status + " response for " + url);
  1.1415 +        throw response;
  1.1416 +      }
  1.1417 +
  1.1418 +      if ("x-weave-timestamp" in response.headers) {
  1.1419 +        timestamp = response.headers["x-weave-timestamp"];
  1.1420 +      }
  1.1421 +    }
  1.1422 +
  1.1423 +    return timestamp;
  1.1424 +  },
  1.1425 +
  1.1426 +  /**
  1.1427 +   * Wipe all local user data.
  1.1428 +   *
  1.1429 +   * @param engines [optional]
  1.1430 +   *        Array of engine names to wipe. If not given, all engines are used.
  1.1431 +   */
  1.1432 +  wipeClient: function wipeClient(engines) {
  1.1433 +    // If we don't have any engines, reset the service and wipe all engines
  1.1434 +    if (!engines) {
  1.1435 +      // Clear out any service data
  1.1436 +      this.resetService();
  1.1437 +
  1.1438 +      engines = [this.clientsEngine].concat(this.engineManager.getAll());
  1.1439 +    }
  1.1440 +    // Convert the array of names into engines
  1.1441 +    else {
  1.1442 +      engines = this.engineManager.get(engines);
  1.1443 +    }
  1.1444 +
  1.1445 +    // Fully wipe each engine if it's able to decrypt data
  1.1446 +    for each (let engine in engines) {
  1.1447 +      if (engine.canDecrypt()) {
  1.1448 +        engine.wipeClient();
  1.1449 +      }
  1.1450 +    }
  1.1451 +
  1.1452 +    // Save the password/passphrase just in-case they aren't restored by sync
  1.1453 +    this.persistLogin();
  1.1454 +  },
  1.1455 +
  1.1456 +  /**
  1.1457 +   * Wipe all remote user data by wiping the server then telling each remote
  1.1458 +   * client to wipe itself.
  1.1459 +   *
  1.1460 +   * @param engines [optional]
  1.1461 +   *        Array of engine names to wipe. If not given, all engines are used.
  1.1462 +   */
  1.1463 +  wipeRemote: function wipeRemote(engines) {
  1.1464 +    try {
  1.1465 +      // Make sure stuff gets uploaded.
  1.1466 +      this.resetClient(engines);
  1.1467 +
  1.1468 +      // Clear out any server data.
  1.1469 +      this.wipeServer(engines);
  1.1470 +
  1.1471 +      // Only wipe the engines provided.
  1.1472 +      if (engines) {
  1.1473 +        engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this);
  1.1474 +      }
  1.1475 +      // Tell the remote machines to wipe themselves.
  1.1476 +      else {
  1.1477 +        this.clientsEngine.sendCommand("wipeAll", []);
  1.1478 +      }
  1.1479 +
  1.1480 +      // Make sure the changed clients get updated.
  1.1481 +      this.clientsEngine.sync();
  1.1482 +    } catch (ex) {
  1.1483 +      this.errorHandler.checkServerError(ex);
  1.1484 +      throw ex;
  1.1485 +    }
  1.1486 +  },
  1.1487 +
  1.1488 +  /**
  1.1489 +   * Reset local service information like logs, sync times, caches.
  1.1490 +   */
  1.1491 +  resetService: function resetService() {
  1.1492 +    this._catch(function reset() {
  1.1493 +      this._log.info("Service reset.");
  1.1494 +
  1.1495 +      // Pretend we've never synced to the server and drop cached data
  1.1496 +      this.syncID = "";
  1.1497 +      this.recordManager.clearCache();
  1.1498 +    })();
  1.1499 +  },
  1.1500 +
  1.1501 +  /**
  1.1502 +   * Reset the client by getting rid of any local server data and client data.
  1.1503 +   *
  1.1504 +   * @param engines [optional]
  1.1505 +   *        Array of engine names to reset. If not given, all engines are used.
  1.1506 +   */
  1.1507 +  resetClient: function resetClient(engines) {
  1.1508 +    this._catch(function doResetClient() {
  1.1509 +      // If we don't have any engines, reset everything including the service
  1.1510 +      if (!engines) {
  1.1511 +        // Clear out any service data
  1.1512 +        this.resetService();
  1.1513 +
  1.1514 +        engines = [this.clientsEngine].concat(this.engineManager.getAll());
  1.1515 +      }
  1.1516 +      // Convert the array of names into engines
  1.1517 +      else {
  1.1518 +        engines = this.engineManager.get(engines);
  1.1519 +      }
  1.1520 +
  1.1521 +      // Have each engine drop any temporary meta data
  1.1522 +      for each (let engine in engines) {
  1.1523 +        engine.resetClient();
  1.1524 +      }
  1.1525 +    })();
  1.1526 +  },
  1.1527 +
  1.1528 +  /**
  1.1529 +   * Fetch storage info from the server.
  1.1530 +   *
  1.1531 +   * @param type
  1.1532 +   *        String specifying what info to fetch from the server. Must be one
  1.1533 +   *        of the INFO_* values. See Sync Storage Server API spec for details.
  1.1534 +   * @param callback
  1.1535 +   *        Callback function with signature (error, data) where `data' is
  1.1536 +   *        the return value from the server already parsed as JSON.
  1.1537 +   *
  1.1538 +   * @return RESTRequest instance representing the request, allowing callers
  1.1539 +   *         to cancel the request.
  1.1540 +   */
  1.1541 +  getStorageInfo: function getStorageInfo(type, callback) {
  1.1542 +    if (STORAGE_INFO_TYPES.indexOf(type) == -1) {
  1.1543 +      throw "Invalid value for 'type': " + type;
  1.1544 +    }
  1.1545 +
  1.1546 +    let info_type = "info/" + type;
  1.1547 +    this._log.trace("Retrieving '" + info_type + "'...");
  1.1548 +    let url = this.userBaseURL + info_type;
  1.1549 +    return this.getStorageRequest(url).get(function onComplete(error) {
  1.1550 +      // Note: 'this' is the request.
  1.1551 +      if (error) {
  1.1552 +        this._log.debug("Failed to retrieve '" + info_type + "': " +
  1.1553 +                        Utils.exceptionStr(error));
  1.1554 +        return callback(error);
  1.1555 +      }
  1.1556 +      if (this.response.status != 200) {
  1.1557 +        this._log.debug("Failed to retrieve '" + info_type +
  1.1558 +                        "': server responded with HTTP" +
  1.1559 +                        this.response.status);
  1.1560 +        return callback(this.response);
  1.1561 +      }
  1.1562 +
  1.1563 +      let result;
  1.1564 +      try {
  1.1565 +        result = JSON.parse(this.response.body);
  1.1566 +      } catch (ex) {
  1.1567 +        this._log.debug("Server returned invalid JSON for '" + info_type +
  1.1568 +                        "': " + this.response.body);
  1.1569 +        return callback(ex);
  1.1570 +      }
  1.1571 +      this._log.trace("Successfully retrieved '" + info_type + "'.");
  1.1572 +      return callback(null, result);
  1.1573 +    });
  1.1574 +  },
  1.1575 +};
  1.1576 +
  1.1577 +this.Service = new Sync11Service();
  1.1578 +Service.onStartup();

mercurial