dom/push/src/PushService.jsm

branch
TOR_BUG_9701
changeset 9
a63d609f5ebe
equal deleted inserted replaced
-1:000000000000 0:4b9f7f6f530c
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 }

mercurial