services/sync/modules/service.js

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

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

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

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

mercurial