michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Service"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: // How long before refreshing the cluster michael@0: const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes michael@0: michael@0: // How long a key to generate from an old passphrase. michael@0: const PBKDF2_KEY_BYTES = 16; michael@0: michael@0: const CRYPTO_COLLECTION = "crypto"; michael@0: const KEYS_WBO = "keys"; michael@0: michael@0: Cu.import("resource://gre/modules/Preferences.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/engines/clients.js"); michael@0: Cu.import("resource://services-sync/identity.js"); michael@0: Cu.import("resource://services-sync/policies.js"); michael@0: Cu.import("resource://services-sync/record.js"); michael@0: Cu.import("resource://services-sync/resource.js"); michael@0: Cu.import("resource://services-sync/rest.js"); michael@0: Cu.import("resource://services-sync/stages/enginesync.js"); michael@0: Cu.import("resource://services-sync/stages/declined.js"); michael@0: Cu.import("resource://services-sync/status.js"); michael@0: Cu.import("resource://services-sync/userapi.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: michael@0: const ENGINE_MODULES = { michael@0: Addons: "addons.js", michael@0: Bookmarks: "bookmarks.js", michael@0: Form: "forms.js", michael@0: History: "history.js", michael@0: Password: "passwords.js", michael@0: Prefs: "prefs.js", michael@0: Tab: "tabs.js", michael@0: }; michael@0: michael@0: const STORAGE_INFO_TYPES = [INFO_COLLECTIONS, michael@0: INFO_COLLECTION_USAGE, michael@0: INFO_COLLECTION_COUNTS, michael@0: INFO_QUOTA]; michael@0: michael@0: michael@0: function Sync11Service() { michael@0: this._notify = Utils.notify("weave:service:"); michael@0: } michael@0: Sync11Service.prototype = { michael@0: michael@0: _lock: Utils.lock, michael@0: _locked: false, michael@0: _loggedIn: false, michael@0: michael@0: infoURL: null, michael@0: storageURL: null, michael@0: metaURL: null, michael@0: cryptoKeyURL: null, michael@0: michael@0: get serverURL() Svc.Prefs.get("serverURL"), michael@0: set serverURL(value) { michael@0: if (!value.endsWith("/")) { michael@0: value += "/"; michael@0: } michael@0: michael@0: // Only do work if it's actually changing michael@0: if (value == this.serverURL) michael@0: return; michael@0: michael@0: // A new server most likely uses a different cluster, so clear that michael@0: Svc.Prefs.set("serverURL", value); michael@0: Svc.Prefs.reset("clusterURL"); michael@0: }, michael@0: michael@0: get clusterURL() Svc.Prefs.get("clusterURL", ""), michael@0: set clusterURL(value) { michael@0: Svc.Prefs.set("clusterURL", value); michael@0: this._updateCachedURLs(); michael@0: }, michael@0: michael@0: get miscAPI() { michael@0: // Append to the serverURL if it's a relative fragment michael@0: let misc = Svc.Prefs.get("miscURL"); michael@0: if (misc.indexOf(":") == -1) michael@0: misc = this.serverURL + misc; michael@0: return misc + MISC_API_VERSION + "/"; michael@0: }, michael@0: michael@0: /** michael@0: * The URI of the User API service. michael@0: * michael@0: * This is the base URI of the service as applicable to all users up to michael@0: * and including the server version path component, complete with trailing michael@0: * forward slash. michael@0: */ michael@0: get userAPIURI() { michael@0: // Append to the serverURL if it's a relative fragment. michael@0: let url = Svc.Prefs.get("userURL"); michael@0: if (!url.contains(":")) { michael@0: url = this.serverURL + url; michael@0: } michael@0: michael@0: return url + USER_API_VERSION + "/"; michael@0: }, michael@0: michael@0: get pwResetURL() { michael@0: return this.serverURL + "weave-password-reset"; michael@0: }, michael@0: michael@0: get syncID() { michael@0: // Generate a random syncID id we don't have one michael@0: let syncID = Svc.Prefs.get("client.syncID", ""); michael@0: return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; michael@0: }, michael@0: set syncID(value) { michael@0: Svc.Prefs.set("client.syncID", value); michael@0: }, michael@0: michael@0: get isLoggedIn() { return this._loggedIn; }, michael@0: michael@0: get locked() { return this._locked; }, michael@0: lock: function lock() { michael@0: if (this._locked) michael@0: return false; michael@0: this._locked = true; michael@0: return true; michael@0: }, michael@0: unlock: function unlock() { michael@0: this._locked = false; michael@0: }, michael@0: michael@0: // A specialized variant of Utils.catch. michael@0: // This provides a more informative error message when we're already syncing: michael@0: // see Bug 616568. michael@0: _catch: function _catch(func) { michael@0: function lockExceptions(ex) { michael@0: if (Utils.isLockException(ex)) { michael@0: // This only happens if we're syncing already. michael@0: this._log.info("Cannot start sync: already syncing?"); michael@0: } michael@0: } michael@0: michael@0: return Utils.catch.call(this, func, lockExceptions); michael@0: }, michael@0: michael@0: get userBaseURL() { michael@0: if (!this._clusterManager) { michael@0: return null; michael@0: } michael@0: return this._clusterManager.getUserBaseURL(); michael@0: }, michael@0: michael@0: _updateCachedURLs: function _updateCachedURLs() { michael@0: // Nothing to cache yet if we don't have the building blocks michael@0: if (!this.clusterURL || !this.identity.username) michael@0: return; michael@0: michael@0: this._log.debug("Caching URLs under storage user base: " + this.userBaseURL); michael@0: michael@0: // Generate and cache various URLs under the storage API for this user michael@0: this.infoURL = this.userBaseURL + "info/collections"; michael@0: this.storageURL = this.userBaseURL + "storage/"; michael@0: this.metaURL = this.storageURL + "meta/global"; michael@0: this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO; michael@0: }, michael@0: michael@0: _checkCrypto: function _checkCrypto() { michael@0: let ok = false; michael@0: michael@0: try { michael@0: let iv = Svc.Crypto.generateRandomIV(); michael@0: if (iv.length == 24) michael@0: ok = true; michael@0: michael@0: } catch (e) { michael@0: this._log.debug("Crypto check failed: " + e); michael@0: } michael@0: michael@0: return ok; michael@0: }, michael@0: michael@0: /** michael@0: * Here is a disgusting yet reasonable way of handling HMAC errors deep in michael@0: * the guts of Sync. The astute reader will note that this is a hacky way of michael@0: * implementing something like continuable conditions. michael@0: * michael@0: * A handler function is glued to each engine. If the engine discovers an michael@0: * HMAC failure, we fetch keys from the server and update our keys, just as michael@0: * we would on startup. michael@0: * michael@0: * If our key collection changed, we signal to the engine (via our return michael@0: * value) that it should retry decryption. michael@0: * michael@0: * If our key collection did not change, it means that we already had the michael@0: * correct keys... and thus a different client has the wrong ones. Reupload michael@0: * the bundle that we fetched, which will bump the modified time on the michael@0: * server and (we hope) prompt a broken client to fix itself. michael@0: * michael@0: * We keep track of the time at which we last applied this reasoning, because michael@0: * thrashing doesn't solve anything. We keep a reasonable interval between michael@0: * these remedial actions. michael@0: */ michael@0: lastHMACEvent: 0, michael@0: michael@0: /* michael@0: * Returns whether to try again. michael@0: */ michael@0: handleHMACEvent: function handleHMACEvent() { michael@0: let now = Date.now(); michael@0: michael@0: // Leave a sizable delay between HMAC recovery attempts. This gives us michael@0: // time for another client to fix themselves if we touch the record. michael@0: if ((now - this.lastHMACEvent) < HMAC_EVENT_INTERVAL) michael@0: return false; michael@0: michael@0: this._log.info("Bad HMAC event detected. Attempting recovery " + michael@0: "or signaling to other clients."); michael@0: michael@0: // Set the last handled time so that we don't act again. michael@0: this.lastHMACEvent = now; michael@0: michael@0: // Fetch keys. michael@0: let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); michael@0: try { michael@0: let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; michael@0: michael@0: // Save out the ciphertext for when we reupload. If there's a bug in michael@0: // CollectionKeyManager, this will prevent us from uploading junk. michael@0: let cipherText = cryptoKeys.ciphertext; michael@0: michael@0: if (!cryptoResp.success) { michael@0: this._log.warn("Failed to download keys."); michael@0: return false; michael@0: } michael@0: michael@0: let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle, michael@0: cryptoKeys, true); michael@0: if (keysChanged) { michael@0: // Did they change? If so, carry on. michael@0: this._log.info("Suggesting retry."); michael@0: return true; // Try again. michael@0: } michael@0: michael@0: // If not, reupload them and continue the current sync. michael@0: cryptoKeys.ciphertext = cipherText; michael@0: cryptoKeys.cleartext = null; michael@0: michael@0: let uploadResp = cryptoKeys.upload(this.resource(this.cryptoKeysURL)); michael@0: if (uploadResp.success) michael@0: this._log.info("Successfully re-uploaded keys. Continuing sync."); michael@0: else michael@0: this._log.warn("Got error response re-uploading keys. " + michael@0: "Continuing sync; let's try again later."); michael@0: michael@0: return false; // Don't try again: same keys. michael@0: michael@0: } catch (ex) { michael@0: this._log.warn("Got exception \"" + ex + "\" fetching and handling " + michael@0: "crypto keys. Will try again later."); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) { michael@0: // Don't want to wipe if we're just starting up! michael@0: let wasBlank = this.collectionKeys.isClear; michael@0: let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys); michael@0: michael@0: if (keysChanged && !wasBlank) { michael@0: this._log.debug("Keys changed: " + JSON.stringify(keysChanged)); michael@0: michael@0: if (!skipReset) { michael@0: this._log.info("Resetting client to reflect key change."); michael@0: michael@0: if (keysChanged.length) { michael@0: // Collection keys only. Reset individual engines. michael@0: this.resetClient(keysChanged); michael@0: } michael@0: else { michael@0: // Default key changed: wipe it all. michael@0: this.resetClient(); michael@0: } michael@0: michael@0: this._log.info("Downloaded new keys, client reset. Proceeding."); michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Prepare to initialize the rest of Weave after waiting a little bit michael@0: */ michael@0: onStartup: function onStartup() { michael@0: this._migratePrefs(); michael@0: michael@0: // Status is instantiated before us and is the first to grab an instance of michael@0: // the IdentityManager. We use that instance because IdentityManager really michael@0: // needs to be a singleton. Ideally, the longer-lived object would spawn michael@0: // this service instance. michael@0: if (!Status || !Status._authManager) { michael@0: throw new Error("Status or Status._authManager not initialized."); michael@0: } michael@0: michael@0: this.status = Status; michael@0: this.identity = Status._authManager; michael@0: this.collectionKeys = new CollectionKeyManager(); michael@0: michael@0: this.errorHandler = new ErrorHandler(this); michael@0: michael@0: this._log = Log.repository.getLogger("Sync.Service"); michael@0: this._log.level = michael@0: Log.Level[Svc.Prefs.get("log.logger.service.main")]; michael@0: michael@0: this._log.info("Loading Weave " + WEAVE_VERSION); michael@0: michael@0: this._clusterManager = this.identity.createClusterManager(this); michael@0: this.recordManager = new RecordManager(this); michael@0: michael@0: this.enabled = true; michael@0: michael@0: this._registerEngines(); michael@0: michael@0: let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. michael@0: getService(Ci.nsIHttpProtocolHandler).userAgent; michael@0: this._log.info(ua); michael@0: michael@0: if (!this._checkCrypto()) { michael@0: this.enabled = false; michael@0: this._log.info("Could not load the Weave crypto component. Disabling " + michael@0: "Weave, since it will not work correctly."); michael@0: } michael@0: michael@0: Svc.Obs.add("weave:service:setup-complete", this); michael@0: Svc.Prefs.observe("engine.", this); michael@0: michael@0: this.scheduler = new SyncScheduler(this); michael@0: michael@0: if (!this.enabled) { michael@0: this._log.info("Firefox Sync disabled."); michael@0: } michael@0: michael@0: this._updateCachedURLs(); michael@0: michael@0: let status = this._checkSetup(); michael@0: if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) { michael@0: Svc.Obs.notify("weave:engine:start-tracking"); michael@0: } michael@0: michael@0: // Send an event now that Weave service is ready. We don't do this michael@0: // synchronously so that observers can import this module before michael@0: // registering an observer. michael@0: Utils.nextTick(function onNextTick() { michael@0: this.status.ready = true; michael@0: michael@0: // UI code uses the flag on the XPCOM service so it doesn't have michael@0: // to load a bunch of modules. michael@0: let xps = Cc["@mozilla.org/weave/service;1"] michael@0: .getService(Ci.nsISupports) michael@0: .wrappedJSObject; michael@0: xps.ready = true; michael@0: michael@0: Svc.Obs.notify("weave:service:ready"); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: _checkSetup: function _checkSetup() { michael@0: if (!this.enabled) { michael@0: return this.status.service = STATUS_DISABLED; michael@0: } michael@0: return this.status.checkSetup(); michael@0: }, michael@0: michael@0: _migratePrefs: function _migratePrefs() { michael@0: // Migrate old debugLog prefs. michael@0: let logLevel = Svc.Prefs.get("log.appender.debugLog"); michael@0: if (logLevel) { michael@0: Svc.Prefs.set("log.appender.file.level", logLevel); michael@0: Svc.Prefs.reset("log.appender.debugLog"); michael@0: } michael@0: if (Svc.Prefs.get("log.appender.debugLog.enabled")) { michael@0: Svc.Prefs.set("log.appender.file.logOnSuccess", true); michael@0: Svc.Prefs.reset("log.appender.debugLog.enabled"); michael@0: } michael@0: michael@0: // Migrate old extensions.weave.* prefs if we haven't already tried. michael@0: if (Svc.Prefs.get("migrated", false)) michael@0: return; michael@0: michael@0: // Grab the list of old pref names michael@0: let oldPrefBranch = "extensions.weave."; michael@0: let oldPrefNames = Cc["@mozilla.org/preferences-service;1"]. michael@0: getService(Ci.nsIPrefService). michael@0: getBranch(oldPrefBranch). michael@0: getChildList("", {}); michael@0: michael@0: // Map each old pref to the current pref branch michael@0: let oldPref = new Preferences(oldPrefBranch); michael@0: for each (let pref in oldPrefNames) michael@0: Svc.Prefs.set(pref, oldPref.get(pref)); michael@0: michael@0: // Remove all the old prefs and remember that we've migrated michael@0: oldPref.resetBranch(""); michael@0: Svc.Prefs.set("migrated", true); michael@0: }, michael@0: michael@0: /** michael@0: * Register the built-in engines for certain applications michael@0: */ michael@0: _registerEngines: function _registerEngines() { michael@0: this.engineManager = new EngineManager(this); michael@0: michael@0: let engines = []; michael@0: // Applications can provide this preference (comma-separated list) michael@0: // to specify which engines should be registered on startup. michael@0: let pref = Svc.Prefs.get("registerEngines"); michael@0: if (pref) { michael@0: engines = pref.split(","); michael@0: } michael@0: michael@0: let declined = []; michael@0: pref = Svc.Prefs.get("declinedEngines"); michael@0: if (pref) { michael@0: declined = pref.split(","); michael@0: } michael@0: michael@0: this.clientsEngine = new ClientEngine(this); michael@0: michael@0: for (let name of engines) { michael@0: if (!name in ENGINE_MODULES) { michael@0: this._log.info("Do not know about engine: " + name); michael@0: continue; michael@0: } michael@0: michael@0: let ns = {}; michael@0: try { michael@0: Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns); michael@0: michael@0: let engineName = name + "Engine"; michael@0: if (!(engineName in ns)) { michael@0: this._log.warn("Could not find exported engine instance: " + engineName); michael@0: continue; michael@0: } michael@0: michael@0: this.engineManager.register(ns[engineName]); michael@0: } catch (ex) { michael@0: this._log.warn("Could not register engine " + name + ": " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: michael@0: this.engineManager.setDeclined(declined); michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: // nsIObserver michael@0: michael@0: observe: function observe(subject, topic, data) { michael@0: switch (topic) { michael@0: case "weave:service:setup-complete": michael@0: let status = this._checkSetup(); michael@0: if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) michael@0: Svc.Obs.notify("weave:engine:start-tracking"); michael@0: break; michael@0: case "nsPref:changed": michael@0: if (this._ignorePrefObserver) michael@0: return; michael@0: let engine = data.slice((PREFS_BRANCH + "engine.").length); michael@0: this._handleEngineStatusChanged(engine); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _handleEngineStatusChanged: function handleEngineDisabled(engine) { michael@0: this._log.trace("Status for " + engine + " engine changed."); michael@0: if (Svc.Prefs.get("engineStatusChanged." + engine, false)) { michael@0: // The enabled status being changed back to what it was before. michael@0: Svc.Prefs.reset("engineStatusChanged." + engine); michael@0: } else { michael@0: // Remember that the engine status changed locally until the next sync. michael@0: Svc.Prefs.set("engineStatusChanged." + engine, true); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a Resource instance with authentication credentials. michael@0: */ michael@0: resource: function resource(url) { michael@0: let res = new Resource(url); michael@0: res.authenticator = this.identity.getResourceAuthenticator(); michael@0: michael@0: return res; michael@0: }, michael@0: michael@0: /** michael@0: * Obtain a SyncStorageRequest instance with authentication credentials. michael@0: */ michael@0: getStorageRequest: function getStorageRequest(url) { michael@0: let request = new SyncStorageRequest(url); michael@0: request.authenticator = this.identity.getRESTRequestAuthenticator(); michael@0: michael@0: return request; michael@0: }, michael@0: michael@0: /** michael@0: * Perform the info fetch as part of a login or key fetch, or michael@0: * inside engine sync. michael@0: */ michael@0: _fetchInfo: function (url) { michael@0: let infoURL = url || this.infoURL; michael@0: michael@0: this._log.trace("In _fetchInfo: " + infoURL); michael@0: let info; michael@0: try { michael@0: info = this.resource(infoURL).get(); michael@0: } catch (ex) { michael@0: this.errorHandler.checkServerError(ex); michael@0: throw ex; michael@0: } michael@0: michael@0: // Always check for errors; this is also where we look for X-Weave-Alert. michael@0: this.errorHandler.checkServerError(info); michael@0: if (!info.success) { michael@0: throw "Aborting sync: failed to get collections."; michael@0: } michael@0: return info; michael@0: }, michael@0: michael@0: verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) { michael@0: michael@0: this._log.debug("Fetching and verifying -- or generating -- symmetric keys."); michael@0: michael@0: // Don't allow empty/missing passphrase. michael@0: // Furthermore, we assume that our sync key is already upgraded, michael@0: // and fail if that assumption is invalidated. michael@0: michael@0: if (!this.identity.syncKey) { michael@0: this.status.login = LOGIN_FAILED_NO_PASSPHRASE; michael@0: this.status.sync = CREDENTIALS_CHANGED; michael@0: return false; michael@0: } michael@0: michael@0: let syncKeyBundle = this.identity.syncKeyBundle; michael@0: if (!syncKeyBundle) { michael@0: this._log.error("Sync Key Bundle not set. Invalid Sync Key?"); michael@0: michael@0: this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; michael@0: this.status.sync = CREDENTIALS_CHANGED; michael@0: return false; michael@0: } michael@0: michael@0: try { michael@0: if (!infoResponse) michael@0: infoResponse = this._fetchInfo(); // Will throw an exception on failure. michael@0: michael@0: // This only applies when the server is already at version 4. michael@0: if (infoResponse.status != 200) { michael@0: this._log.warn("info/collections returned non-200 response. Failing key fetch."); michael@0: this.status.login = LOGIN_FAILED_SERVER_ERROR; michael@0: this.errorHandler.checkServerError(infoResponse); michael@0: return false; michael@0: } michael@0: michael@0: let infoCollections = infoResponse.obj; michael@0: michael@0: this._log.info("Testing info/collections: " + JSON.stringify(infoCollections)); michael@0: michael@0: if (this.collectionKeys.updateNeeded(infoCollections)) { michael@0: this._log.info("collection keys reports that a key update is needed."); michael@0: michael@0: // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this. michael@0: michael@0: // Fetch storage/crypto/keys. michael@0: let cryptoKeys; michael@0: michael@0: if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) { michael@0: try { michael@0: cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); michael@0: let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; michael@0: michael@0: if (cryptoResp.success) { michael@0: let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys); michael@0: return true; michael@0: } michael@0: else if (cryptoResp.status == 404) { michael@0: // On failure, ask to generate new keys and upload them. michael@0: // Fall through to the behavior below. michael@0: this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating."); michael@0: cryptoKeys = null; michael@0: } michael@0: else { michael@0: // Some other problem. michael@0: this.status.login = LOGIN_FAILED_SERVER_ERROR; michael@0: this.errorHandler.checkServerError(cryptoResp); michael@0: this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys."); michael@0: return false; michael@0: } michael@0: } michael@0: catch (ex) { michael@0: this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys."); michael@0: // TODO: Um, what exceptions might we get here? Should we re-throw any? michael@0: michael@0: // One kind of exception: HMAC failure. michael@0: if (Utils.isHMACMismatch(ex)) { michael@0: this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; michael@0: this.status.sync = CREDENTIALS_CHANGED; michael@0: } michael@0: else { michael@0: // In the absence of further disambiguation or more precise michael@0: // failure constants, just report failure. michael@0: this.status.login = LOGIN_FAILED; michael@0: } michael@0: return false; michael@0: } michael@0: } michael@0: else { michael@0: this._log.info("... 'crypto' is not a reported collection. Generating new keys."); michael@0: } michael@0: michael@0: if (!cryptoKeys) { michael@0: this._log.info("No keys! Generating new ones."); michael@0: michael@0: // Better make some and upload them, and wipe the server to ensure michael@0: // consistency. This is all achieved via _freshStart. michael@0: // If _freshStart fails to clear the server or upload keys, it will michael@0: // throw. michael@0: this._freshStart(); michael@0: return true; michael@0: } michael@0: michael@0: // Last-ditch case. michael@0: return false; michael@0: } michael@0: else { michael@0: // No update needed: we're good! michael@0: return true; michael@0: } michael@0: michael@0: } catch (ex) { michael@0: // This means no keys are present, or there's a network error. michael@0: this._log.debug("Failed to fetch and verify keys: " michael@0: + Utils.exceptionStr(ex)); michael@0: this.errorHandler.checkServerError(ex); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: verifyLogin: function verifyLogin(allow40XRecovery = true) { michael@0: // If the identity isn't ready it might not know the username... michael@0: if (!this.identity.readyToAuthenticate) { michael@0: this._log.info("Not ready to authenticate in verifyLogin."); michael@0: this.status.login = LOGIN_FAILED_NOT_READY; michael@0: return false; michael@0: } michael@0: michael@0: if (!this.identity.username) { michael@0: this._log.warn("No username in verifyLogin."); michael@0: this.status.login = LOGIN_FAILED_NO_USERNAME; michael@0: return false; michael@0: } michael@0: michael@0: // Unlock master password, or return. michael@0: // Attaching auth credentials to a request requires access to michael@0: // passwords, which means that Resource.get can throw MP-related michael@0: // exceptions! michael@0: // Try to fetch the passphrase first, while we still have control. michael@0: try { michael@0: this.identity.syncKey; michael@0: } catch (ex) { michael@0: this._log.debug("Fetching passphrase threw " + ex + michael@0: "; assuming master password locked."); michael@0: this.status.login = MASTER_PASSWORD_LOCKED; michael@0: return false; michael@0: } michael@0: michael@0: try { michael@0: // Make sure we have a cluster to verify against. michael@0: // This is a little weird, if we don't get a node we pretend michael@0: // to succeed, since that probably means we just don't have storage. michael@0: if (this.clusterURL == "" && !this._clusterManager.setCluster()) { michael@0: this.status.sync = NO_SYNC_NODE_FOUND; michael@0: return true; michael@0: } michael@0: michael@0: // Fetch collection info on every startup. michael@0: let test = this.resource(this.infoURL).get(); michael@0: michael@0: switch (test.status) { michael@0: case 200: michael@0: // The user is authenticated. michael@0: michael@0: // We have no way of verifying the passphrase right now, michael@0: // so wait until remoteSetup to do so. michael@0: // Just make the most trivial checks. michael@0: if (!this.identity.syncKey) { michael@0: this._log.warn("No passphrase in verifyLogin."); michael@0: this.status.login = LOGIN_FAILED_NO_PASSPHRASE; michael@0: return false; michael@0: } michael@0: michael@0: // Go ahead and do remote setup, so that we can determine michael@0: // conclusively that our passphrase is correct. michael@0: if (this._remoteSetup()) { michael@0: // Username/password verified. michael@0: this.status.login = LOGIN_SUCCEEDED; michael@0: return true; michael@0: } michael@0: michael@0: this._log.warn("Remote setup failed."); michael@0: // Remote setup must have failed. michael@0: return false; michael@0: michael@0: case 401: michael@0: this._log.warn("401: login failed."); michael@0: // Fall through to the 404 case. michael@0: michael@0: case 404: michael@0: // Check that we're verifying with the correct cluster michael@0: if (allow40XRecovery && this._clusterManager.setCluster()) { michael@0: return this.verifyLogin(false); michael@0: } michael@0: michael@0: // We must have the right cluster, but the server doesn't expect us michael@0: this.status.login = LOGIN_FAILED_LOGIN_REJECTED; michael@0: return false; michael@0: michael@0: default: michael@0: // Server didn't respond with something that we expected michael@0: this.status.login = LOGIN_FAILED_SERVER_ERROR; michael@0: this.errorHandler.checkServerError(test); michael@0: return false; michael@0: } michael@0: } catch (ex) { michael@0: // Must have failed on some network issue michael@0: this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex)); michael@0: this.status.login = LOGIN_FAILED_NETWORK_ERROR; michael@0: this.errorHandler.checkServerError(ex); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: generateNewSymmetricKeys: function generateNewSymmetricKeys() { michael@0: this._log.info("Generating new keys WBO..."); michael@0: let wbo = this.collectionKeys.generateNewKeysWBO(); michael@0: this._log.info("Encrypting new key bundle."); michael@0: wbo.encrypt(this.identity.syncKeyBundle); michael@0: michael@0: this._log.info("Uploading..."); michael@0: let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL)); michael@0: if (uploadRes.status != 200) { michael@0: this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!"); michael@0: this.errorHandler.checkServerError(uploadRes); michael@0: throw new Error("Unable to upload symmetric keys."); michael@0: } michael@0: this._log.info("Got status " + uploadRes.status + " uploading keys."); michael@0: let serverModified = uploadRes.obj; // Modified timestamp according to server. michael@0: this._log.debug("Server reports crypto modified: " + serverModified); michael@0: michael@0: // Now verify that info/collections shows them! michael@0: this._log.debug("Verifying server collection records."); michael@0: let info = this._fetchInfo(); michael@0: this._log.debug("info/collections is: " + info); michael@0: michael@0: if (info.status != 200) { michael@0: this._log.warn("Non-200 info/collections response. Aborting."); michael@0: throw new Error("Unable to upload symmetric keys."); michael@0: } michael@0: michael@0: info = info.obj; michael@0: if (!(CRYPTO_COLLECTION in info)) { michael@0: this._log.error("Consistency failure: info/collections excludes " + michael@0: "crypto after successful upload."); michael@0: throw new Error("Symmetric key upload failed."); michael@0: } michael@0: michael@0: // Can't check against local modified: clock drift. michael@0: if (info[CRYPTO_COLLECTION] < serverModified) { michael@0: this._log.error("Consistency failure: info/collections crypto entry " + michael@0: "is stale after successful upload."); michael@0: throw new Error("Symmetric key upload failed."); michael@0: } michael@0: michael@0: // Doesn't matter if the timestamp is ahead. michael@0: michael@0: // Download and install them. michael@0: let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); michael@0: let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; michael@0: if (cryptoResp.status != 200) { michael@0: this._log.warn("Failed to download keys."); michael@0: throw new Error("Symmetric key download failed."); michael@0: } michael@0: let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle, michael@0: cryptoKeys, true); michael@0: if (keysChanged) { michael@0: this._log.info("Downloaded keys differed, as expected."); michael@0: } michael@0: }, michael@0: michael@0: changePassword: function changePassword(newPassword) { michael@0: let client = new UserAPI10Client(this.userAPIURI); michael@0: let cb = Async.makeSpinningCallback(); michael@0: client.changePassword(this.identity.username, michael@0: this.identity.basicPassword, newPassword, cb); michael@0: michael@0: try { michael@0: cb.wait(); michael@0: } catch (ex) { michael@0: this._log.debug("Password change failed: " + michael@0: CommonUtils.exceptionStr(ex)); michael@0: return false; michael@0: } michael@0: michael@0: // Save the new password for requests and login manager. michael@0: this.identity.basicPassword = newPassword; michael@0: this.persistLogin(); michael@0: return true; michael@0: }, michael@0: michael@0: changePassphrase: function changePassphrase(newphrase) { michael@0: return this._catch(function doChangePasphrase() { michael@0: /* Wipe. */ michael@0: this.wipeServer(); michael@0: michael@0: this.logout(); michael@0: michael@0: /* Set this so UI is updated on next run. */ michael@0: this.identity.syncKey = newphrase; michael@0: this.persistLogin(); michael@0: michael@0: /* We need to re-encrypt everything, so reset. */ michael@0: this.resetClient(); michael@0: this.collectionKeys.clear(); michael@0: michael@0: /* Login and sync. This also generates new keys. */ michael@0: this.sync(); michael@0: michael@0: Svc.Obs.notify("weave:service:change-passphrase", true); michael@0: michael@0: return true; michael@0: })(); michael@0: }, michael@0: michael@0: startOver: function startOver() { michael@0: this._log.trace("Invoking Service.startOver."); michael@0: Svc.Obs.notify("weave:engine:stop-tracking"); michael@0: this.status.resetSync(); michael@0: michael@0: // Deletion doesn't make sense if we aren't set up yet! michael@0: if (this.clusterURL != "") { michael@0: // Clear client-specific data from the server, including disabled engines. michael@0: for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) { michael@0: try { michael@0: engine.removeClientData(); michael@0: } catch(ex) { michael@0: this._log.warn("Deleting client data for " + engine.name + " failed:" michael@0: + Utils.exceptionStr(ex)); michael@0: } michael@0: } michael@0: this._log.debug("Finished deleting client data."); michael@0: } else { michael@0: this._log.debug("Skipping client data removal: no cluster URL."); michael@0: } michael@0: michael@0: // We want let UI consumers of the following notification know as soon as michael@0: // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now michael@0: // by emptying the passphrase (we still need the password). michael@0: this._log.info("Service.startOver dropping sync key and logging out."); michael@0: this.identity.resetSyncKey(); michael@0: this.status.login = LOGIN_FAILED_NO_PASSPHRASE; michael@0: this.logout(); michael@0: Svc.Obs.notify("weave:service:start-over"); michael@0: michael@0: // Reset all engines and clear keys. michael@0: this.resetClient(); michael@0: this.collectionKeys.clear(); michael@0: this.status.resetBackoff(); michael@0: michael@0: // Reset Weave prefs. michael@0: this._ignorePrefObserver = true; michael@0: Svc.Prefs.resetBranch(""); michael@0: this._ignorePrefObserver = false; michael@0: michael@0: Svc.Prefs.set("lastversion", WEAVE_VERSION); michael@0: michael@0: this.identity.deleteSyncCredentials(); michael@0: michael@0: // If necessary, reset the identity manager, then re-initialize it so the michael@0: // FxA manager is used. This is configurable via a pref - mainly for tests. michael@0: let keepIdentity = false; michael@0: try { michael@0: keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity"); michael@0: } catch (_) { /* no such pref */ } michael@0: if (keepIdentity) { michael@0: Svc.Obs.notify("weave:service:start-over:finish"); michael@0: return; michael@0: } michael@0: michael@0: this.identity.finalize().then( michael@0: () => { michael@0: this.identity.username = ""; michael@0: this.status.__authManager = null; michael@0: this.identity = Status._authManager; michael@0: this._clusterManager = this.identity.createClusterManager(this); michael@0: Svc.Obs.notify("weave:service:start-over:finish"); michael@0: } michael@0: ).then(null, michael@0: err => { michael@0: this._log.error("startOver failed to re-initialize the identity manager: " + err); michael@0: // Still send the observer notification so the current state is michael@0: // reflected in the UI. michael@0: Svc.Obs.notify("weave:service:start-over:finish"); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: persistLogin: function persistLogin() { michael@0: try { michael@0: this.identity.persistCredentials(true); michael@0: } catch (ex) { michael@0: this._log.info("Unable to persist credentials: " + ex); michael@0: } michael@0: }, michael@0: michael@0: login: function login(username, password, passphrase) { michael@0: function onNotify() { michael@0: this._loggedIn = false; michael@0: if (Services.io.offline) { michael@0: this.status.login = LOGIN_FAILED_NETWORK_ERROR; michael@0: throw "Application is offline, login should not be called"; michael@0: } michael@0: michael@0: let initialStatus = this._checkSetup(); michael@0: if (username) { michael@0: this.identity.username = username; michael@0: } michael@0: if (password) { michael@0: this.identity.basicPassword = password; michael@0: } michael@0: if (passphrase) { michael@0: this.identity.syncKey = passphrase; michael@0: } michael@0: michael@0: if (this._checkSetup() == CLIENT_NOT_CONFIGURED) { michael@0: throw "Aborting login, client not configured."; michael@0: } michael@0: michael@0: // Ask the identity manager to explicitly login now. michael@0: let cb = Async.makeSpinningCallback(); michael@0: this.identity.ensureLoggedIn().then(cb, cb); michael@0: michael@0: // Just let any errors bubble up - they've more context than we do! michael@0: cb.wait(); michael@0: michael@0: // Calling login() with parameters when the client was michael@0: // previously not configured means setup was completed. michael@0: if (initialStatus == CLIENT_NOT_CONFIGURED michael@0: && (username || password || passphrase)) { michael@0: Svc.Obs.notify("weave:service:setup-complete"); michael@0: } michael@0: this._log.info("Logging in the user."); michael@0: this._updateCachedURLs(); michael@0: michael@0: if (!this.verifyLogin()) { michael@0: // verifyLogin sets the failure states here. michael@0: throw "Login failed: " + this.status.login; michael@0: } michael@0: michael@0: this._loggedIn = true; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: let notifier = this._notify("login", "", onNotify.bind(this)); michael@0: return this._catch(this._lock("service.js: login", notifier))(); michael@0: }, michael@0: michael@0: logout: function logout() { michael@0: // If we failed during login, we aren't going to have this._loggedIn set, michael@0: // but we still want to ask the identity to logout, so it doesn't try and michael@0: // reuse any old credentials next time we sync. michael@0: this._log.info("Logging out"); michael@0: this.identity.logout(); michael@0: this._loggedIn = false; michael@0: michael@0: Svc.Obs.notify("weave:service:logout:finish"); michael@0: }, michael@0: michael@0: checkAccount: function checkAccount(account) { michael@0: let client = new UserAPI10Client(this.userAPIURI); michael@0: let cb = Async.makeSpinningCallback(); michael@0: michael@0: let username = this.identity.usernameFromAccount(account); michael@0: client.usernameExists(username, cb); michael@0: michael@0: try { michael@0: let exists = cb.wait(); michael@0: return exists ? "notAvailable" : "available"; michael@0: } catch (ex) { michael@0: // TODO fix API convention. michael@0: return this.errorHandler.errorStr(ex); michael@0: } michael@0: }, michael@0: michael@0: createAccount: function createAccount(email, password, michael@0: captchaChallenge, captchaResponse) { michael@0: let client = new UserAPI10Client(this.userAPIURI); michael@0: michael@0: // Hint to server to allow scripted user creation or otherwise michael@0: // ignore captcha. michael@0: if (Svc.Prefs.isSet("admin-secret")) { michael@0: client.adminSecret = Svc.Prefs.get("admin-secret", ""); michael@0: } michael@0: michael@0: let cb = Async.makeSpinningCallback(); michael@0: michael@0: client.createAccount(email, password, captchaChallenge, captchaResponse, michael@0: cb); michael@0: michael@0: try { michael@0: cb.wait(); michael@0: return null; michael@0: } catch (ex) { michael@0: return this.errorHandler.errorStr(ex.body); michael@0: } michael@0: }, michael@0: michael@0: // Stuff we need to do after login, before we can really do michael@0: // anything (e.g. key setup). michael@0: _remoteSetup: function _remoteSetup(infoResponse) { michael@0: let reset = false; michael@0: michael@0: this._log.debug("Fetching global metadata record"); michael@0: let meta = this.recordManager.get(this.metaURL); michael@0: michael@0: // Checking modified time of the meta record. michael@0: if (infoResponse && michael@0: (infoResponse.obj.meta != this.metaModified) && michael@0: (!meta || !meta.isNew)) { michael@0: michael@0: // Delete the cached meta record... michael@0: this._log.debug("Clearing cached meta record. metaModified is " + michael@0: JSON.stringify(this.metaModified) + ", setting to " + michael@0: JSON.stringify(infoResponse.obj.meta)); michael@0: michael@0: this.recordManager.del(this.metaURL); michael@0: michael@0: // ... fetch the current record from the server, and COPY THE FLAGS. michael@0: let newMeta = this.recordManager.get(this.metaURL); michael@0: michael@0: // If we got a 401, we do not want to create a new meta/global - we michael@0: // should be able to get the existing meta after we get a new node. michael@0: if (this.recordManager.response.status == 401) { michael@0: this._log.debug("Fetching meta/global record on the server returned 401."); michael@0: this.errorHandler.checkServerError(this.recordManager.response); michael@0: return false; michael@0: } michael@0: michael@0: if (!this.recordManager.response.success || !newMeta) { michael@0: this._log.debug("No meta/global record on the server. Creating one."); michael@0: newMeta = new WBORecord("meta", "global"); michael@0: newMeta.payload.syncID = this.syncID; michael@0: newMeta.payload.storageVersion = STORAGE_VERSION; michael@0: newMeta.payload.declined = this.engineManager.getDeclined(); michael@0: michael@0: newMeta.isNew = true; michael@0: michael@0: this.recordManager.set(this.metaURL, newMeta); michael@0: if (!newMeta.upload(this.resource(this.metaURL)).success) { michael@0: this._log.warn("Unable to upload new meta/global. Failing remote setup."); michael@0: return false; michael@0: } michael@0: } else { michael@0: // If newMeta, then it stands to reason that meta != null. michael@0: newMeta.isNew = meta.isNew; michael@0: newMeta.changed = meta.changed; michael@0: } michael@0: michael@0: // Switch in the new meta object and record the new time. michael@0: meta = newMeta; michael@0: this.metaModified = infoResponse.obj.meta; michael@0: } michael@0: michael@0: let remoteVersion = (meta && meta.payload.storageVersion)? michael@0: meta.payload.storageVersion : ""; michael@0: michael@0: this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:", michael@0: STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" ")); michael@0: michael@0: // Check for cases that require a fresh start. When comparing remoteVersion, michael@0: // we need to convert it to a number as older clients used it as a string. michael@0: if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || michael@0: STORAGE_VERSION > parseFloat(remoteVersion)) { michael@0: michael@0: this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed."); michael@0: michael@0: // abort the server wipe if the GET status was anything other than 404 or 200 michael@0: let status = this.recordManager.response.status; michael@0: if (status != 200 && status != 404) { michael@0: this.status.sync = METARECORD_DOWNLOAD_FAIL; michael@0: this.errorHandler.checkServerError(this.recordManager.response); michael@0: this._log.warn("Unknown error while downloading metadata record. " + michael@0: "Aborting sync."); michael@0: return false; michael@0: } michael@0: michael@0: if (!meta) michael@0: this._log.info("No metadata record, server wipe needed"); michael@0: if (meta && !meta.payload.syncID) michael@0: this._log.warn("No sync id, server wipe needed"); michael@0: michael@0: reset = true; michael@0: michael@0: this._log.info("Wiping server data"); michael@0: this._freshStart(); michael@0: michael@0: if (status == 404) michael@0: this._log.info("Metadata record not found, server was wiped to ensure " + michael@0: "consistency."); michael@0: else // 200 michael@0: this._log.info("Wiped server; incompatible metadata: " + remoteVersion); michael@0: michael@0: return true; michael@0: } michael@0: else if (remoteVersion > STORAGE_VERSION) { michael@0: this.status.sync = VERSION_OUT_OF_DATE; michael@0: this._log.warn("Upgrade required to access newer storage version."); michael@0: return false; michael@0: } michael@0: else if (meta.payload.syncID != this.syncID) { michael@0: michael@0: this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID); michael@0: this.resetClient(); michael@0: this.collectionKeys.clear(); michael@0: this.syncID = meta.payload.syncID; michael@0: this._log.debug("Clear cached values and take syncId: " + this.syncID); michael@0: michael@0: if (!this.upgradeSyncKey(meta.payload.syncID)) { michael@0: this._log.warn("Failed to upgrade sync key. Failing remote setup."); michael@0: return false; michael@0: } michael@0: michael@0: if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { michael@0: this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); michael@0: return false; michael@0: } michael@0: michael@0: // bug 545725 - re-verify creds and fail sanely michael@0: if (!this.verifyLogin()) { michael@0: this.status.sync = CREDENTIALS_CHANGED; michael@0: this._log.info("Credentials have changed, aborting sync and forcing re-login."); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: else { michael@0: if (!this.upgradeSyncKey(meta.payload.syncID)) { michael@0: this._log.warn("Failed to upgrade sync key. Failing remote setup."); michael@0: return false; michael@0: } michael@0: michael@0: if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { michael@0: this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return whether we should attempt login at the start of a sync. michael@0: * michael@0: * Note that this function has strong ties to _checkSync: callers michael@0: * of this function should typically use _checkSync to verify that michael@0: * any necessary login took place. michael@0: */ michael@0: _shouldLogin: function _shouldLogin() { michael@0: return this.enabled && michael@0: !Services.io.offline && michael@0: !this.isLoggedIn; michael@0: }, michael@0: michael@0: /** michael@0: * Determine if a sync should run. michael@0: * michael@0: * @param ignore [optional] michael@0: * array of reasons to ignore when checking michael@0: * michael@0: * @return Reason for not syncing; not-truthy if sync should run michael@0: */ michael@0: _checkSync: function _checkSync(ignore) { michael@0: let reason = ""; michael@0: if (!this.enabled) michael@0: reason = kSyncWeaveDisabled; michael@0: else if (Services.io.offline) michael@0: reason = kSyncNetworkOffline; michael@0: else if (this.status.minimumNextSync > Date.now()) michael@0: reason = kSyncBackoffNotMet; michael@0: else if ((this.status.login == MASTER_PASSWORD_LOCKED) && michael@0: Utils.mpLocked()) michael@0: reason = kSyncMasterPasswordLocked; michael@0: else if (Svc.Prefs.get("firstSync") == "notReady") michael@0: reason = kFirstSyncChoiceNotMade; michael@0: michael@0: if (ignore && ignore.indexOf(reason) != -1) michael@0: return ""; michael@0: michael@0: return reason; michael@0: }, michael@0: michael@0: sync: function sync() { michael@0: let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT); michael@0: this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent); michael@0: this._log.info("Starting sync at " + dateStr); michael@0: this._catch(function () { michael@0: // Make sure we're logged in. michael@0: if (this._shouldLogin()) { michael@0: this._log.debug("In sync: should login."); michael@0: if (!this.login()) { michael@0: this._log.debug("Not syncing: login returned false."); michael@0: return; michael@0: } michael@0: } michael@0: else { michael@0: this._log.trace("In sync: no need to login."); michael@0: } michael@0: return this._lockedSync.apply(this, arguments); michael@0: })(); michael@0: }, michael@0: michael@0: /** michael@0: * Sync up engines with the server. michael@0: */ michael@0: _lockedSync: function _lockedSync() { michael@0: return this._lock("service.js: sync", michael@0: this._notify("sync", "", function onNotify() { michael@0: michael@0: let synchronizer = new EngineSynchronizer(this); michael@0: let cb = Async.makeSpinningCallback(); michael@0: synchronizer.onComplete = cb; michael@0: michael@0: synchronizer.sync(); michael@0: // wait() throws if the first argument is truthy, which is exactly what michael@0: // we want. michael@0: let result = cb.wait(); michael@0: michael@0: // We successfully synchronized. Now let's update our declined engines. michael@0: let meta = this.recordManager.get(this.metaURL); michael@0: if (!meta) { michael@0: this._log.warn("No meta/global; can't update declined state."); michael@0: return; michael@0: } michael@0: michael@0: let declinedEngines = new DeclinedEngines(this); michael@0: let didChange = declinedEngines.updateDeclined(meta, this.engineManager); michael@0: if (!didChange) { michael@0: this._log.info("No change to declined engines. Not reuploading meta/global."); michael@0: return; michael@0: } michael@0: michael@0: this.uploadMetaGlobal(meta); michael@0: }))(); michael@0: }, michael@0: michael@0: /** michael@0: * Upload meta/global, throwing the response on failure. michael@0: */ michael@0: uploadMetaGlobal: function (meta) { michael@0: this._log.debug("Uploading meta/global: " + JSON.stringify(meta)); michael@0: michael@0: // It would be good to set the X-If-Unmodified-Since header to `timestamp` michael@0: // for this PUT to ensure at least some level of transactionality. michael@0: // Unfortunately, the servers don't support it after a wipe right now michael@0: // (bug 693893), so we're going to defer this until bug 692700. michael@0: let res = this.resource(this.metaURL); michael@0: let response = res.put(meta); michael@0: if (!response.success) { michael@0: throw response; michael@0: } michael@0: this.recordManager.set(this.metaURL, meta); michael@0: }, michael@0: michael@0: /** michael@0: * If we have a passphrase, rather than a 25-alphadigit sync key, michael@0: * use the provided sync ID to bootstrap it using PBKDF2. michael@0: * michael@0: * Store the new 'passphrase' back into the identity manager. michael@0: * michael@0: * We can check this as often as we want, because once it's done the michael@0: * check will no longer succeed. It only matters that it happens after michael@0: * we decide to bump the server storage version. michael@0: */ michael@0: upgradeSyncKey: function upgradeSyncKey(syncID) { michael@0: let p = this.identity.syncKey; michael@0: michael@0: if (!p) { michael@0: return false; michael@0: } michael@0: michael@0: // Check whether it's already a key that we generated. michael@0: if (Utils.isPassphrase(p)) { michael@0: this._log.info("Sync key is up-to-date: no need to upgrade."); michael@0: return true; michael@0: } michael@0: michael@0: // Otherwise, let's upgrade it. michael@0: // N.B., we persist the sync key without testing it first... michael@0: michael@0: let s = btoa(syncID); // It's what WeaveCrypto expects. *sigh* michael@0: let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES); // Base 32. michael@0: michael@0: if (!k) { michael@0: this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade."); michael@0: return false; michael@0: } michael@0: michael@0: this._log.info("Upgrading sync key..."); michael@0: this.identity.syncKey = k; michael@0: this._log.info("Saving upgraded sync key..."); michael@0: this.persistLogin(); michael@0: this._log.info("Done saving."); michael@0: return true; michael@0: }, michael@0: michael@0: _freshStart: function _freshStart() { michael@0: this._log.info("Fresh start. Resetting client and considering key upgrade."); michael@0: this.resetClient(); michael@0: this.collectionKeys.clear(); michael@0: this.upgradeSyncKey(this.syncID); michael@0: michael@0: // Wipe the server. michael@0: let wipeTimestamp = this.wipeServer(); michael@0: michael@0: // Upload a new meta/global record. michael@0: let meta = new WBORecord("meta", "global"); michael@0: meta.payload.syncID = this.syncID; michael@0: meta.payload.storageVersion = STORAGE_VERSION; michael@0: meta.payload.declined = this.engineManager.getDeclined(); michael@0: meta.isNew = true; michael@0: michael@0: // uploadMetaGlobal throws on failure -- including race conditions. michael@0: // If we got into a race condition, we'll abort the sync this way, too. michael@0: // That's fine. We'll just wait till the next sync. The client that we're michael@0: // racing is probably busy uploading stuff right now anyway. michael@0: this.uploadMetaGlobal(meta); michael@0: michael@0: // Wipe everything we know about except meta because we just uploaded it michael@0: let engines = [this.clientsEngine].concat(this.engineManager.getAll()); michael@0: let collections = [engine.name for each (engine in engines)]; michael@0: // TODO: there's a bug here. We should be calling resetClient, no? michael@0: michael@0: // Generate, upload, and download new keys. Do this last so we don't wipe michael@0: // them... michael@0: this.generateNewSymmetricKeys(); michael@0: }, michael@0: michael@0: /** michael@0: * Wipe user data from the server. michael@0: * michael@0: * @param collections [optional] michael@0: * Array of collections to wipe. If not given, all collections are michael@0: * wiped by issuing a DELETE request for `storageURL`. michael@0: * michael@0: * @return the server's timestamp of the (last) DELETE. michael@0: */ michael@0: wipeServer: function wipeServer(collections) { michael@0: let response; michael@0: if (!collections) { michael@0: // Strip the trailing slash. michael@0: let res = this.resource(this.storageURL.slice(0, -1)); michael@0: res.setHeader("X-Confirm-Delete", "1"); michael@0: try { michael@0: response = res.delete(); michael@0: } catch (ex) { michael@0: this._log.debug("Failed to wipe server: " + CommonUtils.exceptionStr(ex)); michael@0: throw ex; michael@0: } michael@0: if (response.status != 200 && response.status != 404) { michael@0: this._log.debug("Aborting wipeServer. Server responded with " + michael@0: response.status + " response for " + this.storageURL); michael@0: throw response; michael@0: } michael@0: return response.headers["x-weave-timestamp"]; michael@0: } michael@0: michael@0: let timestamp; michael@0: for (let name of collections) { michael@0: let url = this.storageURL + name; michael@0: try { michael@0: response = this.resource(url).delete(); michael@0: } catch (ex) { michael@0: this._log.debug("Failed to wipe '" + name + "' collection: " + michael@0: Utils.exceptionStr(ex)); michael@0: throw ex; michael@0: } michael@0: michael@0: if (response.status != 200 && response.status != 404) { michael@0: this._log.debug("Aborting wipeServer. Server responded with " + michael@0: response.status + " response for " + url); michael@0: throw response; michael@0: } michael@0: michael@0: if ("x-weave-timestamp" in response.headers) { michael@0: timestamp = response.headers["x-weave-timestamp"]; michael@0: } michael@0: } michael@0: michael@0: return timestamp; michael@0: }, michael@0: michael@0: /** michael@0: * Wipe all local user data. michael@0: * michael@0: * @param engines [optional] michael@0: * Array of engine names to wipe. If not given, all engines are used. michael@0: */ michael@0: wipeClient: function wipeClient(engines) { michael@0: // If we don't have any engines, reset the service and wipe all engines michael@0: if (!engines) { michael@0: // Clear out any service data michael@0: this.resetService(); michael@0: michael@0: engines = [this.clientsEngine].concat(this.engineManager.getAll()); michael@0: } michael@0: // Convert the array of names into engines michael@0: else { michael@0: engines = this.engineManager.get(engines); michael@0: } michael@0: michael@0: // Fully wipe each engine if it's able to decrypt data michael@0: for each (let engine in engines) { michael@0: if (engine.canDecrypt()) { michael@0: engine.wipeClient(); michael@0: } michael@0: } michael@0: michael@0: // Save the password/passphrase just in-case they aren't restored by sync michael@0: this.persistLogin(); michael@0: }, michael@0: michael@0: /** michael@0: * Wipe all remote user data by wiping the server then telling each remote michael@0: * client to wipe itself. michael@0: * michael@0: * @param engines [optional] michael@0: * Array of engine names to wipe. If not given, all engines are used. michael@0: */ michael@0: wipeRemote: function wipeRemote(engines) { michael@0: try { michael@0: // Make sure stuff gets uploaded. michael@0: this.resetClient(engines); michael@0: michael@0: // Clear out any server data. michael@0: this.wipeServer(engines); michael@0: michael@0: // Only wipe the engines provided. michael@0: if (engines) { michael@0: engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this); michael@0: } michael@0: // Tell the remote machines to wipe themselves. michael@0: else { michael@0: this.clientsEngine.sendCommand("wipeAll", []); michael@0: } michael@0: michael@0: // Make sure the changed clients get updated. michael@0: this.clientsEngine.sync(); michael@0: } catch (ex) { michael@0: this.errorHandler.checkServerError(ex); michael@0: throw ex; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Reset local service information like logs, sync times, caches. michael@0: */ michael@0: resetService: function resetService() { michael@0: this._catch(function reset() { michael@0: this._log.info("Service reset."); michael@0: michael@0: // Pretend we've never synced to the server and drop cached data michael@0: this.syncID = ""; michael@0: this.recordManager.clearCache(); michael@0: })(); michael@0: }, michael@0: michael@0: /** michael@0: * Reset the client by getting rid of any local server data and client data. michael@0: * michael@0: * @param engines [optional] michael@0: * Array of engine names to reset. If not given, all engines are used. michael@0: */ michael@0: resetClient: function resetClient(engines) { michael@0: this._catch(function doResetClient() { michael@0: // If we don't have any engines, reset everything including the service michael@0: if (!engines) { michael@0: // Clear out any service data michael@0: this.resetService(); michael@0: michael@0: engines = [this.clientsEngine].concat(this.engineManager.getAll()); michael@0: } michael@0: // Convert the array of names into engines michael@0: else { michael@0: engines = this.engineManager.get(engines); michael@0: } michael@0: michael@0: // Have each engine drop any temporary meta data michael@0: for each (let engine in engines) { michael@0: engine.resetClient(); michael@0: } michael@0: })(); michael@0: }, michael@0: michael@0: /** michael@0: * Fetch storage info from the server. michael@0: * michael@0: * @param type michael@0: * String specifying what info to fetch from the server. Must be one michael@0: * of the INFO_* values. See Sync Storage Server API spec for details. michael@0: * @param callback michael@0: * Callback function with signature (error, data) where `data' is michael@0: * the return value from the server already parsed as JSON. michael@0: * michael@0: * @return RESTRequest instance representing the request, allowing callers michael@0: * to cancel the request. michael@0: */ michael@0: getStorageInfo: function getStorageInfo(type, callback) { michael@0: if (STORAGE_INFO_TYPES.indexOf(type) == -1) { michael@0: throw "Invalid value for 'type': " + type; michael@0: } michael@0: michael@0: let info_type = "info/" + type; michael@0: this._log.trace("Retrieving '" + info_type + "'..."); michael@0: let url = this.userBaseURL + info_type; michael@0: return this.getStorageRequest(url).get(function onComplete(error) { michael@0: // Note: 'this' is the request. michael@0: if (error) { michael@0: this._log.debug("Failed to retrieve '" + info_type + "': " + michael@0: Utils.exceptionStr(error)); michael@0: return callback(error); michael@0: } michael@0: if (this.response.status != 200) { michael@0: this._log.debug("Failed to retrieve '" + info_type + michael@0: "': server responded with HTTP" + michael@0: this.response.status); michael@0: return callback(this.response); michael@0: } michael@0: michael@0: let result; michael@0: try { michael@0: result = JSON.parse(this.response.body); michael@0: } catch (ex) { michael@0: this._log.debug("Server returned invalid JSON for '" + info_type + michael@0: "': " + this.response.body); michael@0: return callback(ex); michael@0: } michael@0: this._log.trace("Successfully retrieved '" + info_type + "'."); michael@0: return callback(null, result); michael@0: }); michael@0: }, michael@0: }; michael@0: michael@0: this.Service = new Sync11Service(); michael@0: Service.onStartup();