services/sync/modules/service.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = ["Service"];
michael@0 6
michael@0 7 const Cc = Components.classes;
michael@0 8 const Ci = Components.interfaces;
michael@0 9 const Cr = Components.results;
michael@0 10 const Cu = Components.utils;
michael@0 11
michael@0 12 // How long before refreshing the cluster
michael@0 13 const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes
michael@0 14
michael@0 15 // How long a key to generate from an old passphrase.
michael@0 16 const PBKDF2_KEY_BYTES = 16;
michael@0 17
michael@0 18 const CRYPTO_COLLECTION = "crypto";
michael@0 19 const KEYS_WBO = "keys";
michael@0 20
michael@0 21 Cu.import("resource://gre/modules/Preferences.jsm");
michael@0 22 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 23 Cu.import("resource://gre/modules/Log.jsm");
michael@0 24 Cu.import("resource://services-common/utils.js");
michael@0 25 Cu.import("resource://services-sync/constants.js");
michael@0 26 Cu.import("resource://services-sync/engines.js");
michael@0 27 Cu.import("resource://services-sync/engines/clients.js");
michael@0 28 Cu.import("resource://services-sync/identity.js");
michael@0 29 Cu.import("resource://services-sync/policies.js");
michael@0 30 Cu.import("resource://services-sync/record.js");
michael@0 31 Cu.import("resource://services-sync/resource.js");
michael@0 32 Cu.import("resource://services-sync/rest.js");
michael@0 33 Cu.import("resource://services-sync/stages/enginesync.js");
michael@0 34 Cu.import("resource://services-sync/stages/declined.js");
michael@0 35 Cu.import("resource://services-sync/status.js");
michael@0 36 Cu.import("resource://services-sync/userapi.js");
michael@0 37 Cu.import("resource://services-sync/util.js");
michael@0 38
michael@0 39 const ENGINE_MODULES = {
michael@0 40 Addons: "addons.js",
michael@0 41 Bookmarks: "bookmarks.js",
michael@0 42 Form: "forms.js",
michael@0 43 History: "history.js",
michael@0 44 Password: "passwords.js",
michael@0 45 Prefs: "prefs.js",
michael@0 46 Tab: "tabs.js",
michael@0 47 };
michael@0 48
michael@0 49 const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
michael@0 50 INFO_COLLECTION_USAGE,
michael@0 51 INFO_COLLECTION_COUNTS,
michael@0 52 INFO_QUOTA];
michael@0 53
michael@0 54
michael@0 55 function Sync11Service() {
michael@0 56 this._notify = Utils.notify("weave:service:");
michael@0 57 }
michael@0 58 Sync11Service.prototype = {
michael@0 59
michael@0 60 _lock: Utils.lock,
michael@0 61 _locked: false,
michael@0 62 _loggedIn: false,
michael@0 63
michael@0 64 infoURL: null,
michael@0 65 storageURL: null,
michael@0 66 metaURL: null,
michael@0 67 cryptoKeyURL: null,
michael@0 68
michael@0 69 get serverURL() Svc.Prefs.get("serverURL"),
michael@0 70 set serverURL(value) {
michael@0 71 if (!value.endsWith("/")) {
michael@0 72 value += "/";
michael@0 73 }
michael@0 74
michael@0 75 // Only do work if it's actually changing
michael@0 76 if (value == this.serverURL)
michael@0 77 return;
michael@0 78
michael@0 79 // A new server most likely uses a different cluster, so clear that
michael@0 80 Svc.Prefs.set("serverURL", value);
michael@0 81 Svc.Prefs.reset("clusterURL");
michael@0 82 },
michael@0 83
michael@0 84 get clusterURL() Svc.Prefs.get("clusterURL", ""),
michael@0 85 set clusterURL(value) {
michael@0 86 Svc.Prefs.set("clusterURL", value);
michael@0 87 this._updateCachedURLs();
michael@0 88 },
michael@0 89
michael@0 90 get miscAPI() {
michael@0 91 // Append to the serverURL if it's a relative fragment
michael@0 92 let misc = Svc.Prefs.get("miscURL");
michael@0 93 if (misc.indexOf(":") == -1)
michael@0 94 misc = this.serverURL + misc;
michael@0 95 return misc + MISC_API_VERSION + "/";
michael@0 96 },
michael@0 97
michael@0 98 /**
michael@0 99 * The URI of the User API service.
michael@0 100 *
michael@0 101 * This is the base URI of the service as applicable to all users up to
michael@0 102 * and including the server version path component, complete with trailing
michael@0 103 * forward slash.
michael@0 104 */
michael@0 105 get userAPIURI() {
michael@0 106 // Append to the serverURL if it's a relative fragment.
michael@0 107 let url = Svc.Prefs.get("userURL");
michael@0 108 if (!url.contains(":")) {
michael@0 109 url = this.serverURL + url;
michael@0 110 }
michael@0 111
michael@0 112 return url + USER_API_VERSION + "/";
michael@0 113 },
michael@0 114
michael@0 115 get pwResetURL() {
michael@0 116 return this.serverURL + "weave-password-reset";
michael@0 117 },
michael@0 118
michael@0 119 get syncID() {
michael@0 120 // Generate a random syncID id we don't have one
michael@0 121 let syncID = Svc.Prefs.get("client.syncID", "");
michael@0 122 return syncID == "" ? this.syncID = Utils.makeGUID() : syncID;
michael@0 123 },
michael@0 124 set syncID(value) {
michael@0 125 Svc.Prefs.set("client.syncID", value);
michael@0 126 },
michael@0 127
michael@0 128 get isLoggedIn() { return this._loggedIn; },
michael@0 129
michael@0 130 get locked() { return this._locked; },
michael@0 131 lock: function lock() {
michael@0 132 if (this._locked)
michael@0 133 return false;
michael@0 134 this._locked = true;
michael@0 135 return true;
michael@0 136 },
michael@0 137 unlock: function unlock() {
michael@0 138 this._locked = false;
michael@0 139 },
michael@0 140
michael@0 141 // A specialized variant of Utils.catch.
michael@0 142 // This provides a more informative error message when we're already syncing:
michael@0 143 // see Bug 616568.
michael@0 144 _catch: function _catch(func) {
michael@0 145 function lockExceptions(ex) {
michael@0 146 if (Utils.isLockException(ex)) {
michael@0 147 // This only happens if we're syncing already.
michael@0 148 this._log.info("Cannot start sync: already syncing?");
michael@0 149 }
michael@0 150 }
michael@0 151
michael@0 152 return Utils.catch.call(this, func, lockExceptions);
michael@0 153 },
michael@0 154
michael@0 155 get userBaseURL() {
michael@0 156 if (!this._clusterManager) {
michael@0 157 return null;
michael@0 158 }
michael@0 159 return this._clusterManager.getUserBaseURL();
michael@0 160 },
michael@0 161
michael@0 162 _updateCachedURLs: function _updateCachedURLs() {
michael@0 163 // Nothing to cache yet if we don't have the building blocks
michael@0 164 if (!this.clusterURL || !this.identity.username)
michael@0 165 return;
michael@0 166
michael@0 167 this._log.debug("Caching URLs under storage user base: " + this.userBaseURL);
michael@0 168
michael@0 169 // Generate and cache various URLs under the storage API for this user
michael@0 170 this.infoURL = this.userBaseURL + "info/collections";
michael@0 171 this.storageURL = this.userBaseURL + "storage/";
michael@0 172 this.metaURL = this.storageURL + "meta/global";
michael@0 173 this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO;
michael@0 174 },
michael@0 175
michael@0 176 _checkCrypto: function _checkCrypto() {
michael@0 177 let ok = false;
michael@0 178
michael@0 179 try {
michael@0 180 let iv = Svc.Crypto.generateRandomIV();
michael@0 181 if (iv.length == 24)
michael@0 182 ok = true;
michael@0 183
michael@0 184 } catch (e) {
michael@0 185 this._log.debug("Crypto check failed: " + e);
michael@0 186 }
michael@0 187
michael@0 188 return ok;
michael@0 189 },
michael@0 190
michael@0 191 /**
michael@0 192 * Here is a disgusting yet reasonable way of handling HMAC errors deep in
michael@0 193 * the guts of Sync. The astute reader will note that this is a hacky way of
michael@0 194 * implementing something like continuable conditions.
michael@0 195 *
michael@0 196 * A handler function is glued to each engine. If the engine discovers an
michael@0 197 * HMAC failure, we fetch keys from the server and update our keys, just as
michael@0 198 * we would on startup.
michael@0 199 *
michael@0 200 * If our key collection changed, we signal to the engine (via our return
michael@0 201 * value) that it should retry decryption.
michael@0 202 *
michael@0 203 * If our key collection did not change, it means that we already had the
michael@0 204 * correct keys... and thus a different client has the wrong ones. Reupload
michael@0 205 * the bundle that we fetched, which will bump the modified time on the
michael@0 206 * server and (we hope) prompt a broken client to fix itself.
michael@0 207 *
michael@0 208 * We keep track of the time at which we last applied this reasoning, because
michael@0 209 * thrashing doesn't solve anything. We keep a reasonable interval between
michael@0 210 * these remedial actions.
michael@0 211 */
michael@0 212 lastHMACEvent: 0,
michael@0 213
michael@0 214 /*
michael@0 215 * Returns whether to try again.
michael@0 216 */
michael@0 217 handleHMACEvent: function handleHMACEvent() {
michael@0 218 let now = Date.now();
michael@0 219
michael@0 220 // Leave a sizable delay between HMAC recovery attempts. This gives us
michael@0 221 // time for another client to fix themselves if we touch the record.
michael@0 222 if ((now - this.lastHMACEvent) < HMAC_EVENT_INTERVAL)
michael@0 223 return false;
michael@0 224
michael@0 225 this._log.info("Bad HMAC event detected. Attempting recovery " +
michael@0 226 "or signaling to other clients.");
michael@0 227
michael@0 228 // Set the last handled time so that we don't act again.
michael@0 229 this.lastHMACEvent = now;
michael@0 230
michael@0 231 // Fetch keys.
michael@0 232 let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
michael@0 233 try {
michael@0 234 let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
michael@0 235
michael@0 236 // Save out the ciphertext for when we reupload. If there's a bug in
michael@0 237 // CollectionKeyManager, this will prevent us from uploading junk.
michael@0 238 let cipherText = cryptoKeys.ciphertext;
michael@0 239
michael@0 240 if (!cryptoResp.success) {
michael@0 241 this._log.warn("Failed to download keys.");
michael@0 242 return false;
michael@0 243 }
michael@0 244
michael@0 245 let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
michael@0 246 cryptoKeys, true);
michael@0 247 if (keysChanged) {
michael@0 248 // Did they change? If so, carry on.
michael@0 249 this._log.info("Suggesting retry.");
michael@0 250 return true; // Try again.
michael@0 251 }
michael@0 252
michael@0 253 // If not, reupload them and continue the current sync.
michael@0 254 cryptoKeys.ciphertext = cipherText;
michael@0 255 cryptoKeys.cleartext = null;
michael@0 256
michael@0 257 let uploadResp = cryptoKeys.upload(this.resource(this.cryptoKeysURL));
michael@0 258 if (uploadResp.success)
michael@0 259 this._log.info("Successfully re-uploaded keys. Continuing sync.");
michael@0 260 else
michael@0 261 this._log.warn("Got error response re-uploading keys. " +
michael@0 262 "Continuing sync; let's try again later.");
michael@0 263
michael@0 264 return false; // Don't try again: same keys.
michael@0 265
michael@0 266 } catch (ex) {
michael@0 267 this._log.warn("Got exception \"" + ex + "\" fetching and handling " +
michael@0 268 "crypto keys. Will try again later.");
michael@0 269 return false;
michael@0 270 }
michael@0 271 },
michael@0 272
michael@0 273 handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) {
michael@0 274 // Don't want to wipe if we're just starting up!
michael@0 275 let wasBlank = this.collectionKeys.isClear;
michael@0 276 let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys);
michael@0 277
michael@0 278 if (keysChanged && !wasBlank) {
michael@0 279 this._log.debug("Keys changed: " + JSON.stringify(keysChanged));
michael@0 280
michael@0 281 if (!skipReset) {
michael@0 282 this._log.info("Resetting client to reflect key change.");
michael@0 283
michael@0 284 if (keysChanged.length) {
michael@0 285 // Collection keys only. Reset individual engines.
michael@0 286 this.resetClient(keysChanged);
michael@0 287 }
michael@0 288 else {
michael@0 289 // Default key changed: wipe it all.
michael@0 290 this.resetClient();
michael@0 291 }
michael@0 292
michael@0 293 this._log.info("Downloaded new keys, client reset. Proceeding.");
michael@0 294 }
michael@0 295 return true;
michael@0 296 }
michael@0 297 return false;
michael@0 298 },
michael@0 299
michael@0 300 /**
michael@0 301 * Prepare to initialize the rest of Weave after waiting a little bit
michael@0 302 */
michael@0 303 onStartup: function onStartup() {
michael@0 304 this._migratePrefs();
michael@0 305
michael@0 306 // Status is instantiated before us and is the first to grab an instance of
michael@0 307 // the IdentityManager. We use that instance because IdentityManager really
michael@0 308 // needs to be a singleton. Ideally, the longer-lived object would spawn
michael@0 309 // this service instance.
michael@0 310 if (!Status || !Status._authManager) {
michael@0 311 throw new Error("Status or Status._authManager not initialized.");
michael@0 312 }
michael@0 313
michael@0 314 this.status = Status;
michael@0 315 this.identity = Status._authManager;
michael@0 316 this.collectionKeys = new CollectionKeyManager();
michael@0 317
michael@0 318 this.errorHandler = new ErrorHandler(this);
michael@0 319
michael@0 320 this._log = Log.repository.getLogger("Sync.Service");
michael@0 321 this._log.level =
michael@0 322 Log.Level[Svc.Prefs.get("log.logger.service.main")];
michael@0 323
michael@0 324 this._log.info("Loading Weave " + WEAVE_VERSION);
michael@0 325
michael@0 326 this._clusterManager = this.identity.createClusterManager(this);
michael@0 327 this.recordManager = new RecordManager(this);
michael@0 328
michael@0 329 this.enabled = true;
michael@0 330
michael@0 331 this._registerEngines();
michael@0 332
michael@0 333 let ua = Cc["@mozilla.org/network/protocol;1?name=http"].
michael@0 334 getService(Ci.nsIHttpProtocolHandler).userAgent;
michael@0 335 this._log.info(ua);
michael@0 336
michael@0 337 if (!this._checkCrypto()) {
michael@0 338 this.enabled = false;
michael@0 339 this._log.info("Could not load the Weave crypto component. Disabling " +
michael@0 340 "Weave, since it will not work correctly.");
michael@0 341 }
michael@0 342
michael@0 343 Svc.Obs.add("weave:service:setup-complete", this);
michael@0 344 Svc.Prefs.observe("engine.", this);
michael@0 345
michael@0 346 this.scheduler = new SyncScheduler(this);
michael@0 347
michael@0 348 if (!this.enabled) {
michael@0 349 this._log.info("Firefox Sync disabled.");
michael@0 350 }
michael@0 351
michael@0 352 this._updateCachedURLs();
michael@0 353
michael@0 354 let status = this._checkSetup();
michael@0 355 if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) {
michael@0 356 Svc.Obs.notify("weave:engine:start-tracking");
michael@0 357 }
michael@0 358
michael@0 359 // Send an event now that Weave service is ready. We don't do this
michael@0 360 // synchronously so that observers can import this module before
michael@0 361 // registering an observer.
michael@0 362 Utils.nextTick(function onNextTick() {
michael@0 363 this.status.ready = true;
michael@0 364
michael@0 365 // UI code uses the flag on the XPCOM service so it doesn't have
michael@0 366 // to load a bunch of modules.
michael@0 367 let xps = Cc["@mozilla.org/weave/service;1"]
michael@0 368 .getService(Ci.nsISupports)
michael@0 369 .wrappedJSObject;
michael@0 370 xps.ready = true;
michael@0 371
michael@0 372 Svc.Obs.notify("weave:service:ready");
michael@0 373 }.bind(this));
michael@0 374 },
michael@0 375
michael@0 376 _checkSetup: function _checkSetup() {
michael@0 377 if (!this.enabled) {
michael@0 378 return this.status.service = STATUS_DISABLED;
michael@0 379 }
michael@0 380 return this.status.checkSetup();
michael@0 381 },
michael@0 382
michael@0 383 _migratePrefs: function _migratePrefs() {
michael@0 384 // Migrate old debugLog prefs.
michael@0 385 let logLevel = Svc.Prefs.get("log.appender.debugLog");
michael@0 386 if (logLevel) {
michael@0 387 Svc.Prefs.set("log.appender.file.level", logLevel);
michael@0 388 Svc.Prefs.reset("log.appender.debugLog");
michael@0 389 }
michael@0 390 if (Svc.Prefs.get("log.appender.debugLog.enabled")) {
michael@0 391 Svc.Prefs.set("log.appender.file.logOnSuccess", true);
michael@0 392 Svc.Prefs.reset("log.appender.debugLog.enabled");
michael@0 393 }
michael@0 394
michael@0 395 // Migrate old extensions.weave.* prefs if we haven't already tried.
michael@0 396 if (Svc.Prefs.get("migrated", false))
michael@0 397 return;
michael@0 398
michael@0 399 // Grab the list of old pref names
michael@0 400 let oldPrefBranch = "extensions.weave.";
michael@0 401 let oldPrefNames = Cc["@mozilla.org/preferences-service;1"].
michael@0 402 getService(Ci.nsIPrefService).
michael@0 403 getBranch(oldPrefBranch).
michael@0 404 getChildList("", {});
michael@0 405
michael@0 406 // Map each old pref to the current pref branch
michael@0 407 let oldPref = new Preferences(oldPrefBranch);
michael@0 408 for each (let pref in oldPrefNames)
michael@0 409 Svc.Prefs.set(pref, oldPref.get(pref));
michael@0 410
michael@0 411 // Remove all the old prefs and remember that we've migrated
michael@0 412 oldPref.resetBranch("");
michael@0 413 Svc.Prefs.set("migrated", true);
michael@0 414 },
michael@0 415
michael@0 416 /**
michael@0 417 * Register the built-in engines for certain applications
michael@0 418 */
michael@0 419 _registerEngines: function _registerEngines() {
michael@0 420 this.engineManager = new EngineManager(this);
michael@0 421
michael@0 422 let engines = [];
michael@0 423 // Applications can provide this preference (comma-separated list)
michael@0 424 // to specify which engines should be registered on startup.
michael@0 425 let pref = Svc.Prefs.get("registerEngines");
michael@0 426 if (pref) {
michael@0 427 engines = pref.split(",");
michael@0 428 }
michael@0 429
michael@0 430 let declined = [];
michael@0 431 pref = Svc.Prefs.get("declinedEngines");
michael@0 432 if (pref) {
michael@0 433 declined = pref.split(",");
michael@0 434 }
michael@0 435
michael@0 436 this.clientsEngine = new ClientEngine(this);
michael@0 437
michael@0 438 for (let name of engines) {
michael@0 439 if (!name in ENGINE_MODULES) {
michael@0 440 this._log.info("Do not know about engine: " + name);
michael@0 441 continue;
michael@0 442 }
michael@0 443
michael@0 444 let ns = {};
michael@0 445 try {
michael@0 446 Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns);
michael@0 447
michael@0 448 let engineName = name + "Engine";
michael@0 449 if (!(engineName in ns)) {
michael@0 450 this._log.warn("Could not find exported engine instance: " + engineName);
michael@0 451 continue;
michael@0 452 }
michael@0 453
michael@0 454 this.engineManager.register(ns[engineName]);
michael@0 455 } catch (ex) {
michael@0 456 this._log.warn("Could not register engine " + name + ": " +
michael@0 457 CommonUtils.exceptionStr(ex));
michael@0 458 }
michael@0 459 }
michael@0 460
michael@0 461 this.engineManager.setDeclined(declined);
michael@0 462 },
michael@0 463
michael@0 464 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
michael@0 465 Ci.nsISupportsWeakReference]),
michael@0 466
michael@0 467 // nsIObserver
michael@0 468
michael@0 469 observe: function observe(subject, topic, data) {
michael@0 470 switch (topic) {
michael@0 471 case "weave:service:setup-complete":
michael@0 472 let status = this._checkSetup();
michael@0 473 if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED)
michael@0 474 Svc.Obs.notify("weave:engine:start-tracking");
michael@0 475 break;
michael@0 476 case "nsPref:changed":
michael@0 477 if (this._ignorePrefObserver)
michael@0 478 return;
michael@0 479 let engine = data.slice((PREFS_BRANCH + "engine.").length);
michael@0 480 this._handleEngineStatusChanged(engine);
michael@0 481 break;
michael@0 482 }
michael@0 483 },
michael@0 484
michael@0 485 _handleEngineStatusChanged: function handleEngineDisabled(engine) {
michael@0 486 this._log.trace("Status for " + engine + " engine changed.");
michael@0 487 if (Svc.Prefs.get("engineStatusChanged." + engine, false)) {
michael@0 488 // The enabled status being changed back to what it was before.
michael@0 489 Svc.Prefs.reset("engineStatusChanged." + engine);
michael@0 490 } else {
michael@0 491 // Remember that the engine status changed locally until the next sync.
michael@0 492 Svc.Prefs.set("engineStatusChanged." + engine, true);
michael@0 493 }
michael@0 494 },
michael@0 495
michael@0 496 /**
michael@0 497 * Obtain a Resource instance with authentication credentials.
michael@0 498 */
michael@0 499 resource: function resource(url) {
michael@0 500 let res = new Resource(url);
michael@0 501 res.authenticator = this.identity.getResourceAuthenticator();
michael@0 502
michael@0 503 return res;
michael@0 504 },
michael@0 505
michael@0 506 /**
michael@0 507 * Obtain a SyncStorageRequest instance with authentication credentials.
michael@0 508 */
michael@0 509 getStorageRequest: function getStorageRequest(url) {
michael@0 510 let request = new SyncStorageRequest(url);
michael@0 511 request.authenticator = this.identity.getRESTRequestAuthenticator();
michael@0 512
michael@0 513 return request;
michael@0 514 },
michael@0 515
michael@0 516 /**
michael@0 517 * Perform the info fetch as part of a login or key fetch, or
michael@0 518 * inside engine sync.
michael@0 519 */
michael@0 520 _fetchInfo: function (url) {
michael@0 521 let infoURL = url || this.infoURL;
michael@0 522
michael@0 523 this._log.trace("In _fetchInfo: " + infoURL);
michael@0 524 let info;
michael@0 525 try {
michael@0 526 info = this.resource(infoURL).get();
michael@0 527 } catch (ex) {
michael@0 528 this.errorHandler.checkServerError(ex);
michael@0 529 throw ex;
michael@0 530 }
michael@0 531
michael@0 532 // Always check for errors; this is also where we look for X-Weave-Alert.
michael@0 533 this.errorHandler.checkServerError(info);
michael@0 534 if (!info.success) {
michael@0 535 throw "Aborting sync: failed to get collections.";
michael@0 536 }
michael@0 537 return info;
michael@0 538 },
michael@0 539
michael@0 540 verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) {
michael@0 541
michael@0 542 this._log.debug("Fetching and verifying -- or generating -- symmetric keys.");
michael@0 543
michael@0 544 // Don't allow empty/missing passphrase.
michael@0 545 // Furthermore, we assume that our sync key is already upgraded,
michael@0 546 // and fail if that assumption is invalidated.
michael@0 547
michael@0 548 if (!this.identity.syncKey) {
michael@0 549 this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
michael@0 550 this.status.sync = CREDENTIALS_CHANGED;
michael@0 551 return false;
michael@0 552 }
michael@0 553
michael@0 554 let syncKeyBundle = this.identity.syncKeyBundle;
michael@0 555 if (!syncKeyBundle) {
michael@0 556 this._log.error("Sync Key Bundle not set. Invalid Sync Key?");
michael@0 557
michael@0 558 this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
michael@0 559 this.status.sync = CREDENTIALS_CHANGED;
michael@0 560 return false;
michael@0 561 }
michael@0 562
michael@0 563 try {
michael@0 564 if (!infoResponse)
michael@0 565 infoResponse = this._fetchInfo(); // Will throw an exception on failure.
michael@0 566
michael@0 567 // This only applies when the server is already at version 4.
michael@0 568 if (infoResponse.status != 200) {
michael@0 569 this._log.warn("info/collections returned non-200 response. Failing key fetch.");
michael@0 570 this.status.login = LOGIN_FAILED_SERVER_ERROR;
michael@0 571 this.errorHandler.checkServerError(infoResponse);
michael@0 572 return false;
michael@0 573 }
michael@0 574
michael@0 575 let infoCollections = infoResponse.obj;
michael@0 576
michael@0 577 this._log.info("Testing info/collections: " + JSON.stringify(infoCollections));
michael@0 578
michael@0 579 if (this.collectionKeys.updateNeeded(infoCollections)) {
michael@0 580 this._log.info("collection keys reports that a key update is needed.");
michael@0 581
michael@0 582 // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this.
michael@0 583
michael@0 584 // Fetch storage/crypto/keys.
michael@0 585 let cryptoKeys;
michael@0 586
michael@0 587 if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) {
michael@0 588 try {
michael@0 589 cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
michael@0 590 let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
michael@0 591
michael@0 592 if (cryptoResp.success) {
michael@0 593 let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys);
michael@0 594 return true;
michael@0 595 }
michael@0 596 else if (cryptoResp.status == 404) {
michael@0 597 // On failure, ask to generate new keys and upload them.
michael@0 598 // Fall through to the behavior below.
michael@0 599 this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating.");
michael@0 600 cryptoKeys = null;
michael@0 601 }
michael@0 602 else {
michael@0 603 // Some other problem.
michael@0 604 this.status.login = LOGIN_FAILED_SERVER_ERROR;
michael@0 605 this.errorHandler.checkServerError(cryptoResp);
michael@0 606 this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys.");
michael@0 607 return false;
michael@0 608 }
michael@0 609 }
michael@0 610 catch (ex) {
michael@0 611 this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys.");
michael@0 612 // TODO: Um, what exceptions might we get here? Should we re-throw any?
michael@0 613
michael@0 614 // One kind of exception: HMAC failure.
michael@0 615 if (Utils.isHMACMismatch(ex)) {
michael@0 616 this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
michael@0 617 this.status.sync = CREDENTIALS_CHANGED;
michael@0 618 }
michael@0 619 else {
michael@0 620 // In the absence of further disambiguation or more precise
michael@0 621 // failure constants, just report failure.
michael@0 622 this.status.login = LOGIN_FAILED;
michael@0 623 }
michael@0 624 return false;
michael@0 625 }
michael@0 626 }
michael@0 627 else {
michael@0 628 this._log.info("... 'crypto' is not a reported collection. Generating new keys.");
michael@0 629 }
michael@0 630
michael@0 631 if (!cryptoKeys) {
michael@0 632 this._log.info("No keys! Generating new ones.");
michael@0 633
michael@0 634 // Better make some and upload them, and wipe the server to ensure
michael@0 635 // consistency. This is all achieved via _freshStart.
michael@0 636 // If _freshStart fails to clear the server or upload keys, it will
michael@0 637 // throw.
michael@0 638 this._freshStart();
michael@0 639 return true;
michael@0 640 }
michael@0 641
michael@0 642 // Last-ditch case.
michael@0 643 return false;
michael@0 644 }
michael@0 645 else {
michael@0 646 // No update needed: we're good!
michael@0 647 return true;
michael@0 648 }
michael@0 649
michael@0 650 } catch (ex) {
michael@0 651 // This means no keys are present, or there's a network error.
michael@0 652 this._log.debug("Failed to fetch and verify keys: "
michael@0 653 + Utils.exceptionStr(ex));
michael@0 654 this.errorHandler.checkServerError(ex);
michael@0 655 return false;
michael@0 656 }
michael@0 657 },
michael@0 658
michael@0 659 verifyLogin: function verifyLogin(allow40XRecovery = true) {
michael@0 660 // If the identity isn't ready it might not know the username...
michael@0 661 if (!this.identity.readyToAuthenticate) {
michael@0 662 this._log.info("Not ready to authenticate in verifyLogin.");
michael@0 663 this.status.login = LOGIN_FAILED_NOT_READY;
michael@0 664 return false;
michael@0 665 }
michael@0 666
michael@0 667 if (!this.identity.username) {
michael@0 668 this._log.warn("No username in verifyLogin.");
michael@0 669 this.status.login = LOGIN_FAILED_NO_USERNAME;
michael@0 670 return false;
michael@0 671 }
michael@0 672
michael@0 673 // Unlock master password, or return.
michael@0 674 // Attaching auth credentials to a request requires access to
michael@0 675 // passwords, which means that Resource.get can throw MP-related
michael@0 676 // exceptions!
michael@0 677 // Try to fetch the passphrase first, while we still have control.
michael@0 678 try {
michael@0 679 this.identity.syncKey;
michael@0 680 } catch (ex) {
michael@0 681 this._log.debug("Fetching passphrase threw " + ex +
michael@0 682 "; assuming master password locked.");
michael@0 683 this.status.login = MASTER_PASSWORD_LOCKED;
michael@0 684 return false;
michael@0 685 }
michael@0 686
michael@0 687 try {
michael@0 688 // Make sure we have a cluster to verify against.
michael@0 689 // This is a little weird, if we don't get a node we pretend
michael@0 690 // to succeed, since that probably means we just don't have storage.
michael@0 691 if (this.clusterURL == "" && !this._clusterManager.setCluster()) {
michael@0 692 this.status.sync = NO_SYNC_NODE_FOUND;
michael@0 693 return true;
michael@0 694 }
michael@0 695
michael@0 696 // Fetch collection info on every startup.
michael@0 697 let test = this.resource(this.infoURL).get();
michael@0 698
michael@0 699 switch (test.status) {
michael@0 700 case 200:
michael@0 701 // The user is authenticated.
michael@0 702
michael@0 703 // We have no way of verifying the passphrase right now,
michael@0 704 // so wait until remoteSetup to do so.
michael@0 705 // Just make the most trivial checks.
michael@0 706 if (!this.identity.syncKey) {
michael@0 707 this._log.warn("No passphrase in verifyLogin.");
michael@0 708 this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
michael@0 709 return false;
michael@0 710 }
michael@0 711
michael@0 712 // Go ahead and do remote setup, so that we can determine
michael@0 713 // conclusively that our passphrase is correct.
michael@0 714 if (this._remoteSetup()) {
michael@0 715 // Username/password verified.
michael@0 716 this.status.login = LOGIN_SUCCEEDED;
michael@0 717 return true;
michael@0 718 }
michael@0 719
michael@0 720 this._log.warn("Remote setup failed.");
michael@0 721 // Remote setup must have failed.
michael@0 722 return false;
michael@0 723
michael@0 724 case 401:
michael@0 725 this._log.warn("401: login failed.");
michael@0 726 // Fall through to the 404 case.
michael@0 727
michael@0 728 case 404:
michael@0 729 // Check that we're verifying with the correct cluster
michael@0 730 if (allow40XRecovery && this._clusterManager.setCluster()) {
michael@0 731 return this.verifyLogin(false);
michael@0 732 }
michael@0 733
michael@0 734 // We must have the right cluster, but the server doesn't expect us
michael@0 735 this.status.login = LOGIN_FAILED_LOGIN_REJECTED;
michael@0 736 return false;
michael@0 737
michael@0 738 default:
michael@0 739 // Server didn't respond with something that we expected
michael@0 740 this.status.login = LOGIN_FAILED_SERVER_ERROR;
michael@0 741 this.errorHandler.checkServerError(test);
michael@0 742 return false;
michael@0 743 }
michael@0 744 } catch (ex) {
michael@0 745 // Must have failed on some network issue
michael@0 746 this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex));
michael@0 747 this.status.login = LOGIN_FAILED_NETWORK_ERROR;
michael@0 748 this.errorHandler.checkServerError(ex);
michael@0 749 return false;
michael@0 750 }
michael@0 751 },
michael@0 752
michael@0 753 generateNewSymmetricKeys: function generateNewSymmetricKeys() {
michael@0 754 this._log.info("Generating new keys WBO...");
michael@0 755 let wbo = this.collectionKeys.generateNewKeysWBO();
michael@0 756 this._log.info("Encrypting new key bundle.");
michael@0 757 wbo.encrypt(this.identity.syncKeyBundle);
michael@0 758
michael@0 759 this._log.info("Uploading...");
michael@0 760 let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL));
michael@0 761 if (uploadRes.status != 200) {
michael@0 762 this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!");
michael@0 763 this.errorHandler.checkServerError(uploadRes);
michael@0 764 throw new Error("Unable to upload symmetric keys.");
michael@0 765 }
michael@0 766 this._log.info("Got status " + uploadRes.status + " uploading keys.");
michael@0 767 let serverModified = uploadRes.obj; // Modified timestamp according to server.
michael@0 768 this._log.debug("Server reports crypto modified: " + serverModified);
michael@0 769
michael@0 770 // Now verify that info/collections shows them!
michael@0 771 this._log.debug("Verifying server collection records.");
michael@0 772 let info = this._fetchInfo();
michael@0 773 this._log.debug("info/collections is: " + info);
michael@0 774
michael@0 775 if (info.status != 200) {
michael@0 776 this._log.warn("Non-200 info/collections response. Aborting.");
michael@0 777 throw new Error("Unable to upload symmetric keys.");
michael@0 778 }
michael@0 779
michael@0 780 info = info.obj;
michael@0 781 if (!(CRYPTO_COLLECTION in info)) {
michael@0 782 this._log.error("Consistency failure: info/collections excludes " +
michael@0 783 "crypto after successful upload.");
michael@0 784 throw new Error("Symmetric key upload failed.");
michael@0 785 }
michael@0 786
michael@0 787 // Can't check against local modified: clock drift.
michael@0 788 if (info[CRYPTO_COLLECTION] < serverModified) {
michael@0 789 this._log.error("Consistency failure: info/collections crypto entry " +
michael@0 790 "is stale after successful upload.");
michael@0 791 throw new Error("Symmetric key upload failed.");
michael@0 792 }
michael@0 793
michael@0 794 // Doesn't matter if the timestamp is ahead.
michael@0 795
michael@0 796 // Download and install them.
michael@0 797 let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO);
michael@0 798 let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response;
michael@0 799 if (cryptoResp.status != 200) {
michael@0 800 this._log.warn("Failed to download keys.");
michael@0 801 throw new Error("Symmetric key download failed.");
michael@0 802 }
michael@0 803 let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle,
michael@0 804 cryptoKeys, true);
michael@0 805 if (keysChanged) {
michael@0 806 this._log.info("Downloaded keys differed, as expected.");
michael@0 807 }
michael@0 808 },
michael@0 809
michael@0 810 changePassword: function changePassword(newPassword) {
michael@0 811 let client = new UserAPI10Client(this.userAPIURI);
michael@0 812 let cb = Async.makeSpinningCallback();
michael@0 813 client.changePassword(this.identity.username,
michael@0 814 this.identity.basicPassword, newPassword, cb);
michael@0 815
michael@0 816 try {
michael@0 817 cb.wait();
michael@0 818 } catch (ex) {
michael@0 819 this._log.debug("Password change failed: " +
michael@0 820 CommonUtils.exceptionStr(ex));
michael@0 821 return false;
michael@0 822 }
michael@0 823
michael@0 824 // Save the new password for requests and login manager.
michael@0 825 this.identity.basicPassword = newPassword;
michael@0 826 this.persistLogin();
michael@0 827 return true;
michael@0 828 },
michael@0 829
michael@0 830 changePassphrase: function changePassphrase(newphrase) {
michael@0 831 return this._catch(function doChangePasphrase() {
michael@0 832 /* Wipe. */
michael@0 833 this.wipeServer();
michael@0 834
michael@0 835 this.logout();
michael@0 836
michael@0 837 /* Set this so UI is updated on next run. */
michael@0 838 this.identity.syncKey = newphrase;
michael@0 839 this.persistLogin();
michael@0 840
michael@0 841 /* We need to re-encrypt everything, so reset. */
michael@0 842 this.resetClient();
michael@0 843 this.collectionKeys.clear();
michael@0 844
michael@0 845 /* Login and sync. This also generates new keys. */
michael@0 846 this.sync();
michael@0 847
michael@0 848 Svc.Obs.notify("weave:service:change-passphrase", true);
michael@0 849
michael@0 850 return true;
michael@0 851 })();
michael@0 852 },
michael@0 853
michael@0 854 startOver: function startOver() {
michael@0 855 this._log.trace("Invoking Service.startOver.");
michael@0 856 Svc.Obs.notify("weave:engine:stop-tracking");
michael@0 857 this.status.resetSync();
michael@0 858
michael@0 859 // Deletion doesn't make sense if we aren't set up yet!
michael@0 860 if (this.clusterURL != "") {
michael@0 861 // Clear client-specific data from the server, including disabled engines.
michael@0 862 for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) {
michael@0 863 try {
michael@0 864 engine.removeClientData();
michael@0 865 } catch(ex) {
michael@0 866 this._log.warn("Deleting client data for " + engine.name + " failed:"
michael@0 867 + Utils.exceptionStr(ex));
michael@0 868 }
michael@0 869 }
michael@0 870 this._log.debug("Finished deleting client data.");
michael@0 871 } else {
michael@0 872 this._log.debug("Skipping client data removal: no cluster URL.");
michael@0 873 }
michael@0 874
michael@0 875 // We want let UI consumers of the following notification know as soon as
michael@0 876 // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now
michael@0 877 // by emptying the passphrase (we still need the password).
michael@0 878 this._log.info("Service.startOver dropping sync key and logging out.");
michael@0 879 this.identity.resetSyncKey();
michael@0 880 this.status.login = LOGIN_FAILED_NO_PASSPHRASE;
michael@0 881 this.logout();
michael@0 882 Svc.Obs.notify("weave:service:start-over");
michael@0 883
michael@0 884 // Reset all engines and clear keys.
michael@0 885 this.resetClient();
michael@0 886 this.collectionKeys.clear();
michael@0 887 this.status.resetBackoff();
michael@0 888
michael@0 889 // Reset Weave prefs.
michael@0 890 this._ignorePrefObserver = true;
michael@0 891 Svc.Prefs.resetBranch("");
michael@0 892 this._ignorePrefObserver = false;
michael@0 893
michael@0 894 Svc.Prefs.set("lastversion", WEAVE_VERSION);
michael@0 895
michael@0 896 this.identity.deleteSyncCredentials();
michael@0 897
michael@0 898 // If necessary, reset the identity manager, then re-initialize it so the
michael@0 899 // FxA manager is used. This is configurable via a pref - mainly for tests.
michael@0 900 let keepIdentity = false;
michael@0 901 try {
michael@0 902 keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
michael@0 903 } catch (_) { /* no such pref */ }
michael@0 904 if (keepIdentity) {
michael@0 905 Svc.Obs.notify("weave:service:start-over:finish");
michael@0 906 return;
michael@0 907 }
michael@0 908
michael@0 909 this.identity.finalize().then(
michael@0 910 () => {
michael@0 911 this.identity.username = "";
michael@0 912 this.status.__authManager = null;
michael@0 913 this.identity = Status._authManager;
michael@0 914 this._clusterManager = this.identity.createClusterManager(this);
michael@0 915 Svc.Obs.notify("weave:service:start-over:finish");
michael@0 916 }
michael@0 917 ).then(null,
michael@0 918 err => {
michael@0 919 this._log.error("startOver failed to re-initialize the identity manager: " + err);
michael@0 920 // Still send the observer notification so the current state is
michael@0 921 // reflected in the UI.
michael@0 922 Svc.Obs.notify("weave:service:start-over:finish");
michael@0 923 }
michael@0 924 );
michael@0 925 },
michael@0 926
michael@0 927 persistLogin: function persistLogin() {
michael@0 928 try {
michael@0 929 this.identity.persistCredentials(true);
michael@0 930 } catch (ex) {
michael@0 931 this._log.info("Unable to persist credentials: " + ex);
michael@0 932 }
michael@0 933 },
michael@0 934
michael@0 935 login: function login(username, password, passphrase) {
michael@0 936 function onNotify() {
michael@0 937 this._loggedIn = false;
michael@0 938 if (Services.io.offline) {
michael@0 939 this.status.login = LOGIN_FAILED_NETWORK_ERROR;
michael@0 940 throw "Application is offline, login should not be called";
michael@0 941 }
michael@0 942
michael@0 943 let initialStatus = this._checkSetup();
michael@0 944 if (username) {
michael@0 945 this.identity.username = username;
michael@0 946 }
michael@0 947 if (password) {
michael@0 948 this.identity.basicPassword = password;
michael@0 949 }
michael@0 950 if (passphrase) {
michael@0 951 this.identity.syncKey = passphrase;
michael@0 952 }
michael@0 953
michael@0 954 if (this._checkSetup() == CLIENT_NOT_CONFIGURED) {
michael@0 955 throw "Aborting login, client not configured.";
michael@0 956 }
michael@0 957
michael@0 958 // Ask the identity manager to explicitly login now.
michael@0 959 let cb = Async.makeSpinningCallback();
michael@0 960 this.identity.ensureLoggedIn().then(cb, cb);
michael@0 961
michael@0 962 // Just let any errors bubble up - they've more context than we do!
michael@0 963 cb.wait();
michael@0 964
michael@0 965 // Calling login() with parameters when the client was
michael@0 966 // previously not configured means setup was completed.
michael@0 967 if (initialStatus == CLIENT_NOT_CONFIGURED
michael@0 968 && (username || password || passphrase)) {
michael@0 969 Svc.Obs.notify("weave:service:setup-complete");
michael@0 970 }
michael@0 971 this._log.info("Logging in the user.");
michael@0 972 this._updateCachedURLs();
michael@0 973
michael@0 974 if (!this.verifyLogin()) {
michael@0 975 // verifyLogin sets the failure states here.
michael@0 976 throw "Login failed: " + this.status.login;
michael@0 977 }
michael@0 978
michael@0 979 this._loggedIn = true;
michael@0 980
michael@0 981 return true;
michael@0 982 }
michael@0 983
michael@0 984 let notifier = this._notify("login", "", onNotify.bind(this));
michael@0 985 return this._catch(this._lock("service.js: login", notifier))();
michael@0 986 },
michael@0 987
michael@0 988 logout: function logout() {
michael@0 989 // If we failed during login, we aren't going to have this._loggedIn set,
michael@0 990 // but we still want to ask the identity to logout, so it doesn't try and
michael@0 991 // reuse any old credentials next time we sync.
michael@0 992 this._log.info("Logging out");
michael@0 993 this.identity.logout();
michael@0 994 this._loggedIn = false;
michael@0 995
michael@0 996 Svc.Obs.notify("weave:service:logout:finish");
michael@0 997 },
michael@0 998
michael@0 999 checkAccount: function checkAccount(account) {
michael@0 1000 let client = new UserAPI10Client(this.userAPIURI);
michael@0 1001 let cb = Async.makeSpinningCallback();
michael@0 1002
michael@0 1003 let username = this.identity.usernameFromAccount(account);
michael@0 1004 client.usernameExists(username, cb);
michael@0 1005
michael@0 1006 try {
michael@0 1007 let exists = cb.wait();
michael@0 1008 return exists ? "notAvailable" : "available";
michael@0 1009 } catch (ex) {
michael@0 1010 // TODO fix API convention.
michael@0 1011 return this.errorHandler.errorStr(ex);
michael@0 1012 }
michael@0 1013 },
michael@0 1014
michael@0 1015 createAccount: function createAccount(email, password,
michael@0 1016 captchaChallenge, captchaResponse) {
michael@0 1017 let client = new UserAPI10Client(this.userAPIURI);
michael@0 1018
michael@0 1019 // Hint to server to allow scripted user creation or otherwise
michael@0 1020 // ignore captcha.
michael@0 1021 if (Svc.Prefs.isSet("admin-secret")) {
michael@0 1022 client.adminSecret = Svc.Prefs.get("admin-secret", "");
michael@0 1023 }
michael@0 1024
michael@0 1025 let cb = Async.makeSpinningCallback();
michael@0 1026
michael@0 1027 client.createAccount(email, password, captchaChallenge, captchaResponse,
michael@0 1028 cb);
michael@0 1029
michael@0 1030 try {
michael@0 1031 cb.wait();
michael@0 1032 return null;
michael@0 1033 } catch (ex) {
michael@0 1034 return this.errorHandler.errorStr(ex.body);
michael@0 1035 }
michael@0 1036 },
michael@0 1037
michael@0 1038 // Stuff we need to do after login, before we can really do
michael@0 1039 // anything (e.g. key setup).
michael@0 1040 _remoteSetup: function _remoteSetup(infoResponse) {
michael@0 1041 let reset = false;
michael@0 1042
michael@0 1043 this._log.debug("Fetching global metadata record");
michael@0 1044 let meta = this.recordManager.get(this.metaURL);
michael@0 1045
michael@0 1046 // Checking modified time of the meta record.
michael@0 1047 if (infoResponse &&
michael@0 1048 (infoResponse.obj.meta != this.metaModified) &&
michael@0 1049 (!meta || !meta.isNew)) {
michael@0 1050
michael@0 1051 // Delete the cached meta record...
michael@0 1052 this._log.debug("Clearing cached meta record. metaModified is " +
michael@0 1053 JSON.stringify(this.metaModified) + ", setting to " +
michael@0 1054 JSON.stringify(infoResponse.obj.meta));
michael@0 1055
michael@0 1056 this.recordManager.del(this.metaURL);
michael@0 1057
michael@0 1058 // ... fetch the current record from the server, and COPY THE FLAGS.
michael@0 1059 let newMeta = this.recordManager.get(this.metaURL);
michael@0 1060
michael@0 1061 // If we got a 401, we do not want to create a new meta/global - we
michael@0 1062 // should be able to get the existing meta after we get a new node.
michael@0 1063 if (this.recordManager.response.status == 401) {
michael@0 1064 this._log.debug("Fetching meta/global record on the server returned 401.");
michael@0 1065 this.errorHandler.checkServerError(this.recordManager.response);
michael@0 1066 return false;
michael@0 1067 }
michael@0 1068
michael@0 1069 if (!this.recordManager.response.success || !newMeta) {
michael@0 1070 this._log.debug("No meta/global record on the server. Creating one.");
michael@0 1071 newMeta = new WBORecord("meta", "global");
michael@0 1072 newMeta.payload.syncID = this.syncID;
michael@0 1073 newMeta.payload.storageVersion = STORAGE_VERSION;
michael@0 1074 newMeta.payload.declined = this.engineManager.getDeclined();
michael@0 1075
michael@0 1076 newMeta.isNew = true;
michael@0 1077
michael@0 1078 this.recordManager.set(this.metaURL, newMeta);
michael@0 1079 if (!newMeta.upload(this.resource(this.metaURL)).success) {
michael@0 1080 this._log.warn("Unable to upload new meta/global. Failing remote setup.");
michael@0 1081 return false;
michael@0 1082 }
michael@0 1083 } else {
michael@0 1084 // If newMeta, then it stands to reason that meta != null.
michael@0 1085 newMeta.isNew = meta.isNew;
michael@0 1086 newMeta.changed = meta.changed;
michael@0 1087 }
michael@0 1088
michael@0 1089 // Switch in the new meta object and record the new time.
michael@0 1090 meta = newMeta;
michael@0 1091 this.metaModified = infoResponse.obj.meta;
michael@0 1092 }
michael@0 1093
michael@0 1094 let remoteVersion = (meta && meta.payload.storageVersion)?
michael@0 1095 meta.payload.storageVersion : "";
michael@0 1096
michael@0 1097 this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:",
michael@0 1098 STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" "));
michael@0 1099
michael@0 1100 // Check for cases that require a fresh start. When comparing remoteVersion,
michael@0 1101 // we need to convert it to a number as older clients used it as a string.
michael@0 1102 if (!meta || !meta.payload.storageVersion || !meta.payload.syncID ||
michael@0 1103 STORAGE_VERSION > parseFloat(remoteVersion)) {
michael@0 1104
michael@0 1105 this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed.");
michael@0 1106
michael@0 1107 // abort the server wipe if the GET status was anything other than 404 or 200
michael@0 1108 let status = this.recordManager.response.status;
michael@0 1109 if (status != 200 && status != 404) {
michael@0 1110 this.status.sync = METARECORD_DOWNLOAD_FAIL;
michael@0 1111 this.errorHandler.checkServerError(this.recordManager.response);
michael@0 1112 this._log.warn("Unknown error while downloading metadata record. " +
michael@0 1113 "Aborting sync.");
michael@0 1114 return false;
michael@0 1115 }
michael@0 1116
michael@0 1117 if (!meta)
michael@0 1118 this._log.info("No metadata record, server wipe needed");
michael@0 1119 if (meta && !meta.payload.syncID)
michael@0 1120 this._log.warn("No sync id, server wipe needed");
michael@0 1121
michael@0 1122 reset = true;
michael@0 1123
michael@0 1124 this._log.info("Wiping server data");
michael@0 1125 this._freshStart();
michael@0 1126
michael@0 1127 if (status == 404)
michael@0 1128 this._log.info("Metadata record not found, server was wiped to ensure " +
michael@0 1129 "consistency.");
michael@0 1130 else // 200
michael@0 1131 this._log.info("Wiped server; incompatible metadata: " + remoteVersion);
michael@0 1132
michael@0 1133 return true;
michael@0 1134 }
michael@0 1135 else if (remoteVersion > STORAGE_VERSION) {
michael@0 1136 this.status.sync = VERSION_OUT_OF_DATE;
michael@0 1137 this._log.warn("Upgrade required to access newer storage version.");
michael@0 1138 return false;
michael@0 1139 }
michael@0 1140 else if (meta.payload.syncID != this.syncID) {
michael@0 1141
michael@0 1142 this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID);
michael@0 1143 this.resetClient();
michael@0 1144 this.collectionKeys.clear();
michael@0 1145 this.syncID = meta.payload.syncID;
michael@0 1146 this._log.debug("Clear cached values and take syncId: " + this.syncID);
michael@0 1147
michael@0 1148 if (!this.upgradeSyncKey(meta.payload.syncID)) {
michael@0 1149 this._log.warn("Failed to upgrade sync key. Failing remote setup.");
michael@0 1150 return false;
michael@0 1151 }
michael@0 1152
michael@0 1153 if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
michael@0 1154 this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
michael@0 1155 return false;
michael@0 1156 }
michael@0 1157
michael@0 1158 // bug 545725 - re-verify creds and fail sanely
michael@0 1159 if (!this.verifyLogin()) {
michael@0 1160 this.status.sync = CREDENTIALS_CHANGED;
michael@0 1161 this._log.info("Credentials have changed, aborting sync and forcing re-login.");
michael@0 1162 return false;
michael@0 1163 }
michael@0 1164
michael@0 1165 return true;
michael@0 1166 }
michael@0 1167 else {
michael@0 1168 if (!this.upgradeSyncKey(meta.payload.syncID)) {
michael@0 1169 this._log.warn("Failed to upgrade sync key. Failing remote setup.");
michael@0 1170 return false;
michael@0 1171 }
michael@0 1172
michael@0 1173 if (!this.verifyAndFetchSymmetricKeys(infoResponse)) {
michael@0 1174 this._log.warn("Failed to fetch symmetric keys. Failing remote setup.");
michael@0 1175 return false;
michael@0 1176 }
michael@0 1177
michael@0 1178 return true;
michael@0 1179 }
michael@0 1180 },
michael@0 1181
michael@0 1182 /**
michael@0 1183 * Return whether we should attempt login at the start of a sync.
michael@0 1184 *
michael@0 1185 * Note that this function has strong ties to _checkSync: callers
michael@0 1186 * of this function should typically use _checkSync to verify that
michael@0 1187 * any necessary login took place.
michael@0 1188 */
michael@0 1189 _shouldLogin: function _shouldLogin() {
michael@0 1190 return this.enabled &&
michael@0 1191 !Services.io.offline &&
michael@0 1192 !this.isLoggedIn;
michael@0 1193 },
michael@0 1194
michael@0 1195 /**
michael@0 1196 * Determine if a sync should run.
michael@0 1197 *
michael@0 1198 * @param ignore [optional]
michael@0 1199 * array of reasons to ignore when checking
michael@0 1200 *
michael@0 1201 * @return Reason for not syncing; not-truthy if sync should run
michael@0 1202 */
michael@0 1203 _checkSync: function _checkSync(ignore) {
michael@0 1204 let reason = "";
michael@0 1205 if (!this.enabled)
michael@0 1206 reason = kSyncWeaveDisabled;
michael@0 1207 else if (Services.io.offline)
michael@0 1208 reason = kSyncNetworkOffline;
michael@0 1209 else if (this.status.minimumNextSync > Date.now())
michael@0 1210 reason = kSyncBackoffNotMet;
michael@0 1211 else if ((this.status.login == MASTER_PASSWORD_LOCKED) &&
michael@0 1212 Utils.mpLocked())
michael@0 1213 reason = kSyncMasterPasswordLocked;
michael@0 1214 else if (Svc.Prefs.get("firstSync") == "notReady")
michael@0 1215 reason = kFirstSyncChoiceNotMade;
michael@0 1216
michael@0 1217 if (ignore && ignore.indexOf(reason) != -1)
michael@0 1218 return "";
michael@0 1219
michael@0 1220 return reason;
michael@0 1221 },
michael@0 1222
michael@0 1223 sync: function sync() {
michael@0 1224 let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT);
michael@0 1225 this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent);
michael@0 1226 this._log.info("Starting sync at " + dateStr);
michael@0 1227 this._catch(function () {
michael@0 1228 // Make sure we're logged in.
michael@0 1229 if (this._shouldLogin()) {
michael@0 1230 this._log.debug("In sync: should login.");
michael@0 1231 if (!this.login()) {
michael@0 1232 this._log.debug("Not syncing: login returned false.");
michael@0 1233 return;
michael@0 1234 }
michael@0 1235 }
michael@0 1236 else {
michael@0 1237 this._log.trace("In sync: no need to login.");
michael@0 1238 }
michael@0 1239 return this._lockedSync.apply(this, arguments);
michael@0 1240 })();
michael@0 1241 },
michael@0 1242
michael@0 1243 /**
michael@0 1244 * Sync up engines with the server.
michael@0 1245 */
michael@0 1246 _lockedSync: function _lockedSync() {
michael@0 1247 return this._lock("service.js: sync",
michael@0 1248 this._notify("sync", "", function onNotify() {
michael@0 1249
michael@0 1250 let synchronizer = new EngineSynchronizer(this);
michael@0 1251 let cb = Async.makeSpinningCallback();
michael@0 1252 synchronizer.onComplete = cb;
michael@0 1253
michael@0 1254 synchronizer.sync();
michael@0 1255 // wait() throws if the first argument is truthy, which is exactly what
michael@0 1256 // we want.
michael@0 1257 let result = cb.wait();
michael@0 1258
michael@0 1259 // We successfully synchronized. Now let's update our declined engines.
michael@0 1260 let meta = this.recordManager.get(this.metaURL);
michael@0 1261 if (!meta) {
michael@0 1262 this._log.warn("No meta/global; can't update declined state.");
michael@0 1263 return;
michael@0 1264 }
michael@0 1265
michael@0 1266 let declinedEngines = new DeclinedEngines(this);
michael@0 1267 let didChange = declinedEngines.updateDeclined(meta, this.engineManager);
michael@0 1268 if (!didChange) {
michael@0 1269 this._log.info("No change to declined engines. Not reuploading meta/global.");
michael@0 1270 return;
michael@0 1271 }
michael@0 1272
michael@0 1273 this.uploadMetaGlobal(meta);
michael@0 1274 }))();
michael@0 1275 },
michael@0 1276
michael@0 1277 /**
michael@0 1278 * Upload meta/global, throwing the response on failure.
michael@0 1279 */
michael@0 1280 uploadMetaGlobal: function (meta) {
michael@0 1281 this._log.debug("Uploading meta/global: " + JSON.stringify(meta));
michael@0 1282
michael@0 1283 // It would be good to set the X-If-Unmodified-Since header to `timestamp`
michael@0 1284 // for this PUT to ensure at least some level of transactionality.
michael@0 1285 // Unfortunately, the servers don't support it after a wipe right now
michael@0 1286 // (bug 693893), so we're going to defer this until bug 692700.
michael@0 1287 let res = this.resource(this.metaURL);
michael@0 1288 let response = res.put(meta);
michael@0 1289 if (!response.success) {
michael@0 1290 throw response;
michael@0 1291 }
michael@0 1292 this.recordManager.set(this.metaURL, meta);
michael@0 1293 },
michael@0 1294
michael@0 1295 /**
michael@0 1296 * If we have a passphrase, rather than a 25-alphadigit sync key,
michael@0 1297 * use the provided sync ID to bootstrap it using PBKDF2.
michael@0 1298 *
michael@0 1299 * Store the new 'passphrase' back into the identity manager.
michael@0 1300 *
michael@0 1301 * We can check this as often as we want, because once it's done the
michael@0 1302 * check will no longer succeed. It only matters that it happens after
michael@0 1303 * we decide to bump the server storage version.
michael@0 1304 */
michael@0 1305 upgradeSyncKey: function upgradeSyncKey(syncID) {
michael@0 1306 let p = this.identity.syncKey;
michael@0 1307
michael@0 1308 if (!p) {
michael@0 1309 return false;
michael@0 1310 }
michael@0 1311
michael@0 1312 // Check whether it's already a key that we generated.
michael@0 1313 if (Utils.isPassphrase(p)) {
michael@0 1314 this._log.info("Sync key is up-to-date: no need to upgrade.");
michael@0 1315 return true;
michael@0 1316 }
michael@0 1317
michael@0 1318 // Otherwise, let's upgrade it.
michael@0 1319 // N.B., we persist the sync key without testing it first...
michael@0 1320
michael@0 1321 let s = btoa(syncID); // It's what WeaveCrypto expects. *sigh*
michael@0 1322 let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES); // Base 32.
michael@0 1323
michael@0 1324 if (!k) {
michael@0 1325 this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade.");
michael@0 1326 return false;
michael@0 1327 }
michael@0 1328
michael@0 1329 this._log.info("Upgrading sync key...");
michael@0 1330 this.identity.syncKey = k;
michael@0 1331 this._log.info("Saving upgraded sync key...");
michael@0 1332 this.persistLogin();
michael@0 1333 this._log.info("Done saving.");
michael@0 1334 return true;
michael@0 1335 },
michael@0 1336
michael@0 1337 _freshStart: function _freshStart() {
michael@0 1338 this._log.info("Fresh start. Resetting client and considering key upgrade.");
michael@0 1339 this.resetClient();
michael@0 1340 this.collectionKeys.clear();
michael@0 1341 this.upgradeSyncKey(this.syncID);
michael@0 1342
michael@0 1343 // Wipe the server.
michael@0 1344 let wipeTimestamp = this.wipeServer();
michael@0 1345
michael@0 1346 // Upload a new meta/global record.
michael@0 1347 let meta = new WBORecord("meta", "global");
michael@0 1348 meta.payload.syncID = this.syncID;
michael@0 1349 meta.payload.storageVersion = STORAGE_VERSION;
michael@0 1350 meta.payload.declined = this.engineManager.getDeclined();
michael@0 1351 meta.isNew = true;
michael@0 1352
michael@0 1353 // uploadMetaGlobal throws on failure -- including race conditions.
michael@0 1354 // If we got into a race condition, we'll abort the sync this way, too.
michael@0 1355 // That's fine. We'll just wait till the next sync. The client that we're
michael@0 1356 // racing is probably busy uploading stuff right now anyway.
michael@0 1357 this.uploadMetaGlobal(meta);
michael@0 1358
michael@0 1359 // Wipe everything we know about except meta because we just uploaded it
michael@0 1360 let engines = [this.clientsEngine].concat(this.engineManager.getAll());
michael@0 1361 let collections = [engine.name for each (engine in engines)];
michael@0 1362 // TODO: there's a bug here. We should be calling resetClient, no?
michael@0 1363
michael@0 1364 // Generate, upload, and download new keys. Do this last so we don't wipe
michael@0 1365 // them...
michael@0 1366 this.generateNewSymmetricKeys();
michael@0 1367 },
michael@0 1368
michael@0 1369 /**
michael@0 1370 * Wipe user data from the server.
michael@0 1371 *
michael@0 1372 * @param collections [optional]
michael@0 1373 * Array of collections to wipe. If not given, all collections are
michael@0 1374 * wiped by issuing a DELETE request for `storageURL`.
michael@0 1375 *
michael@0 1376 * @return the server's timestamp of the (last) DELETE.
michael@0 1377 */
michael@0 1378 wipeServer: function wipeServer(collections) {
michael@0 1379 let response;
michael@0 1380 if (!collections) {
michael@0 1381 // Strip the trailing slash.
michael@0 1382 let res = this.resource(this.storageURL.slice(0, -1));
michael@0 1383 res.setHeader("X-Confirm-Delete", "1");
michael@0 1384 try {
michael@0 1385 response = res.delete();
michael@0 1386 } catch (ex) {
michael@0 1387 this._log.debug("Failed to wipe server: " + CommonUtils.exceptionStr(ex));
michael@0 1388 throw ex;
michael@0 1389 }
michael@0 1390 if (response.status != 200 && response.status != 404) {
michael@0 1391 this._log.debug("Aborting wipeServer. Server responded with " +
michael@0 1392 response.status + " response for " + this.storageURL);
michael@0 1393 throw response;
michael@0 1394 }
michael@0 1395 return response.headers["x-weave-timestamp"];
michael@0 1396 }
michael@0 1397
michael@0 1398 let timestamp;
michael@0 1399 for (let name of collections) {
michael@0 1400 let url = this.storageURL + name;
michael@0 1401 try {
michael@0 1402 response = this.resource(url).delete();
michael@0 1403 } catch (ex) {
michael@0 1404 this._log.debug("Failed to wipe '" + name + "' collection: " +
michael@0 1405 Utils.exceptionStr(ex));
michael@0 1406 throw ex;
michael@0 1407 }
michael@0 1408
michael@0 1409 if (response.status != 200 && response.status != 404) {
michael@0 1410 this._log.debug("Aborting wipeServer. Server responded with " +
michael@0 1411 response.status + " response for " + url);
michael@0 1412 throw response;
michael@0 1413 }
michael@0 1414
michael@0 1415 if ("x-weave-timestamp" in response.headers) {
michael@0 1416 timestamp = response.headers["x-weave-timestamp"];
michael@0 1417 }
michael@0 1418 }
michael@0 1419
michael@0 1420 return timestamp;
michael@0 1421 },
michael@0 1422
michael@0 1423 /**
michael@0 1424 * Wipe all local user data.
michael@0 1425 *
michael@0 1426 * @param engines [optional]
michael@0 1427 * Array of engine names to wipe. If not given, all engines are used.
michael@0 1428 */
michael@0 1429 wipeClient: function wipeClient(engines) {
michael@0 1430 // If we don't have any engines, reset the service and wipe all engines
michael@0 1431 if (!engines) {
michael@0 1432 // Clear out any service data
michael@0 1433 this.resetService();
michael@0 1434
michael@0 1435 engines = [this.clientsEngine].concat(this.engineManager.getAll());
michael@0 1436 }
michael@0 1437 // Convert the array of names into engines
michael@0 1438 else {
michael@0 1439 engines = this.engineManager.get(engines);
michael@0 1440 }
michael@0 1441
michael@0 1442 // Fully wipe each engine if it's able to decrypt data
michael@0 1443 for each (let engine in engines) {
michael@0 1444 if (engine.canDecrypt()) {
michael@0 1445 engine.wipeClient();
michael@0 1446 }
michael@0 1447 }
michael@0 1448
michael@0 1449 // Save the password/passphrase just in-case they aren't restored by sync
michael@0 1450 this.persistLogin();
michael@0 1451 },
michael@0 1452
michael@0 1453 /**
michael@0 1454 * Wipe all remote user data by wiping the server then telling each remote
michael@0 1455 * client to wipe itself.
michael@0 1456 *
michael@0 1457 * @param engines [optional]
michael@0 1458 * Array of engine names to wipe. If not given, all engines are used.
michael@0 1459 */
michael@0 1460 wipeRemote: function wipeRemote(engines) {
michael@0 1461 try {
michael@0 1462 // Make sure stuff gets uploaded.
michael@0 1463 this.resetClient(engines);
michael@0 1464
michael@0 1465 // Clear out any server data.
michael@0 1466 this.wipeServer(engines);
michael@0 1467
michael@0 1468 // Only wipe the engines provided.
michael@0 1469 if (engines) {
michael@0 1470 engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this);
michael@0 1471 }
michael@0 1472 // Tell the remote machines to wipe themselves.
michael@0 1473 else {
michael@0 1474 this.clientsEngine.sendCommand("wipeAll", []);
michael@0 1475 }
michael@0 1476
michael@0 1477 // Make sure the changed clients get updated.
michael@0 1478 this.clientsEngine.sync();
michael@0 1479 } catch (ex) {
michael@0 1480 this.errorHandler.checkServerError(ex);
michael@0 1481 throw ex;
michael@0 1482 }
michael@0 1483 },
michael@0 1484
michael@0 1485 /**
michael@0 1486 * Reset local service information like logs, sync times, caches.
michael@0 1487 */
michael@0 1488 resetService: function resetService() {
michael@0 1489 this._catch(function reset() {
michael@0 1490 this._log.info("Service reset.");
michael@0 1491
michael@0 1492 // Pretend we've never synced to the server and drop cached data
michael@0 1493 this.syncID = "";
michael@0 1494 this.recordManager.clearCache();
michael@0 1495 })();
michael@0 1496 },
michael@0 1497
michael@0 1498 /**
michael@0 1499 * Reset the client by getting rid of any local server data and client data.
michael@0 1500 *
michael@0 1501 * @param engines [optional]
michael@0 1502 * Array of engine names to reset. If not given, all engines are used.
michael@0 1503 */
michael@0 1504 resetClient: function resetClient(engines) {
michael@0 1505 this._catch(function doResetClient() {
michael@0 1506 // If we don't have any engines, reset everything including the service
michael@0 1507 if (!engines) {
michael@0 1508 // Clear out any service data
michael@0 1509 this.resetService();
michael@0 1510
michael@0 1511 engines = [this.clientsEngine].concat(this.engineManager.getAll());
michael@0 1512 }
michael@0 1513 // Convert the array of names into engines
michael@0 1514 else {
michael@0 1515 engines = this.engineManager.get(engines);
michael@0 1516 }
michael@0 1517
michael@0 1518 // Have each engine drop any temporary meta data
michael@0 1519 for each (let engine in engines) {
michael@0 1520 engine.resetClient();
michael@0 1521 }
michael@0 1522 })();
michael@0 1523 },
michael@0 1524
michael@0 1525 /**
michael@0 1526 * Fetch storage info from the server.
michael@0 1527 *
michael@0 1528 * @param type
michael@0 1529 * String specifying what info to fetch from the server. Must be one
michael@0 1530 * of the INFO_* values. See Sync Storage Server API spec for details.
michael@0 1531 * @param callback
michael@0 1532 * Callback function with signature (error, data) where `data' is
michael@0 1533 * the return value from the server already parsed as JSON.
michael@0 1534 *
michael@0 1535 * @return RESTRequest instance representing the request, allowing callers
michael@0 1536 * to cancel the request.
michael@0 1537 */
michael@0 1538 getStorageInfo: function getStorageInfo(type, callback) {
michael@0 1539 if (STORAGE_INFO_TYPES.indexOf(type) == -1) {
michael@0 1540 throw "Invalid value for 'type': " + type;
michael@0 1541 }
michael@0 1542
michael@0 1543 let info_type = "info/" + type;
michael@0 1544 this._log.trace("Retrieving '" + info_type + "'...");
michael@0 1545 let url = this.userBaseURL + info_type;
michael@0 1546 return this.getStorageRequest(url).get(function onComplete(error) {
michael@0 1547 // Note: 'this' is the request.
michael@0 1548 if (error) {
michael@0 1549 this._log.debug("Failed to retrieve '" + info_type + "': " +
michael@0 1550 Utils.exceptionStr(error));
michael@0 1551 return callback(error);
michael@0 1552 }
michael@0 1553 if (this.response.status != 200) {
michael@0 1554 this._log.debug("Failed to retrieve '" + info_type +
michael@0 1555 "': server responded with HTTP" +
michael@0 1556 this.response.status);
michael@0 1557 return callback(this.response);
michael@0 1558 }
michael@0 1559
michael@0 1560 let result;
michael@0 1561 try {
michael@0 1562 result = JSON.parse(this.response.body);
michael@0 1563 } catch (ex) {
michael@0 1564 this._log.debug("Server returned invalid JSON for '" + info_type +
michael@0 1565 "': " + this.response.body);
michael@0 1566 return callback(ex);
michael@0 1567 }
michael@0 1568 this._log.trace("Successfully retrieved '" + info_type + "'.");
michael@0 1569 return callback(null, result);
michael@0 1570 });
michael@0 1571 },
michael@0 1572 };
michael@0 1573
michael@0 1574 this.Service = new Sync11Service();
michael@0 1575 Service.onStartup();

mercurial