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