|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 this.EXPORTED_SYMBOLS = ["Service"]; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cr = Components.results; |
|
10 const Cu = Components.utils; |
|
11 |
|
12 // How long before refreshing the cluster |
|
13 const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes |
|
14 |
|
15 // How long a key to generate from an old passphrase. |
|
16 const PBKDF2_KEY_BYTES = 16; |
|
17 |
|
18 const CRYPTO_COLLECTION = "crypto"; |
|
19 const KEYS_WBO = "keys"; |
|
20 |
|
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"); |
|
38 |
|
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 }; |
|
48 |
|
49 const STORAGE_INFO_TYPES = [INFO_COLLECTIONS, |
|
50 INFO_COLLECTION_USAGE, |
|
51 INFO_COLLECTION_COUNTS, |
|
52 INFO_QUOTA]; |
|
53 |
|
54 |
|
55 function Sync11Service() { |
|
56 this._notify = Utils.notify("weave:service:"); |
|
57 } |
|
58 Sync11Service.prototype = { |
|
59 |
|
60 _lock: Utils.lock, |
|
61 _locked: false, |
|
62 _loggedIn: false, |
|
63 |
|
64 infoURL: null, |
|
65 storageURL: null, |
|
66 metaURL: null, |
|
67 cryptoKeyURL: null, |
|
68 |
|
69 get serverURL() Svc.Prefs.get("serverURL"), |
|
70 set serverURL(value) { |
|
71 if (!value.endsWith("/")) { |
|
72 value += "/"; |
|
73 } |
|
74 |
|
75 // Only do work if it's actually changing |
|
76 if (value == this.serverURL) |
|
77 return; |
|
78 |
|
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 }, |
|
83 |
|
84 get clusterURL() Svc.Prefs.get("clusterURL", ""), |
|
85 set clusterURL(value) { |
|
86 Svc.Prefs.set("clusterURL", value); |
|
87 this._updateCachedURLs(); |
|
88 }, |
|
89 |
|
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 }, |
|
97 |
|
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 } |
|
111 |
|
112 return url + USER_API_VERSION + "/"; |
|
113 }, |
|
114 |
|
115 get pwResetURL() { |
|
116 return this.serverURL + "weave-password-reset"; |
|
117 }, |
|
118 |
|
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 }, |
|
127 |
|
128 get isLoggedIn() { return this._loggedIn; }, |
|
129 |
|
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 }, |
|
140 |
|
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 } |
|
151 |
|
152 return Utils.catch.call(this, func, lockExceptions); |
|
153 }, |
|
154 |
|
155 get userBaseURL() { |
|
156 if (!this._clusterManager) { |
|
157 return null; |
|
158 } |
|
159 return this._clusterManager.getUserBaseURL(); |
|
160 }, |
|
161 |
|
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; |
|
166 |
|
167 this._log.debug("Caching URLs under storage user base: " + this.userBaseURL); |
|
168 |
|
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 }, |
|
175 |
|
176 _checkCrypto: function _checkCrypto() { |
|
177 let ok = false; |
|
178 |
|
179 try { |
|
180 let iv = Svc.Crypto.generateRandomIV(); |
|
181 if (iv.length == 24) |
|
182 ok = true; |
|
183 |
|
184 } catch (e) { |
|
185 this._log.debug("Crypto check failed: " + e); |
|
186 } |
|
187 |
|
188 return ok; |
|
189 }, |
|
190 |
|
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, |
|
213 |
|
214 /* |
|
215 * Returns whether to try again. |
|
216 */ |
|
217 handleHMACEvent: function handleHMACEvent() { |
|
218 let now = Date.now(); |
|
219 |
|
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; |
|
224 |
|
225 this._log.info("Bad HMAC event detected. Attempting recovery " + |
|
226 "or signaling to other clients."); |
|
227 |
|
228 // Set the last handled time so that we don't act again. |
|
229 this.lastHMACEvent = now; |
|
230 |
|
231 // Fetch keys. |
|
232 let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); |
|
233 try { |
|
234 let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; |
|
235 |
|
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; |
|
239 |
|
240 if (!cryptoResp.success) { |
|
241 this._log.warn("Failed to download keys."); |
|
242 return false; |
|
243 } |
|
244 |
|
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 } |
|
252 |
|
253 // If not, reupload them and continue the current sync. |
|
254 cryptoKeys.ciphertext = cipherText; |
|
255 cryptoKeys.cleartext = null; |
|
256 |
|
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."); |
|
263 |
|
264 return false; // Don't try again: same keys. |
|
265 |
|
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 }, |
|
272 |
|
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); |
|
277 |
|
278 if (keysChanged && !wasBlank) { |
|
279 this._log.debug("Keys changed: " + JSON.stringify(keysChanged)); |
|
280 |
|
281 if (!skipReset) { |
|
282 this._log.info("Resetting client to reflect key change."); |
|
283 |
|
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 } |
|
292 |
|
293 this._log.info("Downloaded new keys, client reset. Proceeding."); |
|
294 } |
|
295 return true; |
|
296 } |
|
297 return false; |
|
298 }, |
|
299 |
|
300 /** |
|
301 * Prepare to initialize the rest of Weave after waiting a little bit |
|
302 */ |
|
303 onStartup: function onStartup() { |
|
304 this._migratePrefs(); |
|
305 |
|
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 } |
|
313 |
|
314 this.status = Status; |
|
315 this.identity = Status._authManager; |
|
316 this.collectionKeys = new CollectionKeyManager(); |
|
317 |
|
318 this.errorHandler = new ErrorHandler(this); |
|
319 |
|
320 this._log = Log.repository.getLogger("Sync.Service"); |
|
321 this._log.level = |
|
322 Log.Level[Svc.Prefs.get("log.logger.service.main")]; |
|
323 |
|
324 this._log.info("Loading Weave " + WEAVE_VERSION); |
|
325 |
|
326 this._clusterManager = this.identity.createClusterManager(this); |
|
327 this.recordManager = new RecordManager(this); |
|
328 |
|
329 this.enabled = true; |
|
330 |
|
331 this._registerEngines(); |
|
332 |
|
333 let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. |
|
334 getService(Ci.nsIHttpProtocolHandler).userAgent; |
|
335 this._log.info(ua); |
|
336 |
|
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 } |
|
342 |
|
343 Svc.Obs.add("weave:service:setup-complete", this); |
|
344 Svc.Prefs.observe("engine.", this); |
|
345 |
|
346 this.scheduler = new SyncScheduler(this); |
|
347 |
|
348 if (!this.enabled) { |
|
349 this._log.info("Firefox Sync disabled."); |
|
350 } |
|
351 |
|
352 this._updateCachedURLs(); |
|
353 |
|
354 let status = this._checkSetup(); |
|
355 if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) { |
|
356 Svc.Obs.notify("weave:engine:start-tracking"); |
|
357 } |
|
358 |
|
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; |
|
364 |
|
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; |
|
371 |
|
372 Svc.Obs.notify("weave:service:ready"); |
|
373 }.bind(this)); |
|
374 }, |
|
375 |
|
376 _checkSetup: function _checkSetup() { |
|
377 if (!this.enabled) { |
|
378 return this.status.service = STATUS_DISABLED; |
|
379 } |
|
380 return this.status.checkSetup(); |
|
381 }, |
|
382 |
|
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 } |
|
394 |
|
395 // Migrate old extensions.weave.* prefs if we haven't already tried. |
|
396 if (Svc.Prefs.get("migrated", false)) |
|
397 return; |
|
398 |
|
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("", {}); |
|
405 |
|
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)); |
|
410 |
|
411 // Remove all the old prefs and remember that we've migrated |
|
412 oldPref.resetBranch(""); |
|
413 Svc.Prefs.set("migrated", true); |
|
414 }, |
|
415 |
|
416 /** |
|
417 * Register the built-in engines for certain applications |
|
418 */ |
|
419 _registerEngines: function _registerEngines() { |
|
420 this.engineManager = new EngineManager(this); |
|
421 |
|
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 } |
|
429 |
|
430 let declined = []; |
|
431 pref = Svc.Prefs.get("declinedEngines"); |
|
432 if (pref) { |
|
433 declined = pref.split(","); |
|
434 } |
|
435 |
|
436 this.clientsEngine = new ClientEngine(this); |
|
437 |
|
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 } |
|
443 |
|
444 let ns = {}; |
|
445 try { |
|
446 Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns); |
|
447 |
|
448 let engineName = name + "Engine"; |
|
449 if (!(engineName in ns)) { |
|
450 this._log.warn("Could not find exported engine instance: " + engineName); |
|
451 continue; |
|
452 } |
|
453 |
|
454 this.engineManager.register(ns[engineName]); |
|
455 } catch (ex) { |
|
456 this._log.warn("Could not register engine " + name + ": " + |
|
457 CommonUtils.exceptionStr(ex)); |
|
458 } |
|
459 } |
|
460 |
|
461 this.engineManager.setDeclined(declined); |
|
462 }, |
|
463 |
|
464 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
465 Ci.nsISupportsWeakReference]), |
|
466 |
|
467 // nsIObserver |
|
468 |
|
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 }, |
|
484 |
|
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 }, |
|
495 |
|
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(); |
|
502 |
|
503 return res; |
|
504 }, |
|
505 |
|
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(); |
|
512 |
|
513 return request; |
|
514 }, |
|
515 |
|
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; |
|
522 |
|
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 } |
|
531 |
|
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 }, |
|
539 |
|
540 verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) { |
|
541 |
|
542 this._log.debug("Fetching and verifying -- or generating -- symmetric keys."); |
|
543 |
|
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. |
|
547 |
|
548 if (!this.identity.syncKey) { |
|
549 this.status.login = LOGIN_FAILED_NO_PASSPHRASE; |
|
550 this.status.sync = CREDENTIALS_CHANGED; |
|
551 return false; |
|
552 } |
|
553 |
|
554 let syncKeyBundle = this.identity.syncKeyBundle; |
|
555 if (!syncKeyBundle) { |
|
556 this._log.error("Sync Key Bundle not set. Invalid Sync Key?"); |
|
557 |
|
558 this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; |
|
559 this.status.sync = CREDENTIALS_CHANGED; |
|
560 return false; |
|
561 } |
|
562 |
|
563 try { |
|
564 if (!infoResponse) |
|
565 infoResponse = this._fetchInfo(); // Will throw an exception on failure. |
|
566 |
|
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 } |
|
574 |
|
575 let infoCollections = infoResponse.obj; |
|
576 |
|
577 this._log.info("Testing info/collections: " + JSON.stringify(infoCollections)); |
|
578 |
|
579 if (this.collectionKeys.updateNeeded(infoCollections)) { |
|
580 this._log.info("collection keys reports that a key update is needed."); |
|
581 |
|
582 // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this. |
|
583 |
|
584 // Fetch storage/crypto/keys. |
|
585 let cryptoKeys; |
|
586 |
|
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; |
|
591 |
|
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? |
|
613 |
|
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 } |
|
630 |
|
631 if (!cryptoKeys) { |
|
632 this._log.info("No keys! Generating new ones."); |
|
633 |
|
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 } |
|
641 |
|
642 // Last-ditch case. |
|
643 return false; |
|
644 } |
|
645 else { |
|
646 // No update needed: we're good! |
|
647 return true; |
|
648 } |
|
649 |
|
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 }, |
|
658 |
|
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 } |
|
666 |
|
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 } |
|
672 |
|
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 } |
|
686 |
|
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 } |
|
695 |
|
696 // Fetch collection info on every startup. |
|
697 let test = this.resource(this.infoURL).get(); |
|
698 |
|
699 switch (test.status) { |
|
700 case 200: |
|
701 // The user is authenticated. |
|
702 |
|
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 } |
|
711 |
|
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 } |
|
719 |
|
720 this._log.warn("Remote setup failed."); |
|
721 // Remote setup must have failed. |
|
722 return false; |
|
723 |
|
724 case 401: |
|
725 this._log.warn("401: login failed."); |
|
726 // Fall through to the 404 case. |
|
727 |
|
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 } |
|
733 |
|
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; |
|
737 |
|
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 }, |
|
752 |
|
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); |
|
758 |
|
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); |
|
769 |
|
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); |
|
774 |
|
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 } |
|
779 |
|
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 } |
|
786 |
|
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 } |
|
793 |
|
794 // Doesn't matter if the timestamp is ahead. |
|
795 |
|
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 }, |
|
809 |
|
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); |
|
815 |
|
816 try { |
|
817 cb.wait(); |
|
818 } catch (ex) { |
|
819 this._log.debug("Password change failed: " + |
|
820 CommonUtils.exceptionStr(ex)); |
|
821 return false; |
|
822 } |
|
823 |
|
824 // Save the new password for requests and login manager. |
|
825 this.identity.basicPassword = newPassword; |
|
826 this.persistLogin(); |
|
827 return true; |
|
828 }, |
|
829 |
|
830 changePassphrase: function changePassphrase(newphrase) { |
|
831 return this._catch(function doChangePasphrase() { |
|
832 /* Wipe. */ |
|
833 this.wipeServer(); |
|
834 |
|
835 this.logout(); |
|
836 |
|
837 /* Set this so UI is updated on next run. */ |
|
838 this.identity.syncKey = newphrase; |
|
839 this.persistLogin(); |
|
840 |
|
841 /* We need to re-encrypt everything, so reset. */ |
|
842 this.resetClient(); |
|
843 this.collectionKeys.clear(); |
|
844 |
|
845 /* Login and sync. This also generates new keys. */ |
|
846 this.sync(); |
|
847 |
|
848 Svc.Obs.notify("weave:service:change-passphrase", true); |
|
849 |
|
850 return true; |
|
851 })(); |
|
852 }, |
|
853 |
|
854 startOver: function startOver() { |
|
855 this._log.trace("Invoking Service.startOver."); |
|
856 Svc.Obs.notify("weave:engine:stop-tracking"); |
|
857 this.status.resetSync(); |
|
858 |
|
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 } |
|
874 |
|
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"); |
|
883 |
|
884 // Reset all engines and clear keys. |
|
885 this.resetClient(); |
|
886 this.collectionKeys.clear(); |
|
887 this.status.resetBackoff(); |
|
888 |
|
889 // Reset Weave prefs. |
|
890 this._ignorePrefObserver = true; |
|
891 Svc.Prefs.resetBranch(""); |
|
892 this._ignorePrefObserver = false; |
|
893 |
|
894 Svc.Prefs.set("lastversion", WEAVE_VERSION); |
|
895 |
|
896 this.identity.deleteSyncCredentials(); |
|
897 |
|
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 } |
|
908 |
|
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 }, |
|
926 |
|
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 }, |
|
934 |
|
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 } |
|
942 |
|
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 } |
|
953 |
|
954 if (this._checkSetup() == CLIENT_NOT_CONFIGURED) { |
|
955 throw "Aborting login, client not configured."; |
|
956 } |
|
957 |
|
958 // Ask the identity manager to explicitly login now. |
|
959 let cb = Async.makeSpinningCallback(); |
|
960 this.identity.ensureLoggedIn().then(cb, cb); |
|
961 |
|
962 // Just let any errors bubble up - they've more context than we do! |
|
963 cb.wait(); |
|
964 |
|
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(); |
|
973 |
|
974 if (!this.verifyLogin()) { |
|
975 // verifyLogin sets the failure states here. |
|
976 throw "Login failed: " + this.status.login; |
|
977 } |
|
978 |
|
979 this._loggedIn = true; |
|
980 |
|
981 return true; |
|
982 } |
|
983 |
|
984 let notifier = this._notify("login", "", onNotify.bind(this)); |
|
985 return this._catch(this._lock("service.js: login", notifier))(); |
|
986 }, |
|
987 |
|
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; |
|
995 |
|
996 Svc.Obs.notify("weave:service:logout:finish"); |
|
997 }, |
|
998 |
|
999 checkAccount: function checkAccount(account) { |
|
1000 let client = new UserAPI10Client(this.userAPIURI); |
|
1001 let cb = Async.makeSpinningCallback(); |
|
1002 |
|
1003 let username = this.identity.usernameFromAccount(account); |
|
1004 client.usernameExists(username, cb); |
|
1005 |
|
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); |
|
1012 } |
|
1013 }, |
|
1014 |
|
1015 createAccount: function createAccount(email, password, |
|
1016 captchaChallenge, captchaResponse) { |
|
1017 let client = new UserAPI10Client(this.userAPIURI); |
|
1018 |
|
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", ""); |
|
1023 } |
|
1024 |
|
1025 let cb = Async.makeSpinningCallback(); |
|
1026 |
|
1027 client.createAccount(email, password, captchaChallenge, captchaResponse, |
|
1028 cb); |
|
1029 |
|
1030 try { |
|
1031 cb.wait(); |
|
1032 return null; |
|
1033 } catch (ex) { |
|
1034 return this.errorHandler.errorStr(ex.body); |
|
1035 } |
|
1036 }, |
|
1037 |
|
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; |
|
1042 |
|
1043 this._log.debug("Fetching global metadata record"); |
|
1044 let meta = this.recordManager.get(this.metaURL); |
|
1045 |
|
1046 // Checking modified time of the meta record. |
|
1047 if (infoResponse && |
|
1048 (infoResponse.obj.meta != this.metaModified) && |
|
1049 (!meta || !meta.isNew)) { |
|
1050 |
|
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)); |
|
1055 |
|
1056 this.recordManager.del(this.metaURL); |
|
1057 |
|
1058 // ... fetch the current record from the server, and COPY THE FLAGS. |
|
1059 let newMeta = this.recordManager.get(this.metaURL); |
|
1060 |
|
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; |
|
1067 } |
|
1068 |
|
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(); |
|
1075 |
|
1076 newMeta.isNew = true; |
|
1077 |
|
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; |
|
1082 } |
|
1083 } else { |
|
1084 // If newMeta, then it stands to reason that meta != null. |
|
1085 newMeta.isNew = meta.isNew; |
|
1086 newMeta.changed = meta.changed; |
|
1087 } |
|
1088 |
|
1089 // Switch in the new meta object and record the new time. |
|
1090 meta = newMeta; |
|
1091 this.metaModified = infoResponse.obj.meta; |
|
1092 } |
|
1093 |
|
1094 let remoteVersion = (meta && meta.payload.storageVersion)? |
|
1095 meta.payload.storageVersion : ""; |
|
1096 |
|
1097 this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:", |
|
1098 STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" ")); |
|
1099 |
|
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)) { |
|
1104 |
|
1105 this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed."); |
|
1106 |
|
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; |
|
1115 } |
|
1116 |
|
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"); |
|
1121 |
|
1122 reset = true; |
|
1123 |
|
1124 this._log.info("Wiping server data"); |
|
1125 this._freshStart(); |
|
1126 |
|
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); |
|
1132 |
|
1133 return true; |
|
1134 } |
|
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; |
|
1139 } |
|
1140 else if (meta.payload.syncID != this.syncID) { |
|
1141 |
|
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); |
|
1147 |
|
1148 if (!this.upgradeSyncKey(meta.payload.syncID)) { |
|
1149 this._log.warn("Failed to upgrade sync key. Failing remote setup."); |
|
1150 return false; |
|
1151 } |
|
1152 |
|
1153 if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { |
|
1154 this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); |
|
1155 return false; |
|
1156 } |
|
1157 |
|
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; |
|
1163 } |
|
1164 |
|
1165 return true; |
|
1166 } |
|
1167 else { |
|
1168 if (!this.upgradeSyncKey(meta.payload.syncID)) { |
|
1169 this._log.warn("Failed to upgrade sync key. Failing remote setup."); |
|
1170 return false; |
|
1171 } |
|
1172 |
|
1173 if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { |
|
1174 this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); |
|
1175 return false; |
|
1176 } |
|
1177 |
|
1178 return true; |
|
1179 } |
|
1180 }, |
|
1181 |
|
1182 /** |
|
1183 * Return whether we should attempt login at the start of a sync. |
|
1184 * |
|
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 }, |
|
1194 |
|
1195 /** |
|
1196 * Determine if a sync should run. |
|
1197 * |
|
1198 * @param ignore [optional] |
|
1199 * array of reasons to ignore when checking |
|
1200 * |
|
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; |
|
1216 |
|
1217 if (ignore && ignore.indexOf(reason) != -1) |
|
1218 return ""; |
|
1219 |
|
1220 return reason; |
|
1221 }, |
|
1222 |
|
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; |
|
1234 } |
|
1235 } |
|
1236 else { |
|
1237 this._log.trace("In sync: no need to login."); |
|
1238 } |
|
1239 return this._lockedSync.apply(this, arguments); |
|
1240 })(); |
|
1241 }, |
|
1242 |
|
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() { |
|
1249 |
|
1250 let synchronizer = new EngineSynchronizer(this); |
|
1251 let cb = Async.makeSpinningCallback(); |
|
1252 synchronizer.onComplete = cb; |
|
1253 |
|
1254 synchronizer.sync(); |
|
1255 // wait() throws if the first argument is truthy, which is exactly what |
|
1256 // we want. |
|
1257 let result = cb.wait(); |
|
1258 |
|
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; |
|
1264 } |
|
1265 |
|
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; |
|
1271 } |
|
1272 |
|
1273 this.uploadMetaGlobal(meta); |
|
1274 }))(); |
|
1275 }, |
|
1276 |
|
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)); |
|
1282 |
|
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; |
|
1291 } |
|
1292 this.recordManager.set(this.metaURL, meta); |
|
1293 }, |
|
1294 |
|
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. |
|
1298 * |
|
1299 * Store the new 'passphrase' back into the identity manager. |
|
1300 * |
|
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; |
|
1307 |
|
1308 if (!p) { |
|
1309 return false; |
|
1310 } |
|
1311 |
|
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; |
|
1316 } |
|
1317 |
|
1318 // Otherwise, let's upgrade it. |
|
1319 // N.B., we persist the sync key without testing it first... |
|
1320 |
|
1321 let s = btoa(syncID); // It's what WeaveCrypto expects. *sigh* |
|
1322 let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES); // Base 32. |
|
1323 |
|
1324 if (!k) { |
|
1325 this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade."); |
|
1326 return false; |
|
1327 } |
|
1328 |
|
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 }, |
|
1336 |
|
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); |
|
1342 |
|
1343 // Wipe the server. |
|
1344 let wipeTimestamp = this.wipeServer(); |
|
1345 |
|
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; |
|
1352 |
|
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); |
|
1358 |
|
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? |
|
1363 |
|
1364 // Generate, upload, and download new keys. Do this last so we don't wipe |
|
1365 // them... |
|
1366 this.generateNewSymmetricKeys(); |
|
1367 }, |
|
1368 |
|
1369 /** |
|
1370 * Wipe user data from the server. |
|
1371 * |
|
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`. |
|
1375 * |
|
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; |
|
1389 } |
|
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; |
|
1394 } |
|
1395 return response.headers["x-weave-timestamp"]; |
|
1396 } |
|
1397 |
|
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; |
|
1407 } |
|
1408 |
|
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; |
|
1413 } |
|
1414 |
|
1415 if ("x-weave-timestamp" in response.headers) { |
|
1416 timestamp = response.headers["x-weave-timestamp"]; |
|
1417 } |
|
1418 } |
|
1419 |
|
1420 return timestamp; |
|
1421 }, |
|
1422 |
|
1423 /** |
|
1424 * Wipe all local user data. |
|
1425 * |
|
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(); |
|
1434 |
|
1435 engines = [this.clientsEngine].concat(this.engineManager.getAll()); |
|
1436 } |
|
1437 // Convert the array of names into engines |
|
1438 else { |
|
1439 engines = this.engineManager.get(engines); |
|
1440 } |
|
1441 |
|
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(); |
|
1446 } |
|
1447 } |
|
1448 |
|
1449 // Save the password/passphrase just in-case they aren't restored by sync |
|
1450 this.persistLogin(); |
|
1451 }, |
|
1452 |
|
1453 /** |
|
1454 * Wipe all remote user data by wiping the server then telling each remote |
|
1455 * client to wipe itself. |
|
1456 * |
|
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); |
|
1464 |
|
1465 // Clear out any server data. |
|
1466 this.wipeServer(engines); |
|
1467 |
|
1468 // Only wipe the engines provided. |
|
1469 if (engines) { |
|
1470 engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this); |
|
1471 } |
|
1472 // Tell the remote machines to wipe themselves. |
|
1473 else { |
|
1474 this.clientsEngine.sendCommand("wipeAll", []); |
|
1475 } |
|
1476 |
|
1477 // Make sure the changed clients get updated. |
|
1478 this.clientsEngine.sync(); |
|
1479 } catch (ex) { |
|
1480 this.errorHandler.checkServerError(ex); |
|
1481 throw ex; |
|
1482 } |
|
1483 }, |
|
1484 |
|
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."); |
|
1491 |
|
1492 // Pretend we've never synced to the server and drop cached data |
|
1493 this.syncID = ""; |
|
1494 this.recordManager.clearCache(); |
|
1495 })(); |
|
1496 }, |
|
1497 |
|
1498 /** |
|
1499 * Reset the client by getting rid of any local server data and client data. |
|
1500 * |
|
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(); |
|
1510 |
|
1511 engines = [this.clientsEngine].concat(this.engineManager.getAll()); |
|
1512 } |
|
1513 // Convert the array of names into engines |
|
1514 else { |
|
1515 engines = this.engineManager.get(engines); |
|
1516 } |
|
1517 |
|
1518 // Have each engine drop any temporary meta data |
|
1519 for each (let engine in engines) { |
|
1520 engine.resetClient(); |
|
1521 } |
|
1522 })(); |
|
1523 }, |
|
1524 |
|
1525 /** |
|
1526 * Fetch storage info from the server. |
|
1527 * |
|
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. |
|
1534 * |
|
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; |
|
1541 } |
|
1542 |
|
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); |
|
1552 } |
|
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); |
|
1558 } |
|
1559 |
|
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); |
|
1567 } |
|
1568 this._log.trace("Successfully retrieved '" + info_type + "'."); |
|
1569 return callback(null, result); |
|
1570 }); |
|
1571 }, |
|
1572 }; |
|
1573 |
|
1574 this.Service = new Sync11Service(); |
|
1575 Service.onStartup(); |