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