1581 lines, 940 LOC, 241 covered (25%)
2 | 1 | /* ***** BEGIN LICENSE BLOCK ***** |
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
|
3 | * |
|
4 | * The contents of this file are subject to the Mozilla Public License Version |
|
5 | * 1.1 (the "License"); you may not use this file except in compliance with |
|
6 | * the License. You may obtain a copy of the License at |
|
7 | * http://www.mozilla.org/MPL/ |
|
8 | * |
|
9 | * Software distributed under the License is distributed on an "AS IS" basis, |
|
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
|
11 | * for the specific language governing rights and limitations under the |
|
12 | * License. |
|
13 | * |
|
14 | * The Original Code is Bookmarks Sync. |
|
15 | * |
|
16 | * The Initial Developer of the Original Code is Mozilla. |
|
17 | * Portions created by the Initial Developer are Copyright (C) 2007 |
|
18 | * the Initial Developer. All Rights Reserved. |
|
19 | * |
|
20 | * Contributor(s): |
|
21 | * Dan Mills <thunder@mozilla.com> |
|
22 | * Myk Melez <myk@mozilla.org> |
|
23 | * Anant Narayanan <anant@kix.in> |
|
24 | * |
|
25 | * Alternatively, the contents of this file may be used under the terms of |
|
26 | * either the GNU General Public License Version 2 or later (the "GPL"), or |
|
27 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
|
28 | * in which case the provisions of the GPL or the LGPL are applicable instead |
|
29 | * of those above. If you wish to allow use of your version of this file only |
|
30 | * under the terms of either the GPL or the LGPL, and not to allow others to |
|
31 | * use your version of this file under the terms of the MPL, indicate your |
|
32 | * decision by deleting the provisions above and replace them with the notice |
|
33 | * and other provisions required by the GPL or the LGPL. If you do not delete |
|
34 | * the provisions above, a recipient may use your version of this file under |
|
35 | * the terms of any one of the MPL, the GPL or the LGPL. |
|
36 | * |
|
37 | * ***** END LICENSE BLOCK ***** */ |
|
38 | ||
14 | 39 | const EXPORTED_SYMBOLS = ['Weave']; |
40 | ||
8 | 41 | const Cc = Components.classes; |
8 | 42 | const Ci = Components.interfaces; |
8 | 43 | const Cr = Components.results; |
8 | 44 | const Cu = Components.utils; |
45 | ||
46 | // how long we should wait before actually syncing on idle |
|
6 | 47 | const IDLE_TIME = 5; // xxxmpc: in seconds, should be preffable |
48 | ||
49 | // How long before refreshing the cluster |
|
6 | 50 | const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes |
51 | ||
10 | 52 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
10 | 53 | Cu.import("resource://weave/ext/Sync.js"); |
10 | 54 | Cu.import("resource://weave/log4moz.js"); |
10 | 55 | Cu.import("resource://weave/constants.js"); |
10 | 56 | Cu.import("resource://weave/util.js"); |
10 | 57 | Cu.import("resource://weave/auth.js"); |
10 | 58 | Cu.import("resource://weave/resource.js"); |
10 | 59 | Cu.import("resource://weave/base_records/wbo.js"); |
10 | 60 | Cu.import("resource://weave/base_records/crypto.js"); |
10 | 61 | Cu.import("resource://weave/base_records/keys.js"); |
10 | 62 | Cu.import("resource://weave/engines.js"); |
10 | 63 | Cu.import("resource://weave/identity.js"); |
10 | 64 | Cu.import("resource://weave/status.js"); |
10 | 65 | Cu.import("resource://weave/engines/clients.js"); |
66 | ||
67 | // for export |
|
8 | 68 | let Weave = {}; |
12 | 69 | Cu.import("resource://weave/constants.js", Weave); |
12 | 70 | Cu.import("resource://weave/util.js", Weave); |
12 | 71 | Cu.import("resource://weave/auth.js", Weave); |
12 | 72 | Cu.import("resource://weave/resource.js", Weave); |
12 | 73 | Cu.import("resource://weave/base_records/keys.js", Weave); |
12 | 74 | Cu.import("resource://weave/notifications.js", Weave); |
12 | 75 | Cu.import("resource://weave/identity.js", Weave); |
12 | 76 | Cu.import("resource://weave/status.js", Weave); |
12 | 77 | Cu.import("resource://weave/stores.js", Weave); |
12 | 78 | Cu.import("resource://weave/engines.js", Weave); |
79 | ||
12 | 80 | Cu.import("resource://weave/engines/bookmarks.js", Weave); |
12 | 81 | Cu.import("resource://weave/engines/clients.js", Weave); |
12 | 82 | Cu.import("resource://weave/engines/forms.js", Weave); |
12 | 83 | Cu.import("resource://weave/engines/history.js", Weave); |
12 | 84 | Cu.import("resource://weave/engines/prefs.js", Weave); |
12 | 85 | Cu.import("resource://weave/engines/passwords.js", Weave); |
12 | 86 | Cu.import("resource://weave/engines/tabs.js", Weave); |
87 | ||
14 | 88 | Utils.lazy(Weave, 'Service', WeaveSvc); |
89 | ||
90 | /* |
|
91 | * Service singleton |
|
92 | * Main entry point into Weave's sync framework |
|
93 | */ |
|
94 | ||
6 | 95 | function WeaveSvc() { |
14 | 96 | this._notify = Utils.notify("weave:service:"); |
97 | } |
|
4 | 98 | WeaveSvc.prototype = { |
99 | ||
6 | 100 | _lock: Utils.lock, |
6 | 101 | _catch: Utils.catch, |
4 | 102 | _loggedIn: false, |
4 | 103 | _syncInProgress: false, |
4 | 104 | _keyGenEnabled: true, |
105 | ||
106 | // object for caching public and private keys |
|
6 | 107 | _keyPair: {}, |
108 | ||
11 | 109 | get username() { |
63 | 110 | return Svc.Prefs.get("username", "").toLowerCase(); |
111 | }, |
|
4 | 112 | set username(value) { |
113 | if (value) { |
|
114 | // Make sure all uses of this new username is lowercase |
|
115 | value = value.toLowerCase(); |
|
116 | Svc.Prefs.set("username", value); |
|
117 | } |
|
118 | else |
|
119 | Svc.Prefs.reset("username"); |
|
120 | ||
121 | // fixme - need to loop over all Identity objects - needs some rethinking... |
|
122 | ID.get('WeaveID').username = value; |
|
123 | ID.get('WeaveCryptoID').username = value; |
|
124 | ||
125 | // FIXME: need to also call this whenever the username pref changes |
|
126 | this._updateCachedURLs(); |
|
127 | }, |
|
128 | ||
4 | 129 | get password() ID.get("WeaveID").password, |
4 | 130 | set password(value) ID.get("WeaveID").password = value, |
131 | ||
4 | 132 | get passphrase() ID.get("WeaveCryptoID").password, |
4 | 133 | set passphrase(value) ID.get("WeaveCryptoID").password = value, |
134 | ||
4 | 135 | get serverURL() Svc.Prefs.get("serverURL"), |
4 | 136 | set serverURL(value) { |
137 | // Only do work if it's actually changing |
|
138 | if (value == this.serverURL) |
|
139 | return; |
|
140 | ||
141 | // A new server most likely uses a different cluster, so clear that |
|
142 | Svc.Prefs.set("serverURL", value); |
|
143 | Svc.Prefs.reset("clusterURL"); |
|
144 | }, |
|
145 | ||
20 | 146 | get clusterURL() Svc.Prefs.get("clusterURL", ""), |
4 | 147 | set clusterURL(value) { |
148 | Svc.Prefs.set("clusterURL", value); |
|
149 | this._updateCachedURLs(); |
|
150 | }, |
|
151 | ||
4 | 152 | get miscAPI() { |
153 | // Append to the serverURL if it's a relative fragment |
|
154 | let misc = Svc.Prefs.get("miscURL"); |
|
155 | if (misc.indexOf(":") == -1) |
|
156 | misc = this.serverURL + misc; |
|
157 | return misc + "1.0/"; |
|
158 | }, |
|
159 | ||
4 | 160 | get userAPI() { |
161 | // Append to the serverURL if it's a relative fragment |
|
162 | let user = Svc.Prefs.get("userURL"); |
|
163 | if (user.indexOf(":") == -1) |
|
164 | user = this.serverURL + user; |
|
165 | return user + "1.0/"; |
|
166 | }, |
|
167 | ||
4 | 168 | get pwResetURL() { |
169 | return this.serverURL + "weave-password-reset"; |
|
170 | }, |
|
171 | ||
4 | 172 | get syncID() { |
173 | // Generate a random syncID id we don't have one |
|
174 | let syncID = Svc.Prefs.get("client.syncID", ""); |
|
175 | return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; |
|
176 | }, |
|
4 | 177 | set syncID(value) { |
178 | Svc.Prefs.set("client.syncID", value); |
|
179 | }, |
|
180 | ||
4 | 181 | get isLoggedIn() { return this._loggedIn; }, |
182 | ||
4 | 183 | get keyGenEnabled() { return this._keyGenEnabled; }, |
4 | 184 | set keyGenEnabled(value) { this._keyGenEnabled = value; }, |
185 | ||
186 | // nextSync and nextHeartbeat are in milliseconds, but prefs can't hold that much |
|
4 | 187 | get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, |
4 | 188 | set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), |
4 | 189 | get nextHeartbeat() Svc.Prefs.get("nextHeartbeat", 0) * 1000, |
4 | 190 | set nextHeartbeat(value) Svc.Prefs.set("nextHeartbeat", Math.floor(value / 1000)), |
191 | ||
4 | 192 | get syncInterval() { |
193 | // If we have a partial download, sync sooner if we're not mobile |
|
194 | if (Status.partial && Clients.clientType != "mobile") |
|
195 | return PARTIAL_DATA_SYNC; |
|
196 | return Svc.Prefs.get("syncInterval", MULTI_MOBILE_SYNC); |
|
197 | }, |
|
4 | 198 | set syncInterval(value) Svc.Prefs.set("syncInterval", value), |
199 | ||
4 | 200 | get syncThreshold() Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD), |
4 | 201 | set syncThreshold(value) Svc.Prefs.set("syncThreshold", value), |
202 | ||
60 | 203 | get globalScore() Svc.Prefs.get("globalScore", 0), |
52 | 204 | set globalScore(value) Svc.Prefs.set("globalScore", value), |
205 | ||
4 | 206 | get numClients() Svc.Prefs.get("numClients", 0), |
4 | 207 | set numClients(value) Svc.Prefs.set("numClients", value), |
208 | ||
4 | 209 | get locked() { return this._locked; }, |
4 | 210 | lock: function Svc_lock() { |
211 | if (this._locked) |
|
212 | return false; |
|
213 | this._locked = true; |
|
214 | return true; |
|
215 | }, |
|
4 | 216 | unlock: function Svc_unlock() { |
217 | this._locked = false; |
|
218 | }, |
|
219 | ||
6 | 220 | _updateCachedURLs: function _updateCachedURLs() { |
221 | // Nothing to cache yet if we don't have the building blocks |
|
12 | 222 | if (this.clusterURL == "" || this.username == "") |
4 | 223 | return; |
224 | ||
225 | let storageAPI = this.clusterURL + Svc.Prefs.get("storageAPI") + "/"; |
|
226 | let userBase = storageAPI + this.username + "/"; |
|
227 | this._log.debug("Caching URLs under storage user base: " + userBase); |
|
228 | ||
229 | // Generate and cache various URLs under the storage API for this user |
|
230 | this.infoURL = userBase + "info/collections"; |
|
231 | this.storageURL = userBase + "storage/"; |
|
232 | this.metaURL = this.storageURL + "meta/global"; |
|
233 | PubKeys.defaultKeyUri = this.storageURL + "keys/pubkey"; |
|
234 | PrivKeys.defaultKeyUri = this.storageURL + "keys/privkey"; |
|
235 | }, |
|
236 | ||
6 | 237 | _checkCrypto: function WeaveSvc__checkCrypto() { |
4 | 238 | let ok = false; |
239 | ||
4 | 240 | try { |
10 | 241 | let iv = Svc.Crypto.generateRandomIV(); |
8 | 242 | if (iv.length == 24) |
6 | 243 | ok = true; |
244 | ||
245 | } catch (e) { |
|
246 | this._log.debug("Crypto check failed: " + e); |
|
247 | } |
|
248 | ||
4 | 249 | return ok; |
250 | }, |
|
251 | ||
252 | /** |
|
253 | * Prepare to initialize the rest of Weave after waiting a little bit |
|
254 | */ |
|
6 | 255 | onStartup: function onStartup() { |
8 | 256 | this._initLogs(); |
16 | 257 | this._log.info("Loading Weave " + WEAVE_VERSION); |
258 | ||
6 | 259 | this.enabled = true; |
260 | ||
8 | 261 | this._registerEngines(); |
262 | ||
6 | 263 | let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. |
10 | 264 | getService(Ci.nsIHttpProtocolHandler).userAgent; |
12 | 265 | this._log.info(ua); |
266 | ||
10 | 267 | if (!this._checkCrypto()) { |
3 | 268 | this.enabled = false; |
6 | 269 | this._log.error("Could not load the Weave crypto component. Disabling " + |
270 | "Weave, since it will not work correctly."); |
|
271 | } |
|
272 | ||
14 | 273 | Svc.Obs.add("network:offline-status-changed", this); |
14 | 274 | Svc.Obs.add("private-browsing", this); |
14 | 275 | Svc.Obs.add("weave:service:sync:finish", this); |
14 | 276 | Svc.Obs.add("weave:service:sync:error", this); |
14 | 277 | Svc.Obs.add("weave:service:backoff:interval", this); |
14 | 278 | Svc.Obs.add("weave:engine:score:updated", this); |
279 | ||
6 | 280 | if (!this.enabled) |
6 | 281 | this._log.info("Weave Sync disabled"); |
282 | ||
283 | // Create Weave identities (for logging in, and for encryption) |
|
18 | 284 | ID.set('WeaveID', new Identity('Mozilla Services Password', this.username)); |
16 | 285 | Auth.defaultAuthenticator = new BasicAuthenticator(ID.get('WeaveID')); |
286 | ||
6 | 287 | ID.set('WeaveCryptoID', |
12 | 288 | new Identity('Mozilla Services Encryption Passphrase', this.username)); |
289 | ||
8 | 290 | this._updateCachedURLs(); |
291 | ||
292 | // Send an event now that Weave service is ready |
|
12 | 293 | Svc.Obs.notify("weave:service:ready"); |
294 | ||
295 | // Wait a little before checking how long to wait to autoconnect |
|
14 | 296 | if (this._checkSetup() == STATUS_OK && Svc.Prefs.get("autoconnect")) { |
297 | Utils.delay(function() { |
|
298 | // Figure out how many seconds to delay autoconnect based on the app |
|
299 | let wait = 3; |
|
300 | switch (Svc.AppInfo.ID) { |
|
301 | case FIREFOX_ID: |
|
302 | // Add one second delay for each busy tab in every window |
|
303 | let enum = Svc.WinMediator.getEnumerator("navigator:browser"); |
|
304 | while (enum.hasMoreElements()) { |
|
305 | Array.forEach(enum.getNext().gBrowser.mTabs, function(tab) { |
|
306 | wait += tab.hasAttribute("busy"); |
|
307 | }); |
|
308 | } |
|
309 | break; |
|
310 | } |
|
311 | ||
312 | this._log.debug("Autoconnecting in " + wait + " seconds"); |
|
313 | Utils.delay(this._autoConnect, wait * 1000, this, "_autoTimer"); |
|
314 | }, 2000, this, "_autoTimer"); |
|
2 | 315 | } |
316 | }, |
|
317 | ||
6 | 318 | _checkSetup: function WeaveSvc__checkSetup() { |
6 | 319 | if (!this.username) { |
12 | 320 | this._log.debug("checkSetup: no username set"); |
8 | 321 | Status.login = LOGIN_FAILED_NO_USERNAME; |
322 | } |
|
323 | else if (!Utils.mpLocked() && !this.password) { |
|
324 | this._log.debug("checkSetup: no password set"); |
|
325 | Status.login = LOGIN_FAILED_NO_PASSWORD; |
|
326 | } |
|
327 | else if (!Utils.mpLocked() && !this.passphrase) { |
|
328 | this._log.debug("checkSetup: no passphrase set"); |
|
329 | Status.login = LOGIN_FAILED_NO_PASSPHRASE; |
|
330 | } |
|
331 | ||
6 | 332 | return Status.service; |
333 | }, |
|
334 | ||
6 | 335 | _initLogs: function WeaveSvc__initLogs() { |
14 | 336 | this._log = Log4Moz.repository.getLogger("Service.Main"); |
2 | 337 | this._log.level = |
18 | 338 | Log4Moz.Level[Svc.Prefs.get("log.logger.service.main")]; |
339 | ||
8 | 340 | let formatter = new Log4Moz.BasicFormatter(); |
8 | 341 | let root = Log4Moz.repository.rootLogger; |
20 | 342 | root.level = Log4Moz.Level[Svc.Prefs.get("log.rootLogger")]; |
343 | ||
10 | 344 | let capp = new Log4Moz.ConsoleAppender(formatter); |
20 | 345 | capp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.console")]; |
10 | 346 | root.addAppender(capp); |
347 | ||
10 | 348 | let dapp = new Log4Moz.DumpAppender(formatter); |
20 | 349 | dapp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.dump")]; |
10 | 350 | root.addAppender(dapp); |
351 | ||
16 | 352 | let verbose = Svc.Directory.get("ProfD", Ci.nsIFile); |
12 | 353 | verbose.QueryInterface(Ci.nsILocalFile); |
10 | 354 | verbose.append("weave"); |
10 | 355 | verbose.append("logs"); |
10 | 356 | verbose.append("verbose-log.txt"); |
10 | 357 | if (!verbose.exists()) |
358 | verbose.create(verbose.NORMAL_FILE_TYPE, PERMS_FILE); |
|
359 | ||
4 | 360 | let maxSize = 65536; // 64 * 1024 (64KB) |
16 | 361 | this._debugApp = new Log4Moz.RotatingFileAppender(verbose, formatter, maxSize); |
20 | 362 | this._debugApp.level = Log4Moz.Level[Svc.Prefs.get("log.appender.debugLog")]; |
12 | 363 | root.addAppender(this._debugApp); |
364 | }, |
|
365 | ||
4 | 366 | clearLogs: function WeaveSvc_clearLogs() { |
367 | this._debugApp.clear(); |
|
368 | }, |
|
369 | ||
370 | /** |
|
371 | * Register the built-in engines for certain applications |
|
372 | */ |
|
6 | 373 | _registerEngines: function WeaveSvc__registerEngines() { |
4 | 374 | let engines = []; |
8 | 375 | switch (Svc.AppInfo.ID) { |
4 | 376 | case FENNEC_ID: |
377 | engines = ["Tab", "Bookmarks", "Form", "History", "Password"]; |
|
378 | break; |
|
379 | ||
4 | 380 | case "xuth@mozilla.org": |
381 | case FIREFOX_ID: |
|
16 | 382 | engines = ["Bookmarks", "Form", "History", "Password", "Prefs", "Tab"]; |
2 | 383 | break; |
384 | ||
385 | case SEAMONKEY_ID: |
|
386 | engines = ["Form", "History", "Password", "Tab"]; |
|
387 | break; |
|
388 | } |
|
389 | ||
390 | // Grab the actual engine and register them |
|
102 | 391 | Engines.register(engines.map(function(name) Weave[name + "Engine"])); |
392 | }, |
|
393 | ||
16 | 394 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
12 | 395 | Ci.nsISupportsWeakReference]), |
396 | ||
397 | // nsIObserver |
|
398 | ||
5 | 399 | observe: function WeaveSvc__observe(subject, topic, data) { |
3 | 400 | switch (topic) { |
401 | case "network:offline-status-changed": |
|
402 | // Whether online or offline, we'll reschedule syncs |
|
403 | this._log.trace("Network offline status change: " + data); |
|
404 | this._checkSyncStatus(); |
|
405 | break; |
|
406 | case "private-browsing": |
|
407 | // Entering or exiting private browsing? Reschedule syncs |
|
408 | this._log.trace("Private browsing change: " + data); |
|
409 | this._checkSyncStatus(); |
|
410 | break; |
|
411 | case "weave:service:sync:error": |
|
412 | this._handleSyncError(); |
|
413 | if (Status.sync == CREDENTIALS_CHANGED) { |
|
414 | this.logout(); |
|
415 | Utils.delay(function() this.login(), 0, this); |
|
416 | } |
|
417 | break; |
|
418 | case "weave:service:sync:finish": |
|
419 | this._scheduleNextSync(); |
|
420 | this._syncErrors = 0; |
|
421 | break; |
|
422 | case "weave:service:backoff:interval": |
|
423 | let interval = (data + Math.random() * data * 0.25) * 1000; // required backoff + up to 25% |
|
424 | Status.backoffInterval = interval; |
|
425 | Status.minimumNextSync = Date.now() + data; |
|
426 | break; |
|
427 | case "weave:engine:score:updated": |
|
4 | 428 | this._handleScoreUpdate(); |
1 | 429 | break; |
430 | case "idle": |
|
431 | this._log.trace("Idle time hit, trying to sync"); |
|
432 | Svc.Idle.removeIdleObserver(this, this._idleTime); |
|
433 | this._idleTime = 0; |
|
434 | Utils.delay(function() this.sync(false), 0, this); |
|
1 | 435 | break; |
1 | 436 | } |
437 | }, |
|
438 | ||
5 | 439 | _handleScoreUpdate: function WeaveSvc__handleScoreUpdate() { |
2 | 440 | const SCORE_UPDATE_DELAY = 3000; |
9 | 441 | Utils.delay(this._calculateScore, SCORE_UPDATE_DELAY, this, "_scoreTimer"); |
442 | }, |
|
443 | ||
5 | 444 | _calculateScore: function WeaveSvc_calculateScoreAndDoStuff() { |
4 | 445 | var engines = Engines.getEnabled(); |
45 | 446 | for (let i = 0;i < engines.length;i++) { |
96 | 447 | this._log.trace(engines[i].name + ": score: " + engines[i].score); |
54 | 448 | this.globalScore += engines[i].score; |
42 | 449 | engines[i]._tracker.resetScore(); |
450 | } |
|
451 | ||
8 | 452 | this._log.trace("Global score updated: " + this.globalScore); |
5 | 453 | this._checkSyncStatus(); |
454 | }, |
|
455 | ||
456 | // gets cluster from central LDAP server and returns it, or null on error |
|
4 | 457 | _findCluster: function _findCluster() { |
458 | this._log.debug("Finding cluster for user " + this.username); |
|
459 | ||
460 | let fail; |
|
461 | let res = new Resource(this.userAPI + this.username + "/node/weave"); |
|
462 | try { |
|
463 | let node = res.get(); |
|
464 | switch (node.status) { |
|
465 | case 400: |
|
466 | Status.login = LOGIN_FAILED_LOGIN_REJECTED; |
|
467 | fail = "Find cluster denied: " + this._errorStr(node); |
|
468 | break; |
|
469 | case 404: |
|
470 | this._log.debug("Using serverURL as data cluster (multi-cluster support disabled)"); |
|
471 | return this.serverURL; |
|
472 | case 0: |
|
473 | case 200: |
|
474 | if (node == "null") |
|
475 | node = null; |
|
476 | return node; |
|
477 | default: |
|
478 | this._log.debug("Unexpected response code: " + node.status); |
|
479 | break; |
|
480 | } |
|
481 | } catch (e) { |
|
482 | this._log.debug("Network error on findCluster"); |
|
483 | Status.login = LOGIN_FAILED_NETWORK_ERROR; |
|
484 | fail = e; |
|
485 | } |
|
486 | throw fail; |
|
487 | }, |
|
488 | ||
489 | // gets cluster from central LDAP server and sets this.clusterURL |
|
4 | 490 | _setCluster: function _setCluster() { |
491 | // Make sure we didn't get some unexpected response for the cluster |
|
492 | let cluster = this._findCluster(); |
|
493 | this._log.debug("cluster value = " + cluster); |
|
494 | if (cluster == null) |
|
495 | return false; |
|
496 | ||
497 | // Don't update stuff if we already have the right cluster |
|
498 | if (cluster == this.clusterURL) |
|
499 | return false; |
|
500 | ||
501 | this.clusterURL = cluster; |
|
502 | return true; |
|
503 | }, |
|
504 | ||
505 | // update cluster if required. returns false if the update was not required |
|
4 | 506 | _updateCluster: function _updateCluster() { |
507 | let cTime = Date.now(); |
|
508 | let lastUp = parseFloat(Svc.Prefs.get("lastClusterUpdate")); |
|
509 | if (!lastUp || ((cTime - lastUp) >= CLUSTER_BACKOFF)) { |
|
510 | if (this._setCluster()) { |
|
511 | Svc.Prefs.set("lastClusterUpdate", cTime.toString()); |
|
512 | return true; |
|
513 | } |
|
514 | } |
|
515 | return false; |
|
516 | }, |
|
517 | ||
4 | 518 | _verifyLogin: function _verifyLogin() |
519 | this._notify("verify-login", "", function() { |
|
520 | // Make sure we have a cluster to verify against |
|
521 | // this is a little weird, if we don't get a node we pretend |
|
522 | // to succeed, since that probably means we just don't have storage |
|
523 | if (this.clusterURL == "" && !this._setCluster()) { |
|
524 | Status.sync = NO_SYNC_NODE_FOUND; |
|
525 | Svc.Obs.notify("weave:service:sync:delayed"); |
|
526 | return true; |
|
527 | } |
|
528 | ||
529 | try { |
|
530 | let test = new Resource(this.infoURL).get(); |
|
531 | switch (test.status) { |
|
532 | case 200: |
|
533 | // The user is authenticated, so check the passphrase now |
|
534 | if (!this._verifyPassphrase()) { |
|
535 | Status.login = LOGIN_FAILED_INVALID_PASSPHRASE; |
|
536 | return false; |
|
537 | } |
|
538 | ||
539 | // Username/password and passphrase all verified |
|
540 | Status.login = LOGIN_SUCCEEDED; |
|
541 | return true; |
|
542 | ||
543 | case 401: |
|
544 | case 404: |
|
545 | // Check that we're verifying with the correct cluster |
|
546 | if (this._setCluster()) |
|
547 | return this._verifyLogin(); |
|
548 | ||
549 | // We must have the right cluster, but the server doesn't expect us |
|
550 | Status.login = LOGIN_FAILED_LOGIN_REJECTED; |
|
551 | return false; |
|
552 | ||
553 | default: |
|
554 | // Server didn't respond with something that we expected |
|
555 | this._checkServerError(test); |
|
556 | Status.login = LOGIN_FAILED_SERVER_ERROR; |
|
557 | return false; |
|
558 | } |
|
559 | } |
|
560 | catch (ex) { |
|
561 | // Must have failed on some network issue |
|
562 | this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex)); |
|
563 | Status.login = LOGIN_FAILED_NETWORK_ERROR; |
|
564 | return false; |
|
565 | } |
|
566 | })(), |
|
567 | ||
4 | 568 | _verifyPassphrase: function _verifyPassphrase() |
569 | this._catch(this._notify("verify-passphrase", "", function() { |
|
570 | // Don't allow empty/missing passphrase |
|
571 | if (!this.passphrase) |
|
572 | return false; |
|
573 | ||
574 | try { |
|
575 | let pubkey = PubKeys.getDefaultKey(); |
|
576 | let privkey = PrivKeys.get(pubkey.privateKeyUri); |
|
577 | return Svc.Crypto.verifyPassphrase( |
|
578 | privkey.payload.keyData, this.passphrase, |
|
579 | privkey.payload.salt, privkey.payload.iv |
|
580 | ); |
|
581 | } catch (e) { |
|
582 | // this means no keys are present (or there's a network error) |
|
583 | return true; |
|
584 | } |
|
585 | }))(), |
|
586 | ||
4 | 587 | changePassword: function WeaveSvc_changePassword(newpass) |
588 | this._notify("changepwd", "", function() { |
|
589 | let url = this.userAPI + this.username + "/password"; |
|
590 | try { |
|
591 | let resp = new Resource(url).post(newpass); |
|
592 | if (resp.status != 200) { |
|
593 | this._log.debug("Password change failed: " + resp); |
|
594 | return false; |
|
595 | } |
|
596 | } |
|
597 | catch(ex) { |
|
598 | // Must have failed on some network issue |
|
599 | this._log.debug("changePassword failed: " + Utils.exceptionStr(ex)); |
|
600 | return false; |
|
601 | } |
|
602 | ||
603 | // Save the new password for requests and login manager |
|
604 | this.password = newpass; |
|
605 | this.persistLogin(); |
|
606 | return true; |
|
607 | })(), |
|
608 | ||
4 | 609 | changePassphrase: function WeaveSvc_changePassphrase(newphrase) |
610 | this._catch(this._notify("changepph", "", function() { |
|
611 | /* Wipe */ |
|
612 | this.wipeServer(); |
|
613 | PubKeys.clearCache(); |
|
614 | PrivKeys.clearCache(); |
|
615 | ||
616 | this.logout(); |
|
617 | ||
618 | /* Set this so UI is updated on next run */ |
|
619 | this.passphrase = newphrase; |
|
620 | this.persistLogin(); |
|
621 | ||
622 | /* Login in sync: this also generates new keys */ |
|
623 | this.login(); |
|
624 | this.sync(true); |
|
625 | return true; |
|
626 | }))(), |
|
627 | ||
4 | 628 | startOver: function() { |
629 | // Set a username error so the status message shows "set up..." |
|
630 | Status.login = LOGIN_FAILED_NO_USERNAME; |
|
631 | this.logout(); |
|
632 | // Reset all engines |
|
633 | this.resetClient(); |
|
634 | // Reset Weave prefs |
|
635 | Svc.Prefs.resetBranch(""); |
|
636 | // set lastversion pref |
|
637 | Svc.Prefs.set("lastversion", WEAVE_VERSION); |
|
638 | // Find weave logins and remove them. |
|
639 | this.password = ""; |
|
640 | this.passphrase = ""; |
|
641 | Svc.Login.findLogins({}, PWDMGR_HOST, "", "").map(function(login) { |
|
642 | Svc.Login.removeLogin(login); |
|
643 | }); |
|
644 | Svc.Obs.notify("weave:service:start-over"); |
|
645 | }, |
|
646 | ||
12 | 647 | _autoConnect: let (attempts = 0) function _autoConnect() { |
648 | let reason = |
|
649 | Utils.mpLocked() ? "master password still locked" |
|
650 | : this._checkSync([kSyncNotLoggedIn, kFirstSyncChoiceNotMade]); |
|
651 | ||
652 | // Can't autoconnect if we're missing these values |
|
653 | if (!reason) { |
|
654 | if (!this.username || !this.password || !this.passphrase) |
|
655 | return; |
|
656 | ||
657 | // Nothing more to do on a successful login |
|
658 | if (this.login()) |
|
659 | return; |
|
660 | } |
|
661 | ||
662 | // Something failed, so try again some time later |
|
663 | let interval = this._calculateBackoff(++attempts, 60 * 1000); |
|
664 | this._log.debug("Autoconnect failed: " + (reason || Status.login) + |
|
665 | "; retry in " + Math.ceil(interval / 1000) + " sec."); |
|
666 | Utils.delay(function() this._autoConnect(), interval, this, "_autoTimer"); |
|
667 | }, |
|
668 | ||
4 | 669 | persistLogin: function persistLogin() { |
670 | // Canceled master password prompt can prevent these from succeeding |
|
671 | try { |
|
672 | ID.get("WeaveID").persist(); |
|
673 | ID.get("WeaveCryptoID").persist(); |
|
674 | } |
|
675 | catch(ex) {} |
|
676 | }, |
|
677 | ||
4 | 678 | login: function WeaveSvc_login(username, password, passphrase) |
679 | this._catch(this._lock(this._notify("login", "", function() { |
|
680 | this._loggedIn = false; |
|
681 | if (Svc.IO.offline) |
|
682 | throw "Application is offline, login should not be called"; |
|
683 | ||
684 | if (username) |
|
685 | this.username = username; |
|
686 | if (password) |
|
687 | this.password = password; |
|
688 | if (passphrase) |
|
689 | this.passphrase = passphrase; |
|
690 | ||
691 | if (this._checkSetup() == CLIENT_NOT_CONFIGURED) |
|
692 | throw "aborting login, client not configured"; |
|
693 | ||
694 | this._log.info("Logging in user " + this.username); |
|
695 | ||
696 | if (!this._verifyLogin()) { |
|
697 | // verifyLogin sets the failure states here. |
|
698 | throw "Login failed: " + Status.login; |
|
699 | } |
|
700 | ||
701 | // No need to try automatically connecting after a successful login |
|
702 | if (this._autoTimer) |
|
703 | this._autoTimer.clear(); |
|
704 | ||
705 | this._loggedIn = true; |
|
706 | // Try starting the sync timer now that we're logged in |
|
707 | this._checkSyncStatus(); |
|
708 | Svc.Prefs.set("autoconnect", true); |
|
709 | ||
710 | return true; |
|
711 | })))(), |
|
712 | ||
4 | 713 | logout: function WeaveSvc_logout() { |
714 | // No need to do anything if we're already logged out |
|
715 | if (!this._loggedIn) |
|
716 | return; |
|
717 | ||
718 | this._log.info("Logging out"); |
|
719 | this._loggedIn = false; |
|
720 | this._keyPair = {}; |
|
721 | ||
722 | // Cancel the sync timer now that we're logged out |
|
723 | this._checkSyncStatus(); |
|
724 | Svc.Prefs.set("autoconnect", false); |
|
725 | ||
726 | Svc.Obs.notify("weave:service:logout:finish"); |
|
727 | }, |
|
728 | ||
4 | 729 | _errorStr: function WeaveSvc__errorStr(code) { |
730 | switch (code.toString()) { |
|
731 | case "1": |
|
732 | return "illegal-method"; |
|
733 | case "2": |
|
734 | return "invalid-captcha"; |
|
735 | case "3": |
|
736 | return "invalid-username"; |
|
737 | case "4": |
|
738 | return "cannot-overwrite-resource"; |
|
739 | case "5": |
|
740 | return "userid-mismatch"; |
|
741 | case "6": |
|
742 | return "json-parse-failure"; |
|
743 | case "7": |
|
744 | return "invalid-password"; |
|
745 | case "8": |
|
746 | return "invalid-record"; |
|
747 | case "9": |
|
748 | return "weak-password"; |
|
749 | default: |
|
750 | return "generic-server-error"; |
|
751 | } |
|
752 | }, |
|
753 | ||
4 | 754 | checkUsername: function WeaveSvc_checkUsername(username) { |
755 | let url = this.userAPI + username; |
|
756 | let res = new Resource(url); |
|
757 | res.authenticator = new NoOpAuthenticator(); |
|
758 | ||
759 | let data = ""; |
|
760 | try { |
|
761 | data = res.get(); |
|
762 | if (data.status == 200) { |
|
763 | if (data == "0") |
|
764 | return "available"; |
|
765 | else if (data == "1") |
|
766 | return "notAvailable"; |
|
767 | } |
|
768 | ||
769 | } |
|
770 | catch(ex) {} |
|
771 | ||
772 | // Convert to the error string, or default to generic on exception |
|
773 | return this._errorStr(data); |
|
774 | }, |
|
775 | ||
4 | 776 | createAccount: function WeaveSvc_createAccount(username, password, email, |
777 | captchaChallenge, captchaResponse) |
|
778 | { |
|
779 | let payload = JSON.stringify({ |
|
780 | "password": password, "email": email, |
|
781 | "captcha-challenge": captchaChallenge, |
|
782 | "captcha-response": captchaResponse |
|
783 | }); |
|
784 | ||
785 | let url = this.userAPI + username; |
|
786 | let res = new Resource(url); |
|
787 | res.authenticator = new Weave.NoOpAuthenticator(); |
|
788 | if (Svc.Prefs.isSet("admin-secret")) |
|
789 | res.setHeader("X-Weave-Secret", Svc.Prefs.get("admin-secret", "")); |
|
790 | ||
791 | let error = "generic-server-error"; |
|
792 | try { |
|
793 | let register = res.put(payload); |
|
794 | if (register.success) { |
|
795 | this._log.info("Account created: " + register); |
|
796 | return null; |
|
797 | } |
|
798 | ||
799 | // Must have failed, so figure out the reason |
|
800 | if (register.status == 400) |
|
801 | error = this._errorStr(register); |
|
802 | } |
|
803 | catch(ex) { |
|
804 | this._log.warn("Failed to create account: " + ex); |
|
805 | } |
|
806 | ||
807 | return error; |
|
808 | }, |
|
809 | ||
810 | // stuff we need to to after login, before we can really do |
|
811 | // anything (e.g. key setup) |
|
4 | 812 | _remoteSetup: function WeaveSvc__remoteSetup() { |
813 | let reset = false; |
|
814 | ||
815 | this._log.trace("Fetching global metadata record"); |
|
816 | let meta = Records.import(this.metaURL); |
|
817 | ||
818 | let remoteVersion = (meta && meta.payload.storageVersion)? |
|
819 | meta.payload.storageVersion : ""; |
|
820 | ||
821 | this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:", |
|
822 | STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" ")); |
|
823 | ||
824 | // Check for cases that require a fresh start. When comparing remoteVersion, |
|
825 | // we need to convert it to a number as older clients used it as a string. |
|
826 | if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || |
|
827 | STORAGE_VERSION > parseFloat(remoteVersion)) { |
|
828 | ||
829 | // abort the server wipe if the GET status was anything other than 404 or 200 |
|
830 | let status = Records.response.status; |
|
831 | if (status != 200 && status != 404) { |
|
832 | this._checkServerError(Records.response); |
|
833 | Status.sync = METARECORD_DOWNLOAD_FAIL; |
|
834 | this._log.warn("Unknown error while downloading metadata record. " + |
|
835 | "Aborting sync."); |
|
836 | return false; |
|
837 | } |
|
838 | ||
839 | if (!meta) |
|
840 | this._log.info("No metadata record, server wipe needed"); |
|
841 | if (meta && !meta.payload.syncID) |
|
842 | this._log.warn("No sync id, server wipe needed"); |
|
843 | ||
844 | if (!this._keyGenEnabled) { |
|
845 | this._log.info("...and key generation is disabled. Not wiping. " + |
|
846 | "Aborting sync."); |
|
847 | Status.sync = DESKTOP_VERSION_OUT_OF_DATE; |
|
848 | return false; |
|
849 | } |
|
850 | reset = true; |
|
851 | this._log.info("Wiping server data"); |
|
852 | this._freshStart(); |
|
853 | ||
854 | if (status == 404) |
|
855 | this._log.info("Metadata record not found, server wiped to ensure " + |
|
856 | "consistency."); |
|
857 | else // 200 |
|
858 | this._log.info("Wiped server; incompatible metadata: " + remoteVersion); |
|
859 | ||
860 | } |
|
861 | else if (remoteVersion > STORAGE_VERSION) { |
|
862 | Status.sync = VERSION_OUT_OF_DATE; |
|
863 | this._log.warn("Upgrade required to access newer storage version."); |
|
864 | return false; |
|
865 | } |
|
866 | else if (meta.payload.syncID != this.syncID) { |
|
867 | this.resetClient(); |
|
868 | this.syncID = meta.payload.syncID; |
|
869 | this._log.debug("Clear cached values and take syncId: " + this.syncID); |
|
870 | ||
871 | // XXX Bug 531005 Wait long enough to allow potentially another concurrent |
|
872 | // sync to finish generating the keypair and uploading them |
|
873 | Sync.sleep(15000); |
|
874 | ||
875 | // bug 545725 - re-verify creds and fail sanely |
|
876 | if (!this._verifyLogin()) { |
|
877 | Status.sync = CREDENTIALS_CHANGED; |
|
878 | this._log.info("Credentials have changed, aborting sync and forcing re-login."); |
|
879 | return false; |
|
880 | } |
|
881 | } |
|
882 | ||
883 | let needKeys = true; |
|
884 | let pubkey = PubKeys.getDefaultKey(); |
|
885 | if (!pubkey) |
|
886 | this._log.debug("Could not get public key"); |
|
887 | else if (pubkey.keyData == null) |
|
888 | this._log.debug("Public key has no key data"); |
|
889 | else { |
|
890 | // make sure we have a matching privkey |
|
891 | let privkey = PrivKeys.get(pubkey.privateKeyUri); |
|
892 | if (!privkey) |
|
893 | this._log.debug("Could not get private key"); |
|
894 | else if (privkey.keyData == null) |
|
895 | this._log.debug("Private key has no key data"); |
|
896 | else |
|
897 | return true; |
|
898 | } |
|
899 | ||
900 | if (needKeys) { |
|
901 | if (PubKeys.response.status != 404 && PrivKeys.response.status != 404) { |
|
902 | this._log.warn("Couldn't download keys from server, aborting sync"); |
|
903 | this._log.debug("PubKey HTTP status: " + PubKeys.response.status); |
|
904 | this._log.debug("PrivKey HTTP status: " + PrivKeys.response.status); |
|
905 | this._checkServerError(PubKeys.response); |
|
906 | this._checkServerError(PrivKeys.response); |
|
907 | Status.sync = KEYS_DOWNLOAD_FAIL; |
|
908 | return false; |
|
909 | } |
|
910 | ||
911 | if (!this._keyGenEnabled) { |
|
912 | this._log.warn("Couldn't download keys from server, and key generation" + |
|
913 | "is disabled. Aborting sync"); |
|
914 | Status.sync = NO_KEYS_NO_KEYGEN; |
|
915 | return false; |
|
916 | } |
|
917 | ||
918 | if (!reset) { |
|
919 | this._log.warn("Calling freshStart from !reset case."); |
|
920 | this._freshStart(); |
|
921 | this._log.info("Server data wiped to ensure consistency due to missing keys"); |
|
922 | } |
|
923 | ||
924 | let passphrase = ID.get("WeaveCryptoID"); |
|
925 | if (passphrase.password) { |
|
926 | let keys = PubKeys.createKeypair(passphrase, PubKeys.defaultKeyUri, |
|
927 | PrivKeys.defaultKeyUri); |
|
928 | try { |
|
929 | // Upload and cache the keypair |
|
930 | PubKeys.uploadKeypair(keys); |
|
931 | PubKeys.set(keys.pubkey.uri, keys.pubkey); |
|
932 | PrivKeys.set(keys.privkey.uri, keys.privkey); |
|
933 | return true; |
|
934 | } catch (e) { |
|
935 | Status.sync = KEYS_UPLOAD_FAIL; |
|
936 | this._log.error("Could not upload keys: " + Utils.exceptionStr(e)); |
|
937 | } |
|
938 | } else { |
|
939 | Status.sync = SETUP_FAILED_NO_PASSPHRASE; |
|
940 | this._log.warn("Could not get encryption passphrase"); |
|
941 | } |
|
942 | } |
|
943 | ||
944 | return false; |
|
945 | }, |
|
946 | ||
947 | /** |
|
948 | * Determine if a sync should run. |
|
949 | * |
|
950 | * @param ignore [optional] |
|
951 | * array of reasons to ignore when checking |
|
952 | * |
|
953 | * @return Reason for not syncing; not-truthy if sync should run |
|
954 | */ |
|
5 | 955 | _checkSync: function WeaveSvc__checkSync(ignore) { |
2 | 956 | let reason = ""; |
3 | 957 | if (!this.enabled) |
3 | 958 | reason = kSyncWeaveDisabled; |
959 | else if (Svc.IO.offline) |
|
960 | reason = kSyncNetworkOffline; |
|
961 | else if (Svc.Private && Svc.Private.privateBrowsingEnabled) |
|
962 | // Svc.Private doesn't exist on Fennec -- don't assume it's there. |
|
963 | reason = kSyncInPrivateBrowsing; |
|
964 | else if (Status.minimumNextSync > Date.now()) |
|
965 | reason = kSyncBackoffNotMet; |
|
966 | else if (!this._loggedIn) |
|
967 | reason = kSyncNotLoggedIn; |
|
968 | else if (Svc.Prefs.get("firstSync") == "notReady") |
|
969 | reason = kFirstSyncChoiceNotMade; |
|
970 | ||
8 | 971 | if (ignore && ignore.indexOf(reason) != -1) |
972 | return ""; |
|
973 | ||
2 | 974 | return reason; |
975 | }, |
|
976 | ||
977 | /** |
|
978 | * Remove any timers/observers that might trigger a sync |
|
979 | */ |
|
5 | 980 | _clearSyncTriggers: function _clearSyncTriggers() { |
981 | // Clear out any scheduled syncs |
|
2 | 982 | if (this._syncTimer) |
983 | this._syncTimer.clear(); |
|
2 | 984 | if (this._heartbeatTimer) |
985 | this._heartbeatTimer.clear(); |
|
986 | ||
987 | // Clear out a sync that's just waiting for idle if we happen to have one |
|
1 | 988 | try { |
6 | 989 | Svc.Idle.removeIdleObserver(this, this._idleTime); |
990 | this._idleTime = 0; |
|
991 | } |
|
6 | 992 | catch(ex) {} |
993 | }, |
|
994 | ||
995 | /** |
|
996 | * Check if we should be syncing and schedule the next sync, if it's not scheduled |
|
997 | */ |
|
5 | 998 | _checkSyncStatus: function WeaveSvc__checkSyncStatus() { |
999 | // Should we be syncing now, if not, cancel any sync timers and return |
|
1000 | // if we're in backoff, we'll schedule the next sync |
|
6 | 1001 | if (this._checkSync([kSyncBackoffNotMet])) { |
4 | 1002 | this._clearSyncTriggers(); |
2 | 1003 | return; |
1004 | } |
|
1005 | ||
1006 | // Only set the wait time to 0 if we need to sync right away |
|
1007 | let wait; |
|
1008 | if (this.globalScore > this.syncThreshold) { |
|
1009 | this._log.debug("Global Score threshold hit, triggering sync."); |
|
1010 | wait = 0; |
|
1011 | } |
|
1012 | this._scheduleNextSync(wait); |
|
1013 | }, |
|
1014 | ||
1015 | /** |
|
1016 | * Call sync() on an idle timer |
|
1017 | * |
|
1018 | * delay is optional |
|
1019 | */ |
|
4 | 1020 | syncOnIdle: function WeaveSvc_syncOnIdle(delay) { |
1021 | // No need to add a duplicate idle observer |
|
1022 | if (this._idleTime) |
|
1023 | return; |
|
1024 | ||
1025 | this._idleTime = delay || IDLE_TIME; |
|
1026 | this._log.debug("Idle timer created for sync, will sync after " + |
|
1027 | this._idleTime + " seconds of inactivity."); |
|
1028 | Svc.Idle.addIdleObserver(this, this._idleTime); |
|
1029 | }, |
|
1030 | ||
1031 | /** |
|
1032 | * Set a timer for the next sync |
|
1033 | */ |
|
4 | 1034 | _scheduleNextSync: function WeaveSvc__scheduleNextSync(interval) { |
1035 | // Figure out when to sync next if not given a interval to wait |
|
1036 | if (interval == null) { |
|
1037 | // Check if we had a pending sync from last time |
|
1038 | if (this.nextSync != 0) |
|
1039 | interval = this.nextSync - Date.now(); |
|
1040 | // Use the bigger of default sync interval and backoff |
|
1041 | else |
|
1042 | interval = Math.max(this.syncInterval, Status.backoffInterval); |
|
1043 | } |
|
1044 | ||
1045 | // Start the sync right away if we're already late |
|
1046 | if (interval <= 0) { |
|
1047 | this.syncOnIdle(); |
|
1048 | return; |
|
1049 | } |
|
1050 | ||
1051 | this._log.trace("Next sync in " + Math.ceil(interval / 1000) + " sec."); |
|
1052 | Utils.delay(function() this.syncOnIdle(), interval, this, "_syncTimer"); |
|
1053 | ||
1054 | // Save the next sync time in-case sync is disabled (logout/offline/etc.) |
|
1055 | this.nextSync = Date.now() + interval; |
|
1056 | ||
1057 | // if we're a single client, set up a heartbeat to detect new clients sooner |
|
1058 | if (this.numClients == 1) |
|
1059 | this._scheduleHeartbeat(); |
|
1060 | }, |
|
1061 | ||
1062 | /** |
|
1063 | * Hits info/collections on the server to see if there are new clients. |
|
1064 | * This is only called when the account has one active client, and if more |
|
1065 | * are found will trigger a sync to change client sync frequency and update data. |
|
1066 | */ |
|
1067 | ||
4 | 1068 | _doHeartbeat: function WeaveSvc__doHeartbeat() { |
1069 | if (this._heartbeatTimer) |
|
1070 | this._heartbeatTimer.clear(); |
|
1071 | ||
1072 | this.nextHeartbeat = 0; |
|
1073 | let info = null; |
|
1074 | try { |
|
1075 | info = new Resource(this.infoURL).get(); |
|
1076 | if (info && info.success) { |
|
1077 | // if clients.lastModified doesn't match what the server has, |
|
1078 | // we have another client in play |
|
1079 | this._log.trace("Remote timestamp:" + info.obj["clients"] + |
|
1080 | " Local timestamp: " + Clients.lastSync); |
|
1081 | if (info.obj["clients"] > Clients.lastSync) { |
|
1082 | this._log.debug("New clients detected, triggering a full sync"); |
|
1083 | this.syncOnIdle(); |
|
1084 | return; |
|
1085 | } |
|
1086 | } |
|
1087 | else { |
|
1088 | this._checkServerError(info); |
|
1089 | this._log.debug("Heartbeat failed. HTTP Error: " + info.status); |
|
1090 | } |
|
1091 | } catch(ex) { |
|
1092 | // if something throws unexpectedly, not a big deal |
|
1093 | this._log.debug("Heartbeat failed unexpectedly: " + ex); |
|
1094 | } |
|
1095 | ||
1096 | // no joy, schedule the next heartbeat |
|
1097 | this._scheduleHeartbeat(); |
|
1098 | }, |
|
1099 | ||
1100 | /** |
|
1101 | * Sets up a heartbeat ping to check for new clients. This is not a critical |
|
1102 | * behaviour for the client, so if we hit server/network issues, we'll just drop |
|
1103 | * this until the next sync. |
|
1104 | */ |
|
4 | 1105 | _scheduleHeartbeat: function WeaveSvc__scheduleNextHeartbeat() { |
1106 | if (this._heartbeatTimer) |
|
1107 | return; |
|
1108 | ||
1109 | let now = Date.now(); |
|
1110 | if (this.nextHeartbeat && this.nextHeartbeat < now) { |
|
1111 | this._doHeartbeat(); |
|
1112 | return; |
|
1113 | } |
|
1114 | ||
1115 | // if the next sync is in less than an hour, don't bother |
|
1116 | let interval = MULTI_DESKTOP_SYNC; |
|
1117 | if (this.nextSync < Date.now() + interval || |
|
1118 | Status.enforceBackoff) |
|
1119 | return; |
|
1120 | ||
1121 | if (this.nextHeartbeat) |
|
1122 | interval = this.nextHeartbeat - now; |
|
1123 | else |
|
1124 | this.nextHeartbeat = now + interval; |
|
1125 | ||
1126 | this._log.trace("Setting up heartbeat, next ping in " + |
|
1127 | Math.ceil(interval / 1000) + " sec."); |
|
1128 | Utils.delay(function() this._doHeartbeat(), interval, this, "_heartbeatTimer"); |
|
1129 | }, |
|
1130 | ||
4 | 1131 | _syncErrors: 0, |
1132 | /** |
|
1133 | * Deal with sync errors appropriately |
|
1134 | */ |
|
4 | 1135 | _handleSyncError: function WeaveSvc__handleSyncError() { |
1136 | this._syncErrors++; |
|
1137 | ||
1138 | // do nothing on the first couple of failures, if we're not in backoff due to 5xx errors |
|
1139 | if (!Status.enforceBackoff) { |
|
1140 | if (this._syncErrors < 3) { |
|
1141 | this._scheduleNextSync(); |
|
1142 | return; |
|
1143 | } |
|
1144 | Status.enforceBackoff = true; |
|
1145 | } |
|
1146 | ||
1147 | const MINIMUM_BACKOFF_INTERVAL = 15 * 60 * 1000; // 15 minutes |
|
1148 | let interval = this._calculateBackoff(this._syncErrors, MINIMUM_BACKOFF_INTERVAL); |
|
1149 | ||
1150 | this._scheduleNextSync(interval); |
|
1151 | ||
1152 | let d = new Date(Date.now() + interval); |
|
1153 | this._log.config("Starting backoff, next sync at:" + d.toString()); |
|
1154 | }, |
|
1155 | ||
1156 | /** |
|
1157 | * Sync up engines with the server. |
|
1158 | */ |
|
4 | 1159 | sync: function sync() |
1160 | this._catch(this._lock(this._notify("sync", "", function() { |
|
1161 | ||
1162 | let syncStartTime = Date.now(); |
|
1163 | ||
1164 | Status.resetSync(); |
|
1165 | ||
1166 | // Make sure we should sync or record why we shouldn't |
|
1167 | let reason = this._checkSync(); |
|
1168 | if (reason) { |
|
1169 | // this is a purposeful abort rather than a failure, so don't set |
|
1170 | // any status bits |
|
1171 | reason = "Can't sync: " + reason; |
|
1172 | throw reason; |
|
1173 | } |
|
1174 | ||
1175 | // if we don't have a node, get one. if that fails, retry in 10 minutes |
|
1176 | if (this.clusterURL == "" && !this._setCluster()) { |
|
1177 | this._scheduleNextSync(10 * 60 * 1000); |
|
1178 | return; |
|
1179 | } |
|
1180 | ||
1181 | // Clear out any potentially pending syncs now that we're syncing |
|
1182 | this._clearSyncTriggers(); |
|
1183 | this.nextSync = 0; |
|
1184 | this.nextHeartbeat = 0; |
|
1185 | ||
1186 | // reset backoff info, if the server tells us to continue backing off, |
|
1187 | // we'll handle that later |
|
1188 | Status.resetBackoff(); |
|
1189 | ||
1190 | this.globalScore = 0; |
|
1191 | ||
1192 | // Ping the server with a special info request once a day |
|
1193 | let infoURL = this.infoURL; |
|
1194 | let now = Math.floor(Date.now() / 1000); |
|
1195 | let lastPing = Svc.Prefs.get("lastPing", 0); |
|
1196 | if (now - lastPing > 86400) { // 60 * 60 * 24 |
|
1197 | infoURL += "?v=" + WEAVE_VERSION; |
|
1198 | Svc.Prefs.set("lastPing", now); |
|
1199 | } |
|
1200 | ||
1201 | // Figure out what the last modified time is for each collection |
|
1202 | let info = new Resource(infoURL).get(); |
|
1203 | if (!info.success) |
|
1204 | throw "aborting sync, failed to get collections"; |
|
1205 | ||
1206 | // Convert the response to an object and read out the modified times |
|
1207 | for each (let engine in [Clients].concat(Engines.getAll())) |
|
1208 | engine.lastModified = info.obj[engine.name] || 0; |
|
1209 | ||
1210 | if (!(this._remoteSetup())) |
|
1211 | throw "aborting sync, remote setup failed"; |
|
1212 | ||
1213 | // Make sure we have an up-to-date list of clients before sending commands |
|
1214 | this._log.trace("Refreshing client list"); |
|
1215 | Clients.sync(); |
|
1216 | ||
1217 | // Wipe data in the desired direction if necessary |
|
1218 | switch (Svc.Prefs.get("firstSync")) { |
|
1219 | case "resetClient": |
|
1220 | this.resetClient(Engines.getEnabled().map(function(e) e.name)); |
|
1221 | break; |
|
1222 | case "wipeClient": |
|
1223 | this.wipeClient(Engines.getEnabled().map(function(e) e.name)); |
|
1224 | break; |
|
1225 | case "wipeRemote": |
|
1226 | this.wipeRemote(Engines.getEnabled().map(function(e) e.name)); |
|
1227 | break; |
|
1228 | } |
|
1229 | ||
1230 | // Process the incoming commands if we have any |
|
1231 | if (Clients.localCommands) { |
|
1232 | try { |
|
1233 | if (!(this.processCommands())) { |
|
1234 | Status.sync = ABORT_SYNC_COMMAND; |
|
1235 | throw "aborting sync, process commands said so"; |
|
1236 | } |
|
1237 | ||
1238 | // Repeat remoteSetup in-case the commands forced us to reset |
|
1239 | if (!(this._remoteSetup())) |
|
1240 | throw "aborting sync, remote setup failed after processing commands"; |
|
1241 | } |
|
1242 | finally { |
|
1243 | // Always immediately push back the local client (now without commands) |
|
1244 | Clients.sync(); |
|
1245 | } |
|
1246 | } |
|
1247 | ||
1248 | // Update the client mode now because it might change what we sync |
|
1249 | this._updateClientMode(); |
|
1250 | ||
1251 | try { |
|
1252 | for each (let engine in Engines.getEnabled()) { |
|
1253 | // If there's any problems with syncing the engine, report the failure |
|
1254 | if (!(this._syncEngine(engine)) || Status.enforceBackoff) { |
|
1255 | this._log.info("Aborting sync"); |
|
1256 | break; |
|
1257 | } |
|
1258 | } |
|
1259 | ||
1260 | // Upload meta/global if any engines changed anything |
|
1261 | let meta = Records.get(this.metaURL); |
|
1262 | if (meta.changed) |
|
1263 | new Resource(meta.uri).put(meta); |
|
1264 | ||
1265 | if (this._syncError) |
|
1266 | throw "Some engines did not sync correctly"; |
|
1267 | else { |
|
1268 | Svc.Prefs.set("lastSync", new Date().toString()); |
|
1269 | Status.sync = SYNC_SUCCEEDED; |
|
1270 | let syncTime = ((Date.now() - syncStartTime) / 1000).toFixed(2); |
|
1271 | this._log.info("Sync completed successfully in " + syncTime + " secs."); |
|
1272 | } |
|
1273 | } finally { |
|
1274 | this._syncError = false; |
|
1275 | Svc.Prefs.reset("firstSync"); |
|
1276 | } |
|
1277 | })))(), |
|
1278 | ||
1279 | /** |
|
1280 | * Process the locally stored clients list to figure out what mode to be in |
|
1281 | */ |
|
4 | 1282 | _updateClientMode: function _updateClientMode() { |
1283 | // Nothing to do if it's the same amount |
|
1284 | let {numClients, hasMobile} = Clients.stats; |
|
1285 | if (this.numClients == numClients) |
|
1286 | return; |
|
1287 | ||
1288 | this._log.debug("Client count: " + this.numClients + " -> " + numClients); |
|
1289 | this.numClients = numClients; |
|
1290 | ||
1291 | if (numClients == 1) { |
|
1292 | this.syncInterval = SINGLE_USER_SYNC; |
|
1293 | this.syncThreshold = SINGLE_USER_THRESHOLD; |
|
1294 | } |
|
1295 | else { |
|
1296 | this.syncInterval = hasMobile ? MULTI_MOBILE_SYNC : MULTI_DESKTOP_SYNC; |
|
1297 | this.syncThreshold = hasMobile ? MULTI_MOBILE_THRESHOLD : MULTI_DESKTOP_THRESHOLD; |
|
1298 | } |
|
1299 | }, |
|
1300 | ||
1301 | // returns true if sync should proceed |
|
1302 | // false / no return value means sync should be aborted |
|
4 | 1303 | _syncEngine: function WeaveSvc__syncEngine(engine) { |
1304 | try { |
|
1305 | engine.sync(); |
|
1306 | return true; |
|
1307 | } |
|
1308 | catch(e) { |
|
1309 | // maybe a 401, cluster update needed? |
|
1310 | if (e.status == 401 && this._updateCluster()) |
|
1311 | return this._syncEngine(engine); |
|
1312 | ||
1313 | this._checkServerError(e); |
|
1314 | ||
1315 | Status.engines = [engine.name, e.failureCode || ENGINE_UNKNOWN_FAIL]; |
|
1316 | ||
1317 | this._syncError = true; |
|
1318 | this._log.debug(Utils.exceptionStr(e)); |
|
1319 | return true; |
|
1320 | } |
|
1321 | finally { |
|
1322 | // If this engine has more to fetch, remember that globally |
|
1323 | if (engine.toFetch != null && engine.toFetch.length > 0) |
|
1324 | Status.partial = true; |
|
1325 | } |
|
1326 | }, |
|
1327 | ||
4 | 1328 | _freshStart: function WeaveSvc__freshStart() { |
1329 | this.resetClient(); |
|
1330 | ||
1331 | let meta = new WBORecord(this.metaURL); |
|
1332 | meta.payload.syncID = this.syncID; |
|
1333 | meta.payload.storageVersion = STORAGE_VERSION; |
|
1334 | ||
1335 | this._log.debug("New metadata record: " + JSON.stringify(meta.payload)); |
|
1336 | let resp = new Resource(meta.uri).put(meta); |
|
1337 | if (!resp.success) |
|
1338 | throw resp; |
|
1339 | Records.set(meta.uri, meta); |
|
1340 | ||
1341 | // Wipe everything we know about except meta because we just uploaded it |
|
1342 | let collections = [Clients].concat(Engines.getAll()).map(function(engine) { |
|
1343 | return engine.name; |
|
1344 | }); |
|
1345 | this.wipeServer(["crypto", "keys"].concat(collections)); |
|
1346 | }, |
|
1347 | ||
1348 | /** |
|
1349 | * Check to see if this is a failure |
|
1350 | * |
|
1351 | */ |
|
4 | 1352 | _checkServerError: function WeaveSvc__checkServerError(resp) { |
1353 | if (Utils.checkStatus(resp.status, null, [500, [502, 504]])) { |
|
1354 | Status.enforceBackoff = true; |
|
1355 | if (resp.status == 503 && resp.headers["retry-after"]) |
|
1356 | Observers.notify("weave:service:backoff:interval", parseInt(resp.headers["retry-after"], 10)); |
|
1357 | } |
|
1358 | }, |
|
1359 | /** |
|
1360 | * Return a value for a backoff interval. Maximum is eight hours, unless |
|
1361 | * Status.backoffInterval is higher. |
|
1362 | * |
|
1363 | */ |
|
4 | 1364 | _calculateBackoff: function WeaveSvc__calculateBackoff(attempts, base_interval) { |
1365 | const MAXIMUM_BACKOFF_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours |
|
1366 | let backoffInterval = attempts * |
|
1367 | (Math.floor(Math.random() * base_interval) + |
|
1368 | base_interval); |
|
1369 | return Math.max(Math.min(backoffInterval, MAXIMUM_BACKOFF_INTERVAL), Status.backoffInterval); |
|
1370 | }, |
|
1371 | ||
1372 | /** |
|
1373 | * Wipe user data from the server. |
|
1374 | * |
|
1375 | * @param collections [optional] |
|
1376 | * Array of collections to wipe. If not given, all collections are wiped. |
|
1377 | */ |
|
4 | 1378 | wipeServer: function WeaveSvc_wipeServer(collections) |
1379 | this._catch(this._notify("wipe-server", "", function() { |
|
1380 | if (!collections) { |
|
1381 | collections = []; |
|
1382 | let info = new Resource(this.infoURL).get(); |
|
1383 | for (let name in info.obj) |
|
1384 | collections.push(name); |
|
1385 | } |
|
1386 | for each (let name in collections) { |
|
1387 | try { |
|
1388 | new Resource(this.storageURL + name).delete(); |
|
1389 | new Resource(this.storageURL + "crypto/" + name).delete(); |
|
1390 | } |
|
1391 | catch(ex) { |
|
1392 | this._log.debug("Exception on wipe of '" + name + "': " + Utils.exceptionStr(ex)); |
|
1393 | } |
|
1394 | } |
|
1395 | }))(), |
|
1396 | ||
1397 | /** |
|
1398 | * Wipe all local user data. |
|
1399 | * |
|
1400 | * @param engines [optional] |
|
1401 | * Array of engine names to wipe. If not given, all engines are used. |
|
1402 | */ |
|
4 | 1403 | wipeClient: function WeaveSvc_wipeClient(engines) |
1404 | this._catch(this._notify("wipe-client", "", function() { |
|
1405 | // If we don't have any engines, reset the service and wipe all engines |
|
1406 | if (!engines) { |
|
1407 | // Clear out any service data |
|
1408 | this.resetService(); |
|
1409 | ||
1410 | engines = [Clients].concat(Engines.getAll()); |
|
1411 | } |
|
1412 | // Convert the array of names into engines |
|
1413 | else |
|
1414 | engines = Engines.get(engines); |
|
1415 | ||
1416 | // Fully wipe each engine if it's able to decrypt data |
|
1417 | for each (let engine in engines) |
|
1418 | if (engine._testDecrypt()) |
|
1419 | engine.wipeClient(); |
|
1420 | ||
1421 | // Save the password/passphrase just in-case they aren't restored by sync |
|
1422 | this.persistLogin(); |
|
1423 | }))(), |
|
1424 | ||
1425 | /** |
|
1426 | * Wipe all remote user data by wiping the server then telling each remote |
|
1427 | * client to wipe itself. |
|
1428 | * |
|
1429 | * @param engines [optional] |
|
1430 | * Array of engine names to wipe. If not given, all engines are used. |
|
1431 | */ |
|
4 | 1432 | wipeRemote: function WeaveSvc_wipeRemote(engines) |
1433 | this._catch(this._notify("wipe-remote", "", function() { |
|
1434 | // Make sure stuff gets uploaded |
|
1435 | this.resetClient(engines); |
|
1436 | ||
1437 | // Clear out any server data |
|
1438 | this.wipeServer(engines); |
|
1439 | ||
1440 | // Only wipe the engines provided |
|
1441 | if (engines) |
|
1442 | engines.forEach(function(e) this.prepCommand("wipeEngine", [e]), this); |
|
1443 | // Tell the remote machines to wipe themselves |
|
1444 | else |
|
1445 | this.prepCommand("wipeAll", []); |
|
1446 | ||
1447 | // Make sure the changed clients get updated |
|
1448 | Clients.sync(); |
|
1449 | }))(), |
|
1450 | ||
1451 | /** |
|
1452 | * Reset local service information like logs, sync times, caches. |
|
1453 | */ |
|
4 | 1454 | resetService: function WeaveSvc_resetService() |
1455 | this._catch(this._notify("reset-service", "", function() { |
|
1456 | // First drop old logs to track client resetting behavior |
|
1457 | this.clearLogs(); |
|
1458 | this._log.info("Logs reinitialized for service reset"); |
|
1459 | ||
1460 | // Pretend we've never synced to the server and drop cached data |
|
1461 | this.syncID = ""; |
|
1462 | Svc.Prefs.reset("lastSync"); |
|
1463 | for each (let cache in [PubKeys, PrivKeys, CryptoMetas, Records]) |
|
1464 | cache.clearCache(); |
|
1465 | }))(), |
|
1466 | ||
1467 | /** |
|
1468 | * Reset the client by getting rid of any local server data and client data. |
|
1469 | * |
|
1470 | * @param engines [optional] |
|
1471 | * Array of engine names to reset. If not given, all engines are used. |
|
1472 | */ |
|
4 | 1473 | resetClient: function WeaveSvc_resetClient(engines) |
1474 | this._catch(this._notify("reset-client", "", function() { |
|
1475 | // If we don't have any engines, reset everything including the service |
|
1476 | if (!engines) { |
|
1477 | // Clear out any service data |
|
1478 | this.resetService(); |
|
1479 | ||
1480 | engines = [Clients].concat(Engines.getAll()); |
|
1481 | } |
|
1482 | // Convert the array of names into engines |
|
1483 | else |
|
1484 | engines = Engines.get(engines); |
|
1485 | ||
1486 | // Have each engine drop any temporary meta data |
|
1487 | for each (let engine in engines) |
|
1488 | engine.resetClient(); |
|
1489 | }))(), |
|
1490 | ||
1491 | /** |
|
1492 | * A hash of valid commands that the client knows about. The key is a command |
|
1493 | * and the value is a hash containing information about the command such as |
|
1494 | * number of arguments and description. |
|
1495 | */ |
|
4 | 1496 | _commands: [ |
26 | 1497 | ["resetAll", 0, "Clear temporary local data for all engines"], |
26 | 1498 | ["resetEngine", 1, "Clear temporary local data for engine"], |
26 | 1499 | ["wipeAll", 0, "Delete all client data for all engines"], |
26 | 1500 | ["wipeEngine", 1, "Delete all client data for engine"], |
28 | 1501 | ["logout", 0, "Log out client"], |
12 | 1502 | ].reduce(function WeaveSvc__commands(commands, entry) { |
70 | 1503 | commands[entry[0]] = {}; |
300 | 1504 | for (let [i, attr] in Iterator(["args", "desc"])) |
320 | 1505 | commands[entry[0]][attr] = entry[i + 1]; |
20 | 1506 | return commands; |
8 | 1507 | }, {}), |
1508 | ||
1509 | /** |
|
1510 | * Check if the local client has any remote commands and perform them. |
|
1511 | * |
|
1512 | * @return False to abort sync |
|
1513 | */ |
|
4 | 1514 | processCommands: function WeaveSvc_processCommands() |
1515 | this._notify("process-commands", "", function() { |
|
1516 | // Immediately clear out the commands as we've got them locally |
|
1517 | let commands = Clients.localCommands; |
|
1518 | Clients.clearCommands(); |
|
1519 | ||
1520 | // Process each command in order |
|
1521 | for each ({command: command, args: args} in commands) { |
|
1522 | this._log.debug("Processing command: " + command + "(" + args + ")"); |
|
1523 | ||
1524 | let engines = [args[0]]; |
|
1525 | switch (command) { |
|
1526 | case "resetAll": |
|
1527 | engines = null; |
|
1528 | // Fallthrough |
|
1529 | case "resetEngine": |
|
1530 | this.resetClient(engines); |
|
1531 | break; |
|
1532 | case "wipeAll": |
|
1533 | engines = null; |
|
1534 | // Fallthrough |
|
1535 | case "wipeEngine": |
|
1536 | this.wipeClient(engines); |
|
1537 | break; |
|
1538 | case "logout": |
|
1539 | this.logout(); |
|
1540 | return false; |
|
1541 | default: |
|
1542 | this._log.debug("Received an unknown command: " + command); |
|
1543 | break; |
|
1544 | } |
|
1545 | } |
|
1546 | ||
1547 | return true; |
|
1548 | })(), |
|
1549 | ||
1550 | /** |
|
1551 | * Prepare to send a command to each remote client. Calling this doesn't |
|
1552 | * actually sync the command data to the server. If the client already has |
|
1553 | * the command/args pair, it won't get a duplicate action. |
|
1554 | * |
|
1555 | * @param command |
|
1556 | * Command to invoke on remote clients |
|
1557 | * @param args |
|
1558 | * Array of arguments to give to the command |
|
1559 | */ |
|
10 | 1560 | prepCommand: function WeaveSvc_prepCommand(command, args) { |
1561 | let commandData = this._commands[command]; |
|
1562 | // Don't send commands that we don't know about |
|
1563 | if (commandData == null) { |
|
1564 | this._log.error("Unknown command to send: " + command); |
|
1565 | return; |
|
1566 | } |
|
1567 | // Don't send a command with the wrong number of arguments |
|
1568 | else if (args == null || args.length != commandData.args) { |
|
1569 | this._log.error("Expected " + commandData.args + " args for '" + |
|
1570 | command + "', but got " + args); |
|
1571 | return; |
|
1572 | } |
|
1573 | ||
1574 | // Send the command to all remote clients |
|
1575 | this._log.debug("Sending clients: " + [command, args, commandData.desc]); |
|
1576 | Clients.sendCommand(command, args); |
|
1577 | }, |
|
1578 | }; |
|
1579 | ||
1580 | // Load Weave on the first time this file is loaded |
|
12 | 1581 | Weave.Service.onStartup(); |