|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 // Don't modify this, instead set services.push.debug. |
|
8 let gDebuggingEnabled = false; |
|
9 |
|
10 function debug(s) { |
|
11 if (gDebuggingEnabled) |
|
12 dump("-*- PushService.jsm: " + s + "\n"); |
|
13 } |
|
14 |
|
15 const Cc = Components.classes; |
|
16 const Ci = Components.interfaces; |
|
17 const Cu = Components.utils; |
|
18 const Cr = Components.results; |
|
19 |
|
20 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
21 Cu.import("resource://gre/modules/Services.jsm"); |
|
22 Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); |
|
23 Cu.import("resource://gre/modules/Timer.jsm"); |
|
24 Cu.import("resource://gre/modules/Preferences.jsm"); |
|
25 Cu.import("resource://gre/modules/Promise.jsm"); |
|
26 Cu.importGlobalProperties(["indexedDB"]); |
|
27 |
|
28 XPCOMUtils.defineLazyModuleGetter(this, "AlarmService", |
|
29 "resource://gre/modules/AlarmService.jsm"); |
|
30 |
|
31 this.EXPORTED_SYMBOLS = ["PushService"]; |
|
32 |
|
33 const prefs = new Preferences("services.push."); |
|
34 // Set debug first so that all debugging actually works. |
|
35 gDebuggingEnabled = prefs.get("debug"); |
|
36 |
|
37 const kPUSHDB_DB_NAME = "push"; |
|
38 const kPUSHDB_DB_VERSION = 1; // Change this if the IndexedDB format changes |
|
39 const kPUSHDB_STORE_NAME = "push"; |
|
40 |
|
41 const kUDP_WAKEUP_WS_STATUS_CODE = 4774; // WebSocket Close status code sent |
|
42 // by server to signal that it can |
|
43 // wake client up using UDP. |
|
44 |
|
45 const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister", |
|
46 "Push:Registrations"]; |
|
47 |
|
48 // This is a singleton |
|
49 this.PushDB = function PushDB() { |
|
50 debug("PushDB()"); |
|
51 |
|
52 // set the indexeddb database |
|
53 this.initDBHelper(kPUSHDB_DB_NAME, kPUSHDB_DB_VERSION, |
|
54 [kPUSHDB_STORE_NAME]); |
|
55 }; |
|
56 |
|
57 this.PushDB.prototype = { |
|
58 __proto__: IndexedDBHelper.prototype, |
|
59 |
|
60 upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) { |
|
61 debug("PushDB.upgradeSchema()") |
|
62 |
|
63 let objectStore = aDb.createObjectStore(kPUSHDB_STORE_NAME, |
|
64 { keyPath: "channelID" }); |
|
65 |
|
66 // index to fetch records based on endpoints. used by unregister |
|
67 objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true }); |
|
68 |
|
69 // index to fetch records per manifest, so we can identify endpoints |
|
70 // associated with an app. Since an app can have multiple endpoints |
|
71 // uniqueness cannot be enforced |
|
72 objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); |
|
73 }, |
|
74 |
|
75 /* |
|
76 * @param aChannelRecord |
|
77 * The record to be added. |
|
78 * @param aSuccessCb |
|
79 * Callback function to invoke with result ID. |
|
80 * @param aErrorCb [optional] |
|
81 * Callback function to invoke when there was an error. |
|
82 */ |
|
83 put: function(aChannelRecord, aSuccessCb, aErrorCb) { |
|
84 debug("put()"); |
|
85 |
|
86 this.newTxn( |
|
87 "readwrite", |
|
88 kPUSHDB_STORE_NAME, |
|
89 function txnCb(aTxn, aStore) { |
|
90 debug("Going to put " + aChannelRecord.channelID); |
|
91 aStore.put(aChannelRecord).onsuccess = function setTxnResult(aEvent) { |
|
92 debug("Request successful. Updated record ID: " + |
|
93 aEvent.target.result); |
|
94 }; |
|
95 }, |
|
96 aSuccessCb, |
|
97 aErrorCb |
|
98 ); |
|
99 }, |
|
100 |
|
101 /* |
|
102 * @param aChannelID |
|
103 * The ID of record to be deleted. |
|
104 * @param aSuccessCb |
|
105 * Callback function to invoke with result. |
|
106 * @param aErrorCb [optional] |
|
107 * Callback function to invoke when there was an error. |
|
108 */ |
|
109 delete: function(aChannelID, aSuccessCb, aErrorCb) { |
|
110 debug("delete()"); |
|
111 |
|
112 this.newTxn( |
|
113 "readwrite", |
|
114 kPUSHDB_STORE_NAME, |
|
115 function txnCb(aTxn, aStore) { |
|
116 debug("Going to delete " + aChannelID); |
|
117 aStore.delete(aChannelID); |
|
118 }, |
|
119 aSuccessCb, |
|
120 aErrorCb |
|
121 ); |
|
122 }, |
|
123 |
|
124 getByPushEndpoint: function(aPushEndpoint, aSuccessCb, aErrorCb) { |
|
125 debug("getByPushEndpoint()"); |
|
126 |
|
127 this.newTxn( |
|
128 "readonly", |
|
129 kPUSHDB_STORE_NAME, |
|
130 function txnCb(aTxn, aStore) { |
|
131 aTxn.result = undefined; |
|
132 |
|
133 let index = aStore.index("pushEndpoint"); |
|
134 index.get(aPushEndpoint).onsuccess = function setTxnResult(aEvent) { |
|
135 aTxn.result = aEvent.target.result; |
|
136 debug("Fetch successful " + aEvent.target.result); |
|
137 } |
|
138 }, |
|
139 aSuccessCb, |
|
140 aErrorCb |
|
141 ); |
|
142 }, |
|
143 |
|
144 getByChannelID: function(aChannelID, aSuccessCb, aErrorCb) { |
|
145 debug("getByChannelID()"); |
|
146 |
|
147 this.newTxn( |
|
148 "readonly", |
|
149 kPUSHDB_STORE_NAME, |
|
150 function txnCb(aTxn, aStore) { |
|
151 aTxn.result = undefined; |
|
152 |
|
153 aStore.get(aChannelID).onsuccess = function setTxnResult(aEvent) { |
|
154 aTxn.result = aEvent.target.result; |
|
155 debug("Fetch successful " + aEvent.target.result); |
|
156 } |
|
157 }, |
|
158 aSuccessCb, |
|
159 aErrorCb |
|
160 ); |
|
161 }, |
|
162 |
|
163 getAllByManifestURL: function(aManifestURL, aSuccessCb, aErrorCb) { |
|
164 debug("getAllByManifestURL()"); |
|
165 if (!aManifestURL) { |
|
166 if (typeof aErrorCb == "function") { |
|
167 aErrorCb("PushDB.getAllByManifestURL: Got undefined aManifestURL"); |
|
168 } |
|
169 return; |
|
170 } |
|
171 |
|
172 let self = this; |
|
173 this.newTxn( |
|
174 "readonly", |
|
175 kPUSHDB_STORE_NAME, |
|
176 function txnCb(aTxn, aStore) { |
|
177 let index = aStore.index("manifestURL"); |
|
178 let range = IDBKeyRange.only(aManifestURL); |
|
179 aTxn.result = []; |
|
180 index.openCursor(range).onsuccess = function(event) { |
|
181 let cursor = event.target.result; |
|
182 if (cursor) { |
|
183 debug(cursor.value.manifestURL + " " + cursor.value.channelID); |
|
184 aTxn.result.push(cursor.value); |
|
185 cursor.continue(); |
|
186 } |
|
187 } |
|
188 }, |
|
189 aSuccessCb, |
|
190 aErrorCb |
|
191 ); |
|
192 }, |
|
193 |
|
194 getAllChannelIDs: function(aSuccessCb, aErrorCb) { |
|
195 debug("getAllChannelIDs()"); |
|
196 |
|
197 this.newTxn( |
|
198 "readonly", |
|
199 kPUSHDB_STORE_NAME, |
|
200 function txnCb(aTxn, aStore) { |
|
201 aStore.mozGetAll().onsuccess = function(event) { |
|
202 aTxn.result = event.target.result; |
|
203 } |
|
204 }, |
|
205 aSuccessCb, |
|
206 aErrorCb |
|
207 ); |
|
208 }, |
|
209 |
|
210 drop: function(aSuccessCb, aErrorCb) { |
|
211 debug("drop()"); |
|
212 this.newTxn( |
|
213 "readwrite", |
|
214 kPUSHDB_STORE_NAME, |
|
215 function txnCb(aTxn, aStore) { |
|
216 aStore.clear(); |
|
217 }, |
|
218 aSuccessCb(), |
|
219 aErrorCb() |
|
220 ); |
|
221 } |
|
222 }; |
|
223 |
|
224 /** |
|
225 * A proxy between the PushService and the WebSocket. The listener is used so |
|
226 * that the PushService can silence messages from the WebSocket by setting |
|
227 * PushWebSocketListener._pushService to null. This is required because |
|
228 * a WebSocket can continue to send messages or errors after it has been |
|
229 * closed but the PushService may not be interested in these. It's easier to |
|
230 * stop listening than to have checks at specific points. |
|
231 */ |
|
232 this.PushWebSocketListener = function(pushService) { |
|
233 this._pushService = pushService; |
|
234 } |
|
235 |
|
236 this.PushWebSocketListener.prototype = { |
|
237 onStart: function(context) { |
|
238 if (!this._pushService) |
|
239 return; |
|
240 this._pushService._wsOnStart(context); |
|
241 }, |
|
242 |
|
243 onStop: function(context, statusCode) { |
|
244 if (!this._pushService) |
|
245 return; |
|
246 this._pushService._wsOnStop(context, statusCode); |
|
247 }, |
|
248 |
|
249 onAcknowledge: function(context, size) { |
|
250 // EMPTY |
|
251 }, |
|
252 |
|
253 onBinaryMessageAvailable: function(context, message) { |
|
254 // EMPTY |
|
255 }, |
|
256 |
|
257 onMessageAvailable: function(context, message) { |
|
258 if (!this._pushService) |
|
259 return; |
|
260 this._pushService._wsOnMessageAvailable(context, message); |
|
261 }, |
|
262 |
|
263 onServerClose: function(context, aStatusCode, aReason) { |
|
264 if (!this._pushService) |
|
265 return; |
|
266 this._pushService._wsOnServerClose(context, aStatusCode, aReason); |
|
267 } |
|
268 } |
|
269 |
|
270 // websocket states |
|
271 // websocket is off |
|
272 const STATE_SHUT_DOWN = 0; |
|
273 // Websocket has been opened on client side, waiting for successful open. |
|
274 // (_wsOnStart) |
|
275 const STATE_WAITING_FOR_WS_START = 1; |
|
276 // Websocket opened, hello sent, waiting for server reply (_handleHelloReply). |
|
277 const STATE_WAITING_FOR_HELLO = 2; |
|
278 // Websocket operational, handshake completed, begin protocol messaging. |
|
279 const STATE_READY = 3; |
|
280 |
|
281 /** |
|
282 * The implementation of the SimplePush system. This runs in the B2G parent |
|
283 * process and is started on boot. It uses WebSockets to communicate with the |
|
284 * server and PushDB (IndexedDB) for persistence. |
|
285 */ |
|
286 this.PushService = { |
|
287 observe: function observe(aSubject, aTopic, aData) { |
|
288 switch (aTopic) { |
|
289 /* |
|
290 * We need to call uninit() on shutdown to clean up things that modules aren't very good |
|
291 * at automatically cleaning up, so we don't get shutdown leaks on browser shutdown. |
|
292 */ |
|
293 case "xpcom-shutdown": |
|
294 this.uninit(); |
|
295 case "network-active-changed": /* On B2G. */ |
|
296 case "network:offline-status-changed": /* On desktop. */ |
|
297 // In case of network-active-changed, always disconnect existing |
|
298 // connections. In case of offline-status changing from offline to |
|
299 // online, it is likely that these statements will be no-ops. |
|
300 if (this._udpServer) { |
|
301 this._udpServer.close(); |
|
302 // Set to null since this is checked in _listenForUDPWakeup() |
|
303 this._udpServer = null; |
|
304 } |
|
305 |
|
306 this._shutdownWS(); |
|
307 |
|
308 // Try to connect if network-active-changed or the offline-status |
|
309 // changed to online. |
|
310 if (aTopic === "network-active-changed" || aData === "online") { |
|
311 this._startListeningIfChannelsPresent(); |
|
312 } |
|
313 break; |
|
314 case "nsPref:changed": |
|
315 if (aData == "services.push.serverURL") { |
|
316 debug("services.push.serverURL changed! websocket. new value " + |
|
317 prefs.get("serverURL")); |
|
318 this._shutdownWS(); |
|
319 } else if (aData == "services.push.connection.enabled") { |
|
320 if (prefs.get("connection.enabled")) { |
|
321 this._startListeningIfChannelsPresent(); |
|
322 } else { |
|
323 this._shutdownWS(); |
|
324 } |
|
325 } else if (aData == "services.push.debug") { |
|
326 gDebuggingEnabled = prefs.get("debug"); |
|
327 } |
|
328 break; |
|
329 case "timer-callback": |
|
330 if (aSubject == this._requestTimeoutTimer) { |
|
331 if (Object.keys(this._pendingRequests).length == 0) { |
|
332 this._requestTimeoutTimer.cancel(); |
|
333 } |
|
334 |
|
335 // Set to true if at least one request timed out. |
|
336 let requestTimedOut = false; |
|
337 for (let channelID in this._pendingRequests) { |
|
338 let duration = Date.now() - this._pendingRequests[channelID].ctime; |
|
339 |
|
340 // If any of the registration requests time out, all the ones after it |
|
341 // also made to fail, since we are going to be disconnecting the socket. |
|
342 if (requestTimedOut || duration > this._requestTimeout) { |
|
343 debug("Request timeout: Removing " + channelID); |
|
344 requestTimedOut = true; |
|
345 this._pendingRequests[channelID] |
|
346 .deferred.reject({status: 0, error: "TimeoutError"}); |
|
347 |
|
348 delete this._pendingRequests[channelID]; |
|
349 for (let i = this._requestQueue.length - 1; i >= 0; --i) |
|
350 if (this._requestQueue[i].channelID == channelID) |
|
351 this._requestQueue.splice(i, 1); |
|
352 } |
|
353 } |
|
354 |
|
355 // The most likely reason for a registration request timing out is |
|
356 // that the socket has disconnected. Best to reconnect. |
|
357 if (requestTimedOut) { |
|
358 this._shutdownWS(); |
|
359 this._reconnectAfterBackoff(); |
|
360 } |
|
361 } |
|
362 break; |
|
363 case "webapps-clear-data": |
|
364 debug("webapps-clear-data"); |
|
365 |
|
366 let data = aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams); |
|
367 if (!data) { |
|
368 debug("webapps-clear-data: Failed to get information about application"); |
|
369 return; |
|
370 } |
|
371 |
|
372 // Only remove push registrations for apps. |
|
373 if (data.browserOnly) { |
|
374 return; |
|
375 } |
|
376 |
|
377 let appsService = Cc["@mozilla.org/AppsService;1"] |
|
378 .getService(Ci.nsIAppsService); |
|
379 let manifestURL = appsService.getManifestURLByLocalId(data.appId); |
|
380 if (!manifestURL) { |
|
381 debug("webapps-clear-data: No manifest URL found for " + data.appId); |
|
382 return; |
|
383 } |
|
384 |
|
385 this._db.getAllByManifestURL(manifestURL, function(records) { |
|
386 debug("Got " + records.length); |
|
387 for (let i = 0; i < records.length; i++) { |
|
388 this._db.delete(records[i].channelID, null, function() { |
|
389 debug("webapps-clear-data: " + manifestURL + |
|
390 " Could not delete entry " + records[i].channelID); |
|
391 }); |
|
392 // courtesy, but don't establish a connection |
|
393 // just for it |
|
394 if (this._ws) { |
|
395 debug("Had a connection, so telling the server"); |
|
396 this._send("unregister", {channelID: records[i].channelID}); |
|
397 } |
|
398 } |
|
399 }.bind(this), function() { |
|
400 debug("webapps-clear-data: Error in getAllByManifestURL(" + manifestURL + ")"); |
|
401 }); |
|
402 |
|
403 break; |
|
404 } |
|
405 }, |
|
406 |
|
407 get _UAID() { |
|
408 return prefs.get("userAgentID"); |
|
409 }, |
|
410 |
|
411 set _UAID(newID) { |
|
412 if (typeof(newID) !== "string") { |
|
413 debug("Got invalid, non-string UAID " + newID + |
|
414 ". Not updating userAgentID"); |
|
415 return; |
|
416 } |
|
417 debug("New _UAID: " + newID); |
|
418 prefs.set("userAgentID", newID); |
|
419 }, |
|
420 |
|
421 // keeps requests buffered if the websocket disconnects or is not connected |
|
422 _requestQueue: [], |
|
423 _ws: null, |
|
424 _pendingRequests: {}, |
|
425 _currentState: STATE_SHUT_DOWN, |
|
426 _requestTimeout: 0, |
|
427 _requestTimeoutTimer: null, |
|
428 _retryFailCount: 0, |
|
429 |
|
430 /** |
|
431 * According to the WS spec, servers should immediately close the underlying |
|
432 * TCP connection after they close a WebSocket. This causes wsOnStop to be |
|
433 * called with error NS_BASE_STREAM_CLOSED. Since the client has to keep the |
|
434 * WebSocket up, it should try to reconnect. But if the server closes the |
|
435 * WebSocket because it will wake up the client via UDP, then the client |
|
436 * shouldn't re-establish the connection. If the server says that it will |
|
437 * wake up the client over UDP, this is set to true in wsOnServerClose. It is |
|
438 * checked in wsOnStop. |
|
439 */ |
|
440 _willBeWokenUpByUDP: false, |
|
441 |
|
442 /** |
|
443 * Sends a message to the Push Server through an open websocket. |
|
444 * typeof(msg) shall be an object |
|
445 */ |
|
446 _wsSendMessage: function(msg) { |
|
447 if (!this._ws) { |
|
448 debug("No WebSocket initialized. Cannot send a message."); |
|
449 return; |
|
450 } |
|
451 msg = JSON.stringify(msg); |
|
452 debug("Sending message: " + msg); |
|
453 this._ws.sendMsg(msg); |
|
454 }, |
|
455 |
|
456 init: function() { |
|
457 debug("init()"); |
|
458 if (!prefs.get("enabled")) |
|
459 return null; |
|
460 |
|
461 this._db = new PushDB(); |
|
462 |
|
463 let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"] |
|
464 .getService(Ci.nsIMessageBroadcaster); |
|
465 |
|
466 kCHILD_PROCESS_MESSAGES.forEach(function addMessage(msgName) { |
|
467 ppmm.addMessageListener(msgName, this); |
|
468 }.bind(this)); |
|
469 |
|
470 this._alarmID = null; |
|
471 |
|
472 this._requestTimeout = prefs.get("requestTimeout"); |
|
473 |
|
474 this._startListeningIfChannelsPresent(); |
|
475 |
|
476 Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
477 Services.obs.addObserver(this, "webapps-clear-data", false); |
|
478 |
|
479 // On B2G the NetworkManager interface fires a network-active-changed |
|
480 // event. |
|
481 // |
|
482 // The "active network" is based on priority - i.e. Wi-Fi has higher |
|
483 // priority than data. The PushService should just use the preferred |
|
484 // network, and not care about all interface changes. |
|
485 // network-active-changed is not fired when the network goes offline, but |
|
486 // socket connections time out. The check for Services.io.offline in |
|
487 // _beginWSSetup() prevents unnecessary retries. When the network comes |
|
488 // back online, network-active-changed is fired. |
|
489 // |
|
490 // On non-B2G platforms, the offline-status-changed event is used to know |
|
491 // when to (dis)connect. It may not fire if the underlying OS changes |
|
492 // networks; in such a case we rely on timeout. |
|
493 // |
|
494 // On B2G both events fire, one after the other, when the network goes |
|
495 // online, so we explicitly check for the presence of NetworkManager and |
|
496 // don't add an observer for offline-status-changed on B2G. |
|
497 Services.obs.addObserver(this, this._getNetworkStateChangeEventName(), false); |
|
498 |
|
499 // This is only used for testing. Different tests require connecting to |
|
500 // slightly different URLs. |
|
501 prefs.observe("serverURL", this); |
|
502 // Used to monitor if the user wishes to disable Push. |
|
503 prefs.observe("connection.enabled", this); |
|
504 // Debugging |
|
505 prefs.observe("debug", this); |
|
506 |
|
507 this._started = true; |
|
508 }, |
|
509 |
|
510 _shutdownWS: function() { |
|
511 debug("shutdownWS()"); |
|
512 this._currentState = STATE_SHUT_DOWN; |
|
513 this._willBeWokenUpByUDP = false; |
|
514 |
|
515 if (this._wsListener) |
|
516 this._wsListener._pushService = null; |
|
517 try { |
|
518 this._ws.close(0, null); |
|
519 } catch (e) {} |
|
520 this._ws = null; |
|
521 |
|
522 this._waitingForPong = false; |
|
523 this._stopAlarm(); |
|
524 }, |
|
525 |
|
526 uninit: function() { |
|
527 if (!this._started) |
|
528 return; |
|
529 |
|
530 debug("uninit()"); |
|
531 |
|
532 prefs.ignore("debug", this); |
|
533 prefs.ignore("connection.enabled", this); |
|
534 prefs.ignore("serverURL", this); |
|
535 Services.obs.removeObserver(this, this._getNetworkStateChangeEventName()); |
|
536 Services.obs.removeObserver(this, "webapps-clear-data", false); |
|
537 Services.obs.removeObserver(this, "xpcom-shutdown", false); |
|
538 |
|
539 if (this._db) { |
|
540 this._db.close(); |
|
541 this._db = null; |
|
542 } |
|
543 |
|
544 if (this._udpServer) { |
|
545 this._udpServer.close(); |
|
546 this._udpServer = null; |
|
547 } |
|
548 |
|
549 // All pending requests (ideally none) are dropped at this point. We |
|
550 // shouldn't have any applications performing registration/unregistration |
|
551 // or receiving notifications. |
|
552 this._shutdownWS(); |
|
553 |
|
554 // At this point, profile-change-net-teardown has already fired, so the |
|
555 // WebSocket has been closed with NS_ERROR_ABORT (if it was up) and will |
|
556 // try to reconnect. Stop the timer. |
|
557 this._stopAlarm(); |
|
558 |
|
559 if (this._requestTimeoutTimer) { |
|
560 this._requestTimeoutTimer.cancel(); |
|
561 } |
|
562 |
|
563 debug("shutdown complete!"); |
|
564 }, |
|
565 |
|
566 /** |
|
567 * How retries work: The goal is to ensure websocket is always up on |
|
568 * networks not supporting UDP. So the websocket should only be shutdown if |
|
569 * onServerClose indicates UDP wakeup. If WS is closed due to socket error, |
|
570 * _reconnectAfterBackoff() is called. The retry alarm is started and when |
|
571 * it times out, beginWSSetup() is called again. |
|
572 * |
|
573 * On a successful connection, the alarm is cancelled in |
|
574 * wsOnMessageAvailable() when the ping alarm is started. |
|
575 * |
|
576 * If we are in the middle of a timeout (i.e. waiting), but |
|
577 * a register/unregister is called, we don't want to wait around anymore. |
|
578 * _sendRequest will automatically call beginWSSetup(), which will cancel the |
|
579 * timer. In addition since the state will have changed, even if a pending |
|
580 * timer event comes in (because the timer fired the event before it was |
|
581 * cancelled), so the connection won't be reset. |
|
582 */ |
|
583 _reconnectAfterBackoff: function() { |
|
584 debug("reconnectAfterBackoff()"); |
|
585 |
|
586 // Calculate new timeout, but cap it to pingInterval. |
|
587 let retryTimeout = prefs.get("retryBaseInterval") * |
|
588 Math.pow(2, this._retryFailCount); |
|
589 retryTimeout = Math.min(retryTimeout, prefs.get("pingInterval")); |
|
590 |
|
591 this._retryFailCount++; |
|
592 |
|
593 debug("Retry in " + retryTimeout + " Try number " + this._retryFailCount); |
|
594 this._setAlarm(retryTimeout); |
|
595 }, |
|
596 |
|
597 _beginWSSetup: function() { |
|
598 debug("beginWSSetup()"); |
|
599 if (this._currentState != STATE_SHUT_DOWN) { |
|
600 debug("_beginWSSetup: Not in shutdown state! Current state " + |
|
601 this._currentState); |
|
602 return; |
|
603 } |
|
604 |
|
605 if (!prefs.get("connection.enabled")) { |
|
606 debug("_beginWSSetup: connection.enabled is not set to true. Aborting."); |
|
607 return; |
|
608 } |
|
609 |
|
610 // Stop any pending reconnects scheduled for the near future. |
|
611 this._stopAlarm(); |
|
612 |
|
613 if (Services.io.offline) { |
|
614 debug("Network is offline."); |
|
615 return; |
|
616 } |
|
617 |
|
618 let serverURL = prefs.get("serverURL"); |
|
619 if (!serverURL) { |
|
620 debug("No services.push.serverURL found!"); |
|
621 return; |
|
622 } |
|
623 |
|
624 let uri; |
|
625 try { |
|
626 uri = Services.io.newURI(serverURL, null, null); |
|
627 } catch(e) { |
|
628 debug("Error creating valid URI from services.push.serverURL (" + |
|
629 serverURL + ")"); |
|
630 return; |
|
631 } |
|
632 |
|
633 if (uri.scheme === "wss") { |
|
634 this._ws = Cc["@mozilla.org/network/protocol;1?name=wss"] |
|
635 .createInstance(Ci.nsIWebSocketChannel); |
|
636 } |
|
637 else if (uri.scheme === "ws") { |
|
638 debug("Push over an insecure connection (ws://) is not allowed!"); |
|
639 return; |
|
640 } |
|
641 else { |
|
642 debug("Unsupported websocket scheme " + uri.scheme); |
|
643 return; |
|
644 } |
|
645 |
|
646 |
|
647 debug("serverURL: " + uri.spec); |
|
648 this._wsListener = new PushWebSocketListener(this); |
|
649 this._ws.protocol = "push-notification"; |
|
650 this._ws.asyncOpen(uri, serverURL, this._wsListener, null); |
|
651 this._currentState = STATE_WAITING_FOR_WS_START; |
|
652 }, |
|
653 |
|
654 _startListeningIfChannelsPresent: function() { |
|
655 // Check to see if we need to do anything. |
|
656 this._db.getAllChannelIDs(function(channelIDs) { |
|
657 if (channelIDs.length > 0) { |
|
658 this._beginWSSetup(); |
|
659 } |
|
660 }.bind(this)); |
|
661 }, |
|
662 |
|
663 /** |delay| should be in milliseconds. */ |
|
664 _setAlarm: function(delay) { |
|
665 // Bug 909270: Since calls to AlarmService.add() are async, calls must be |
|
666 // 'queued' to ensure only one alarm is ever active. |
|
667 if (this._settingAlarm) { |
|
668 // onSuccess will handle the set. Overwriting the variable enforces the |
|
669 // last-writer-wins semantics. |
|
670 this._queuedAlarmDelay = delay; |
|
671 this._waitingForAlarmSet = true; |
|
672 return; |
|
673 } |
|
674 |
|
675 // Stop any existing alarm. |
|
676 this._stopAlarm(); |
|
677 |
|
678 this._settingAlarm = true; |
|
679 AlarmService.add( |
|
680 { |
|
681 date: new Date(Date.now() + delay), |
|
682 ignoreTimezone: true |
|
683 }, |
|
684 this._onAlarmFired.bind(this), |
|
685 function onSuccess(alarmID) { |
|
686 this._alarmID = alarmID; |
|
687 debug("Set alarm " + delay + " in the future " + this._alarmID); |
|
688 this._settingAlarm = false; |
|
689 |
|
690 if (this._waitingForAlarmSet) { |
|
691 this._waitingForAlarmSet = false; |
|
692 this._setAlarm(this._queuedAlarmDelay); |
|
693 } |
|
694 }.bind(this) |
|
695 ) |
|
696 }, |
|
697 |
|
698 _stopAlarm: function() { |
|
699 if (this._alarmID !== null) { |
|
700 debug("Stopped existing alarm " + this._alarmID); |
|
701 AlarmService.remove(this._alarmID); |
|
702 this._alarmID = null; |
|
703 } |
|
704 }, |
|
705 |
|
706 /** |
|
707 * There is only one alarm active at any time. This alarm has 3 intervals |
|
708 * corresponding to 3 tasks. |
|
709 * |
|
710 * 1) Reconnect on ping timeout. |
|
711 * If we haven't received any messages from the server by the time this |
|
712 * alarm fires, the connection is closed and PushService tries to |
|
713 * reconnect, repurposing the alarm for (3). |
|
714 * |
|
715 * 2) Send a ping. |
|
716 * The protocol sends a ping ({}) on the wire every pingInterval ms. Once |
|
717 * it sends the ping, the alarm goes to task (1) which is waiting for |
|
718 * a pong. If data is received after the ping is sent, |
|
719 * _wsOnMessageAvailable() will reset the ping alarm (which cancels |
|
720 * waiting for the pong). So as long as the connection is fine, pong alarm |
|
721 * never fires. |
|
722 * |
|
723 * 3) Reconnect after backoff. |
|
724 * The alarm is set by _reconnectAfterBackoff() and increases in duration |
|
725 * every time we try and fail to connect. When it triggers, websocket |
|
726 * setup begins again. On successful socket setup, the socket starts |
|
727 * receiving messages. The alarm now goes to (2) where it monitors the |
|
728 * WebSocket by sending a ping. Since incoming data is a sign of the |
|
729 * connection being up, the ping alarm is reset every time data is |
|
730 * received. |
|
731 */ |
|
732 _onAlarmFired: function() { |
|
733 // Conditions are arranged in decreasing specificity. |
|
734 // i.e. when _waitingForPong is true, other conditions are also true. |
|
735 if (this._waitingForPong) { |
|
736 debug("Did not receive pong in time. Reconnecting WebSocket."); |
|
737 this._shutdownWS(); |
|
738 this._reconnectAfterBackoff(); |
|
739 } |
|
740 else if (this._currentState == STATE_READY) { |
|
741 // Send a ping. |
|
742 // Bypass the queue; we don't want this to be kept pending. |
|
743 // Watch out for exception in case the socket has disconnected. |
|
744 // When this happens, we pretend the ping was sent and don't specially |
|
745 // handle the exception, as the lack of a pong will lead to the socket |
|
746 // being reset. |
|
747 try { |
|
748 this._wsSendMessage({}); |
|
749 } catch (e) { |
|
750 } |
|
751 |
|
752 this._waitingForPong = true; |
|
753 this._setAlarm(prefs.get("requestTimeout")); |
|
754 } |
|
755 else if (this._alarmID !== null) { |
|
756 debug("reconnect alarm fired."); |
|
757 // Reconnect after back-off. |
|
758 // The check for a non-null _alarmID prevents a situation where the alarm |
|
759 // fires, but _shutdownWS() is called from another code-path (e.g. |
|
760 // network state change) and we don't want to reconnect. |
|
761 // |
|
762 // It also handles the case where _beginWSSetup() is called from another |
|
763 // code-path. |
|
764 // |
|
765 // alarmID will be non-null only when no shutdown/connect is |
|
766 // called between _reconnectAfterBackoff() setting the alarm and the |
|
767 // alarm firing. |
|
768 |
|
769 // Websocket is shut down. Backoff interval expired, try to connect. |
|
770 this._beginWSSetup(); |
|
771 } |
|
772 }, |
|
773 |
|
774 /** |
|
775 * Protocol handler invoked by server message. |
|
776 */ |
|
777 _handleHelloReply: function(reply) { |
|
778 debug("handleHelloReply()"); |
|
779 if (this._currentState != STATE_WAITING_FOR_HELLO) { |
|
780 debug("Unexpected state " + this._currentState + |
|
781 "(expected STATE_WAITING_FOR_HELLO)"); |
|
782 this._shutdownWS(); |
|
783 return; |
|
784 } |
|
785 |
|
786 if (typeof reply.uaid !== "string") { |
|
787 debug("No UAID received or non string UAID received"); |
|
788 this._shutdownWS(); |
|
789 return; |
|
790 } |
|
791 |
|
792 if (reply.uaid === "") { |
|
793 debug("Empty UAID received!"); |
|
794 this._shutdownWS(); |
|
795 return; |
|
796 } |
|
797 |
|
798 // To avoid sticking extra large values sent by an evil server into prefs. |
|
799 if (reply.uaid.length > 128) { |
|
800 debug("UAID received from server was too long: " + |
|
801 reply.uaid); |
|
802 this._shutdownWS(); |
|
803 return; |
|
804 } |
|
805 |
|
806 function finishHandshake() { |
|
807 this._UAID = reply.uaid; |
|
808 this._currentState = STATE_READY; |
|
809 this._processNextRequestInQueue(); |
|
810 } |
|
811 |
|
812 // By this point we've got a UAID from the server that we are ready to |
|
813 // accept. |
|
814 // |
|
815 // If we already had a valid UAID before, we have to ask apps to |
|
816 // re-register. |
|
817 if (this._UAID && this._UAID != reply.uaid) { |
|
818 debug("got new UAID: all re-register"); |
|
819 |
|
820 this._notifyAllAppsRegister() |
|
821 .then(this._dropRegistrations.bind(this)) |
|
822 .then(finishHandshake.bind(this)); |
|
823 |
|
824 return; |
|
825 } |
|
826 |
|
827 // otherwise we are good to go |
|
828 finishHandshake.bind(this)(); |
|
829 }, |
|
830 |
|
831 /** |
|
832 * Protocol handler invoked by server message. |
|
833 */ |
|
834 _handleRegisterReply: function(reply) { |
|
835 debug("handleRegisterReply()"); |
|
836 if (typeof reply.channelID !== "string" || |
|
837 typeof this._pendingRequests[reply.channelID] !== "object") |
|
838 return; |
|
839 |
|
840 let tmp = this._pendingRequests[reply.channelID]; |
|
841 delete this._pendingRequests[reply.channelID]; |
|
842 if (Object.keys(this._pendingRequests).length == 0 && |
|
843 this._requestTimeoutTimer) |
|
844 this._requestTimeoutTimer.cancel(); |
|
845 |
|
846 if (reply.status == 200) { |
|
847 tmp.deferred.resolve(reply); |
|
848 } else { |
|
849 tmp.deferred.reject(reply); |
|
850 } |
|
851 }, |
|
852 |
|
853 /** |
|
854 * Protocol handler invoked by server message. |
|
855 */ |
|
856 _handleNotificationReply: function(reply) { |
|
857 debug("handleNotificationReply()"); |
|
858 if (typeof reply.updates !== 'object') { |
|
859 debug("No 'updates' field in response. Type = " + typeof reply.updates); |
|
860 return; |
|
861 } |
|
862 |
|
863 debug("Reply updates: " + reply.updates.length); |
|
864 for (let i = 0; i < reply.updates.length; i++) { |
|
865 let update = reply.updates[i]; |
|
866 debug("Update: " + update.channelID + ": " + update.version); |
|
867 if (typeof update.channelID !== "string") { |
|
868 debug("Invalid update literal at index " + i); |
|
869 continue; |
|
870 } |
|
871 |
|
872 if (update.version === undefined) { |
|
873 debug("update.version does not exist"); |
|
874 continue; |
|
875 } |
|
876 |
|
877 let version = update.version; |
|
878 |
|
879 if (typeof version === "string") { |
|
880 version = parseInt(version, 10); |
|
881 } |
|
882 |
|
883 if (typeof version === "number" && version >= 0) { |
|
884 // FIXME(nsm): this relies on app update notification being infallible! |
|
885 // eventually fix this |
|
886 this._receivedUpdate(update.channelID, version); |
|
887 this._sendAck(update.channelID, version); |
|
888 } |
|
889 } |
|
890 }, |
|
891 |
|
892 // FIXME(nsm): batch acks for efficiency reasons. |
|
893 _sendAck: function(channelID, version) { |
|
894 debug("sendAck()"); |
|
895 this._send('ack', { |
|
896 updates: [{channelID: channelID, version: version}] |
|
897 }); |
|
898 }, |
|
899 |
|
900 /* |
|
901 * Must be used only by request/response style calls over the websocket. |
|
902 */ |
|
903 _sendRequest: function(action, data) { |
|
904 debug("sendRequest() " + action); |
|
905 if (typeof data.channelID !== "string") { |
|
906 debug("Received non-string channelID"); |
|
907 return Promise.reject("Received non-string channelID"); |
|
908 } |
|
909 |
|
910 let deferred = Promise.defer(); |
|
911 |
|
912 if (Object.keys(this._pendingRequests).length == 0) { |
|
913 // start the timer since we now have at least one request |
|
914 if (!this._requestTimeoutTimer) |
|
915 this._requestTimeoutTimer = Cc["@mozilla.org/timer;1"] |
|
916 .createInstance(Ci.nsITimer); |
|
917 this._requestTimeoutTimer.init(this, |
|
918 this._requestTimeout, |
|
919 Ci.nsITimer.TYPE_REPEATING_SLACK); |
|
920 } |
|
921 |
|
922 this._pendingRequests[data.channelID] = { deferred: deferred, |
|
923 ctime: Date.now() }; |
|
924 |
|
925 this._send(action, data); |
|
926 return deferred.promise; |
|
927 }, |
|
928 |
|
929 _send: function(action, data) { |
|
930 debug("send()"); |
|
931 this._requestQueue.push([action, data]); |
|
932 debug("Queued " + action); |
|
933 this._processNextRequestInQueue(); |
|
934 }, |
|
935 |
|
936 _processNextRequestInQueue: function() { |
|
937 debug("_processNextRequestInQueue()"); |
|
938 |
|
939 if (this._requestQueue.length == 0) { |
|
940 debug("Request queue empty"); |
|
941 return; |
|
942 } |
|
943 |
|
944 if (this._currentState != STATE_READY) { |
|
945 if (!this._ws) { |
|
946 // This will end up calling processNextRequestInQueue(). |
|
947 this._beginWSSetup(); |
|
948 } |
|
949 else { |
|
950 // We have a socket open so we are just waiting for hello to finish. |
|
951 // That will call processNextRequestInQueue(). |
|
952 } |
|
953 return; |
|
954 } |
|
955 |
|
956 let [action, data] = this._requestQueue.shift(); |
|
957 data.messageType = action; |
|
958 if (!this._ws) { |
|
959 // If our websocket is not ready and our state is STATE_READY we may as |
|
960 // well give up all assumptions about the world and start from scratch |
|
961 // again. Discard the message itself, let the timeout notify error to |
|
962 // the app. |
|
963 debug("This should never happen!"); |
|
964 this._shutdownWS(); |
|
965 } |
|
966 |
|
967 this._wsSendMessage(data); |
|
968 // Process the next one as soon as possible. |
|
969 setTimeout(this._processNextRequestInQueue.bind(this), 0); |
|
970 }, |
|
971 |
|
972 _receivedUpdate: function(aChannelID, aLatestVersion) { |
|
973 debug("Updating: " + aChannelID + " -> " + aLatestVersion); |
|
974 |
|
975 let compareRecordVersionAndNotify = function(aPushRecord) { |
|
976 debug("compareRecordVersionAndNotify()"); |
|
977 if (!aPushRecord) { |
|
978 debug("No record for channel ID " + aChannelID); |
|
979 return; |
|
980 } |
|
981 |
|
982 if (aPushRecord.version == null || |
|
983 aPushRecord.version < aLatestVersion) { |
|
984 debug("Version changed, notifying app and updating DB"); |
|
985 aPushRecord.version = aLatestVersion; |
|
986 this._notifyApp(aPushRecord); |
|
987 this._updatePushRecord(aPushRecord) |
|
988 .then( |
|
989 null, |
|
990 function(e) { |
|
991 debug("Error updating push record"); |
|
992 } |
|
993 ); |
|
994 } |
|
995 else { |
|
996 debug("No significant version change: " + aLatestVersion); |
|
997 } |
|
998 } |
|
999 |
|
1000 let recoverNoSuchChannelID = function(aChannelIDFromServer) { |
|
1001 debug("Could not get channelID " + aChannelIDFromServer + " from DB"); |
|
1002 } |
|
1003 |
|
1004 this._db.getByChannelID(aChannelID, |
|
1005 compareRecordVersionAndNotify.bind(this), |
|
1006 recoverNoSuchChannelID.bind(this)); |
|
1007 }, |
|
1008 |
|
1009 // Fires a push-register system message to all applications that have |
|
1010 // registrations. |
|
1011 _notifyAllAppsRegister: function() { |
|
1012 debug("notifyAllAppsRegister()"); |
|
1013 let deferred = Promise.defer(); |
|
1014 |
|
1015 // records are objects describing the registrations as stored in IndexedDB. |
|
1016 function wakeupRegisteredApps(records) { |
|
1017 // Pages to be notified. |
|
1018 // wakeupTable[manifestURL] -> [ pageURL ] |
|
1019 let wakeupTable = {}; |
|
1020 for (let i = 0; i < records.length; i++) { |
|
1021 let record = records[i]; |
|
1022 if (!(record.manifestURL in wakeupTable)) |
|
1023 wakeupTable[record.manifestURL] = []; |
|
1024 |
|
1025 wakeupTable[record.manifestURL].push(record.pageURL); |
|
1026 } |
|
1027 |
|
1028 let messenger = Cc["@mozilla.org/system-message-internal;1"] |
|
1029 .getService(Ci.nsISystemMessagesInternal); |
|
1030 |
|
1031 for (let manifestURL in wakeupTable) { |
|
1032 wakeupTable[manifestURL].forEach(function(pageURL) { |
|
1033 messenger.sendMessage('push-register', {}, |
|
1034 Services.io.newURI(pageURL, null, null), |
|
1035 Services.io.newURI(manifestURL, null, null)); |
|
1036 }); |
|
1037 } |
|
1038 |
|
1039 deferred.resolve(); |
|
1040 } |
|
1041 |
|
1042 this._db.getAllChannelIDs(wakeupRegisteredApps, deferred.reject); |
|
1043 |
|
1044 return deferred.promise; |
|
1045 }, |
|
1046 |
|
1047 _notifyApp: function(aPushRecord) { |
|
1048 if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.manifestURL) { |
|
1049 debug("notifyApp() something is undefined. Dropping notification"); |
|
1050 return; |
|
1051 } |
|
1052 |
|
1053 debug("notifyApp() " + aPushRecord.pageURL + |
|
1054 " " + aPushRecord.manifestURL); |
|
1055 let pageURI = Services.io.newURI(aPushRecord.pageURL, null, null); |
|
1056 let manifestURI = Services.io.newURI(aPushRecord.manifestURL, null, null); |
|
1057 let message = { |
|
1058 pushEndpoint: aPushRecord.pushEndpoint, |
|
1059 version: aPushRecord.version |
|
1060 }; |
|
1061 let messenger = Cc["@mozilla.org/system-message-internal;1"] |
|
1062 .getService(Ci.nsISystemMessagesInternal); |
|
1063 messenger.sendMessage('push', message, pageURI, manifestURI); |
|
1064 }, |
|
1065 |
|
1066 _updatePushRecord: function(aPushRecord) { |
|
1067 debug("updatePushRecord()"); |
|
1068 let deferred = Promise.defer(); |
|
1069 this._db.put(aPushRecord, deferred.resolve, deferred.reject); |
|
1070 return deferred.promise; |
|
1071 }, |
|
1072 |
|
1073 _dropRegistrations: function() { |
|
1074 let deferred = Promise.defer(); |
|
1075 this._db.drop(deferred.resolve, deferred.reject); |
|
1076 return deferred.promise; |
|
1077 }, |
|
1078 |
|
1079 receiveMessage: function(aMessage) { |
|
1080 debug("receiveMessage(): " + aMessage.name); |
|
1081 |
|
1082 if (kCHILD_PROCESS_MESSAGES.indexOf(aMessage.name) == -1) { |
|
1083 debug("Invalid message from child " + aMessage.name); |
|
1084 return; |
|
1085 } |
|
1086 |
|
1087 let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender); |
|
1088 let json = aMessage.data; |
|
1089 this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm); |
|
1090 }, |
|
1091 |
|
1092 /** |
|
1093 * Called on message from the child process. aPageRecord is an object sent by |
|
1094 * navigator.push, identifying the sending page and other fields. |
|
1095 */ |
|
1096 register: function(aPageRecord, aMessageManager) { |
|
1097 debug("register()"); |
|
1098 |
|
1099 let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] |
|
1100 .getService(Ci.nsIUUIDGenerator); |
|
1101 // generateUUID() gives a UUID surrounded by {...}, slice them off. |
|
1102 let channelID = uuidGenerator.generateUUID().toString().slice(1, -1); |
|
1103 |
|
1104 this._sendRequest("register", {channelID: channelID}) |
|
1105 .then( |
|
1106 this._onRegisterSuccess.bind(this, aPageRecord, channelID), |
|
1107 this._onRegisterError.bind(this, aPageRecord, aMessageManager) |
|
1108 ) |
|
1109 .then( |
|
1110 function(message) { |
|
1111 aMessageManager.sendAsyncMessage("PushService:Register:OK", message); |
|
1112 }, |
|
1113 function(message) { |
|
1114 aMessageManager.sendAsyncMessage("PushService:Register:KO", message); |
|
1115 }); |
|
1116 }, |
|
1117 |
|
1118 /** |
|
1119 * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained |
|
1120 * from _sendRequest, causing the promise to be rejected instead. |
|
1121 */ |
|
1122 _onRegisterSuccess: function(aPageRecord, generatedChannelID, data) { |
|
1123 debug("_onRegisterSuccess()"); |
|
1124 let deferred = Promise.defer(); |
|
1125 let message = { requestID: aPageRecord.requestID }; |
|
1126 |
|
1127 if (typeof data.channelID !== "string") { |
|
1128 debug("Invalid channelID " + message); |
|
1129 message["error"] = "Invalid channelID received"; |
|
1130 throw message; |
|
1131 } |
|
1132 else if (data.channelID != generatedChannelID) { |
|
1133 debug("Server replied with different channelID " + data.channelID + |
|
1134 " than what UA generated " + generatedChannelID); |
|
1135 message["error"] = "Server sent 200 status code but different channelID"; |
|
1136 throw message; |
|
1137 } |
|
1138 |
|
1139 try { |
|
1140 Services.io.newURI(data.pushEndpoint, null, null); |
|
1141 } |
|
1142 catch (e) { |
|
1143 debug("Invalid pushEndpoint " + data.pushEndpoint); |
|
1144 message["error"] = "Invalid pushEndpoint " + data.pushEndpoint; |
|
1145 throw message; |
|
1146 } |
|
1147 |
|
1148 let record = { |
|
1149 channelID: data.channelID, |
|
1150 pushEndpoint: data.pushEndpoint, |
|
1151 pageURL: aPageRecord.pageURL, |
|
1152 manifestURL: aPageRecord.manifestURL, |
|
1153 version: null |
|
1154 }; |
|
1155 |
|
1156 this._updatePushRecord(record) |
|
1157 .then( |
|
1158 function() { |
|
1159 message["pushEndpoint"] = data.pushEndpoint; |
|
1160 deferred.resolve(message); |
|
1161 }, |
|
1162 function(error) { |
|
1163 // Unable to save. |
|
1164 this._send("unregister", {channelID: record.channelID}); |
|
1165 message["error"] = error; |
|
1166 deferred.reject(message); |
|
1167 } |
|
1168 ); |
|
1169 |
|
1170 return deferred.promise; |
|
1171 }, |
|
1172 |
|
1173 /** |
|
1174 * Exceptions thrown in _onRegisterError are caught by the promise obtained |
|
1175 * from _sendRequest, causing the promise to be rejected instead. |
|
1176 */ |
|
1177 _onRegisterError: function(aPageRecord, aMessageManager, reply) { |
|
1178 debug("_onRegisterError()"); |
|
1179 if (!reply.error) { |
|
1180 debug("Called without valid error message!"); |
|
1181 } |
|
1182 throw { requestID: aPageRecord.requestID, error: reply.error }; |
|
1183 }, |
|
1184 |
|
1185 /** |
|
1186 * Called on message from the child process. |
|
1187 * |
|
1188 * Why is the record being deleted from the local database before the server |
|
1189 * is told? |
|
1190 * |
|
1191 * Unregistration is for the benefit of the app and the AppServer |
|
1192 * so that the AppServer does not keep pinging a channel the UserAgent isn't |
|
1193 * watching The important part of the transaction in this case is left to the |
|
1194 * app, to tell its server of the unregistration. Even if the request to the |
|
1195 * PushServer were to fail, it would not affect correctness of the protocol, |
|
1196 * and the server GC would just clean up the channelID eventually. Since the |
|
1197 * appserver doesn't ping it, no data is lost. |
|
1198 * |
|
1199 * If rather we were to unregister at the server and update the database only |
|
1200 * on success: If the server receives the unregister, and deletes the |
|
1201 * channelID, but the response is lost because of network failure, the |
|
1202 * application is never informed. In addition the application may retry the |
|
1203 * unregister when it fails due to timeout at which point the server will say |
|
1204 * it does not know of this unregistration. We'll have to make the |
|
1205 * registration/unregistration phases have retries and attempts to resend |
|
1206 * messages from the server, and have the client acknowledge. On a server, |
|
1207 * data is cheap, reliable notification is not. |
|
1208 */ |
|
1209 unregister: function(aPageRecord, aMessageManager) { |
|
1210 debug("unregister()"); |
|
1211 |
|
1212 let fail = function(error) { |
|
1213 debug("unregister() fail() error " + error); |
|
1214 let message = {requestID: aPageRecord.requestID, error: error}; |
|
1215 aMessageManager.sendAsyncMessage("PushService:Unregister:KO", message); |
|
1216 } |
|
1217 |
|
1218 this._db.getByPushEndpoint(aPageRecord.pushEndpoint, function(record) { |
|
1219 // If the endpoint didn't exist, let's just fail. |
|
1220 if (record === undefined) { |
|
1221 fail("NotFoundError"); |
|
1222 return; |
|
1223 } |
|
1224 |
|
1225 // Non-owner tried to unregister, say success, but don't do anything. |
|
1226 if (record.manifestURL !== aPageRecord.manifestURL) { |
|
1227 aMessageManager.sendAsyncMessage("PushService:Unregister:OK", { |
|
1228 requestID: aPageRecord.requestID, |
|
1229 pushEndpoint: aPageRecord.pushEndpoint |
|
1230 }); |
|
1231 return; |
|
1232 } |
|
1233 |
|
1234 this._db.delete(record.channelID, function() { |
|
1235 // Let's be nice to the server and try to inform it, but we don't care |
|
1236 // about the reply. |
|
1237 this._send("unregister", {channelID: record.channelID}); |
|
1238 aMessageManager.sendAsyncMessage("PushService:Unregister:OK", { |
|
1239 requestID: aPageRecord.requestID, |
|
1240 pushEndpoint: aPageRecord.pushEndpoint |
|
1241 }); |
|
1242 }.bind(this), fail); |
|
1243 }.bind(this), fail); |
|
1244 }, |
|
1245 |
|
1246 /** |
|
1247 * Called on message from the child process |
|
1248 */ |
|
1249 registrations: function(aPageRecord, aMessageManager) { |
|
1250 debug("registrations()"); |
|
1251 |
|
1252 if (aPageRecord.manifestURL) { |
|
1253 this._db.getAllByManifestURL(aPageRecord.manifestURL, |
|
1254 this._onRegistrationsSuccess.bind(this, aPageRecord, aMessageManager), |
|
1255 this._onRegistrationsError.bind(this, aPageRecord, aMessageManager)); |
|
1256 } |
|
1257 else { |
|
1258 this._onRegistrationsError(aPageRecord, aMessageManager); |
|
1259 } |
|
1260 }, |
|
1261 |
|
1262 _onRegistrationsSuccess: function(aPageRecord, |
|
1263 aMessageManager, |
|
1264 pushRecords) { |
|
1265 let registrations = []; |
|
1266 pushRecords.forEach(function(pushRecord) { |
|
1267 registrations.push({ |
|
1268 __exposedProps__: { pushEndpoint: 'r', version: 'r' }, |
|
1269 pushEndpoint: pushRecord.pushEndpoint, |
|
1270 version: pushRecord.version |
|
1271 }); |
|
1272 }); |
|
1273 aMessageManager.sendAsyncMessage("PushService:Registrations:OK", { |
|
1274 requestID: aPageRecord.requestID, |
|
1275 registrations: registrations |
|
1276 }); |
|
1277 }, |
|
1278 |
|
1279 _onRegistrationsError: function(aPageRecord, aMessageManager) { |
|
1280 aMessageManager.sendAsyncMessage("PushService:Registrations:KO", { |
|
1281 requestID: aPageRecord.requestID, |
|
1282 error: "Database error" |
|
1283 }); |
|
1284 }, |
|
1285 |
|
1286 // begin Push protocol handshake |
|
1287 _wsOnStart: function(context) { |
|
1288 debug("wsOnStart()"); |
|
1289 if (this._currentState != STATE_WAITING_FOR_WS_START) { |
|
1290 debug("NOT in STATE_WAITING_FOR_WS_START. Current state " + |
|
1291 this._currentState + ". Skipping"); |
|
1292 return; |
|
1293 } |
|
1294 |
|
1295 // Since we've had a successful connection reset the retry fail count. |
|
1296 this._retryFailCount = 0; |
|
1297 |
|
1298 // Openning an available UDP port. |
|
1299 this._listenForUDPWakeup(); |
|
1300 |
|
1301 let data = { |
|
1302 messageType: "hello", |
|
1303 } |
|
1304 |
|
1305 if (this._UAID) |
|
1306 data["uaid"] = this._UAID; |
|
1307 |
|
1308 let networkState = this._getNetworkState(); |
|
1309 if (networkState.ip) { |
|
1310 // Hostport is apparently a thing. |
|
1311 data["wakeup_hostport"] = { |
|
1312 ip: networkState.ip, |
|
1313 port: this._udpServer && this._udpServer.port |
|
1314 }; |
|
1315 |
|
1316 data["mobilenetwork"] = { |
|
1317 mcc: networkState.mcc, |
|
1318 mnc: networkState.mnc |
|
1319 }; |
|
1320 } |
|
1321 |
|
1322 function sendHelloMessage(ids) { |
|
1323 // On success, ids is an array, on error its not. |
|
1324 data["channelIDs"] = ids.map ? |
|
1325 ids.map(function(el) { return el.channelID; }) : []; |
|
1326 this._wsSendMessage(data); |
|
1327 this._currentState = STATE_WAITING_FOR_HELLO; |
|
1328 } |
|
1329 |
|
1330 this._db.getAllChannelIDs(sendHelloMessage.bind(this), |
|
1331 sendHelloMessage.bind(this)); |
|
1332 }, |
|
1333 |
|
1334 /** |
|
1335 * This statusCode is not the websocket protocol status code, but the TCP |
|
1336 * connection close status code. |
|
1337 * |
|
1338 * If we do not explicitly call ws.close() then statusCode is always |
|
1339 * NS_BASE_STREAM_CLOSED, even on a successful close. |
|
1340 */ |
|
1341 _wsOnStop: function(context, statusCode) { |
|
1342 debug("wsOnStop()"); |
|
1343 |
|
1344 if (statusCode != Cr.NS_OK && |
|
1345 !(statusCode == Cr.NS_BASE_STREAM_CLOSED && this._willBeWokenUpByUDP)) { |
|
1346 debug("Socket error " + statusCode); |
|
1347 this._reconnectAfterBackoff(); |
|
1348 } |
|
1349 |
|
1350 // Bug 896919. We always shutdown the WebSocket, even if we need to |
|
1351 // reconnect. This works because _reconnectAfterBackoff() is "async" |
|
1352 // (there is a minimum delay of the pref retryBaseInterval, which by default |
|
1353 // is 5000ms), so that function will open the WebSocket again. |
|
1354 this._shutdownWS(); |
|
1355 }, |
|
1356 |
|
1357 _wsOnMessageAvailable: function(context, message) { |
|
1358 debug("wsOnMessageAvailable() " + message); |
|
1359 |
|
1360 this._waitingForPong = false; |
|
1361 |
|
1362 // Reset the ping timer. Note: This path is executed at every step of the |
|
1363 // handshake, so this alarm does not need to be set explicitly at startup. |
|
1364 this._setAlarm(prefs.get("pingInterval")); |
|
1365 |
|
1366 let reply = undefined; |
|
1367 try { |
|
1368 reply = JSON.parse(message); |
|
1369 } catch(e) { |
|
1370 debug("Parsing JSON failed. text : " + message); |
|
1371 return; |
|
1372 } |
|
1373 |
|
1374 if (typeof reply.messageType != "string") { |
|
1375 debug("messageType not a string " + reply.messageType); |
|
1376 return; |
|
1377 } |
|
1378 |
|
1379 // A whitelist of protocol handlers. Add to these if new messages are added |
|
1380 // in the protocol. |
|
1381 let handlers = ["Hello", "Register", "Notification"]; |
|
1382 |
|
1383 // Build up the handler name to call from messageType. |
|
1384 // e.g. messageType == "register" -> _handleRegisterReply. |
|
1385 let handlerName = reply.messageType[0].toUpperCase() + |
|
1386 reply.messageType.slice(1).toLowerCase(); |
|
1387 |
|
1388 if (handlers.indexOf(handlerName) == -1) { |
|
1389 debug("No whitelisted handler " + handlerName + ". messageType: " + |
|
1390 reply.messageType); |
|
1391 return; |
|
1392 } |
|
1393 |
|
1394 let handler = "_handle" + handlerName + "Reply"; |
|
1395 |
|
1396 if (typeof this[handler] !== "function") { |
|
1397 debug("Handler whitelisted but not implemented! " + handler); |
|
1398 return; |
|
1399 } |
|
1400 |
|
1401 this[handler](reply); |
|
1402 }, |
|
1403 |
|
1404 /** |
|
1405 * The websocket should never be closed. Since we don't call ws.close(), |
|
1406 * _wsOnStop() receives error code NS_BASE_STREAM_CLOSED (see comment in that |
|
1407 * function), which calls reconnect and re-establishes the WebSocket |
|
1408 * connection. |
|
1409 * |
|
1410 * If the server said it'll use UDP for wakeup, we set _willBeWokenUpByUDP |
|
1411 * and stop reconnecting in _wsOnStop(). |
|
1412 */ |
|
1413 _wsOnServerClose: function(context, aStatusCode, aReason) { |
|
1414 debug("wsOnServerClose() " + aStatusCode + " " + aReason); |
|
1415 |
|
1416 // Switch over to UDP. |
|
1417 if (aStatusCode == kUDP_WAKEUP_WS_STATUS_CODE) { |
|
1418 debug("Server closed with promise to wake up"); |
|
1419 this._willBeWokenUpByUDP = true; |
|
1420 // TODO: there should be no pending requests |
|
1421 } |
|
1422 }, |
|
1423 |
|
1424 _listenForUDPWakeup: function() { |
|
1425 debug("listenForUDPWakeup()"); |
|
1426 |
|
1427 if (this._udpServer) { |
|
1428 debug("UDP Server already running"); |
|
1429 return; |
|
1430 } |
|
1431 |
|
1432 if (!this._getNetworkState().ip) { |
|
1433 debug("No IP"); |
|
1434 return; |
|
1435 } |
|
1436 |
|
1437 if (!prefs.get("udp.wakeupEnabled")) { |
|
1438 debug("UDP support disabled"); |
|
1439 return; |
|
1440 } |
|
1441 |
|
1442 this._udpServer = Cc["@mozilla.org/network/udp-socket;1"] |
|
1443 .createInstance(Ci.nsIUDPSocket); |
|
1444 this._udpServer.init(-1, false); |
|
1445 this._udpServer.asyncListen(this); |
|
1446 debug("listenForUDPWakeup listening on " + this._udpServer.port); |
|
1447 |
|
1448 return this._udpServer.port; |
|
1449 }, |
|
1450 |
|
1451 /** |
|
1452 * Called by UDP Server Socket. As soon as a ping is recieved via UDP, |
|
1453 * reconnect the WebSocket and get the actual data. |
|
1454 */ |
|
1455 onPacketReceived: function(aServ, aMessage) { |
|
1456 debug("Recv UDP datagram on port: " + this._udpServer.port); |
|
1457 this._beginWSSetup(); |
|
1458 }, |
|
1459 |
|
1460 /** |
|
1461 * Called by UDP Server Socket if the socket was closed for some reason. |
|
1462 * |
|
1463 * If this happens, we reconnect the WebSocket to not miss out on |
|
1464 * notifications. |
|
1465 */ |
|
1466 onStopListening: function(aServ, aStatus) { |
|
1467 debug("UDP Server socket was shutdown. Status: " + aStatus); |
|
1468 this._udpServer = undefined; |
|
1469 this._beginWSSetup(); |
|
1470 }, |
|
1471 |
|
1472 /** |
|
1473 * Get mobile network information to decide if the client is capable of being |
|
1474 * woken up by UDP (which currently just means having an mcc and mnc along |
|
1475 * with an IP). |
|
1476 */ |
|
1477 _getNetworkState: function() { |
|
1478 debug("getNetworkState()"); |
|
1479 try { |
|
1480 if (!prefs.get("udp.wakeupEnabled")) { |
|
1481 debug("UDP support disabled, we do not send any carrier info"); |
|
1482 throw "UDP disabled"; |
|
1483 } |
|
1484 |
|
1485 let nm = Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager); |
|
1486 if (nm.active && nm.active.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) { |
|
1487 let icc = Cc["@mozilla.org/ril/content-helper;1"].getService(Ci.nsIIccProvider); |
|
1488 // TODO: Bug 927721 - PushService for multi-sim |
|
1489 // In Multi-sim, there is more than one client in iccProvider. Each |
|
1490 // client represents a icc service. To maintain backward compatibility |
|
1491 // with single sim, we always use client 0 for now. Adding support |
|
1492 // for multiple sim will be addressed in bug 927721, if needed. |
|
1493 let clientId = 0; |
|
1494 let iccInfo = icc.getIccInfo(clientId); |
|
1495 if (iccInfo) { |
|
1496 debug("Running on mobile data"); |
|
1497 return { |
|
1498 mcc: iccInfo.mcc, |
|
1499 mnc: iccInfo.mnc, |
|
1500 ip: nm.active.ip |
|
1501 } |
|
1502 } |
|
1503 } |
|
1504 } catch (e) {} |
|
1505 |
|
1506 debug("Running on wifi"); |
|
1507 |
|
1508 return { |
|
1509 mcc: 0, |
|
1510 mnc: 0, |
|
1511 ip: undefined |
|
1512 }; |
|
1513 }, |
|
1514 |
|
1515 // utility function used to add/remove observers in init() and shutdown() |
|
1516 _getNetworkStateChangeEventName: function() { |
|
1517 try { |
|
1518 Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager); |
|
1519 return "network-active-changed"; |
|
1520 } catch (e) { |
|
1521 return "network:offline-status-changed"; |
|
1522 } |
|
1523 } |
|
1524 } |