dom/push/src/PushService.jsm

Thu, 15 Jan 2015 15:55:04 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:55:04 +0100
branch
TOR_BUG_9701
changeset 9
a63d609f5ebe
permissions
-rw-r--r--

Back out 97036ab72558 which inappropriately compared turds to third parties.

     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/. */
     5 "use strict";
     7 // Don't modify this, instead set services.push.debug.
     8 let gDebuggingEnabled = false;
    10 function debug(s) {
    11   if (gDebuggingEnabled)
    12     dump("-*- PushService.jsm: " + s + "\n");
    13 }
    15 const Cc = Components.classes;
    16 const Ci = Components.interfaces;
    17 const Cu = Components.utils;
    18 const Cr = Components.results;
    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"]);
    28 XPCOMUtils.defineLazyModuleGetter(this, "AlarmService",
    29                                   "resource://gre/modules/AlarmService.jsm");
    31 this.EXPORTED_SYMBOLS = ["PushService"];
    33 const prefs = new Preferences("services.push.");
    34 // Set debug first so that all debugging actually works.
    35 gDebuggingEnabled = prefs.get("debug");
    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";
    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.
    45 const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister",
    46                                  "Push:Registrations"];
    48 // This is a singleton
    49 this.PushDB = function PushDB() {
    50   debug("PushDB()");
    52   // set the indexeddb database
    53   this.initDBHelper(kPUSHDB_DB_NAME, kPUSHDB_DB_VERSION,
    54                     [kPUSHDB_STORE_NAME]);
    55 };
    57 this.PushDB.prototype = {
    58   __proto__: IndexedDBHelper.prototype,
    60   upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
    61     debug("PushDB.upgradeSchema()")
    63     let objectStore = aDb.createObjectStore(kPUSHDB_STORE_NAME,
    64                                             { keyPath: "channelID" });
    66     // index to fetch records based on endpoints. used by unregister
    67     objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
    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   },
    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()");
    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   },
   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()");
   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   },
   124   getByPushEndpoint: function(aPushEndpoint, aSuccessCb, aErrorCb) {
   125     debug("getByPushEndpoint()");
   127     this.newTxn(
   128       "readonly",
   129       kPUSHDB_STORE_NAME,
   130       function txnCb(aTxn, aStore) {
   131         aTxn.result = undefined;
   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   },
   144   getByChannelID: function(aChannelID, aSuccessCb, aErrorCb) {
   145     debug("getByChannelID()");
   147     this.newTxn(
   148       "readonly",
   149       kPUSHDB_STORE_NAME,
   150       function txnCb(aTxn, aStore) {
   151         aTxn.result = undefined;
   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   },
   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     }
   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   },
   194   getAllChannelIDs: function(aSuccessCb, aErrorCb) {
   195     debug("getAllChannelIDs()");
   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   },
   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 };
   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 }
   236 this.PushWebSocketListener.prototype = {
   237   onStart: function(context) {
   238     if (!this._pushService)
   239         return;
   240     this._pushService._wsOnStart(context);
   241   },
   243   onStop: function(context, statusCode) {
   244     if (!this._pushService)
   245         return;
   246     this._pushService._wsOnStop(context, statusCode);
   247   },
   249   onAcknowledge: function(context, size) {
   250     // EMPTY
   251   },
   253   onBinaryMessageAvailable: function(context, message) {
   254     // EMPTY
   255   },
   257   onMessageAvailable: function(context, message) {
   258     if (!this._pushService)
   259         return;
   260     this._pushService._wsOnMessageAvailable(context, message);
   261   },
   263   onServerClose: function(context, aStatusCode, aReason) {
   264     if (!this._pushService)
   265         return;
   266     this._pushService._wsOnServerClose(context, aStatusCode, aReason);
   267   }
   268 }
   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;
   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         }
   306         this._shutdownWS();
   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           }
   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;
   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"});
   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           }
   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");
   366         let data = aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
   367         if (!data) {
   368           debug("webapps-clear-data: Failed to get information about application");
   369           return;
   370         }
   372         // Only remove push registrations for apps.
   373         if (data.browserOnly) {
   374           return;
   375         }
   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         }
   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         });
   403         break;
   404     }
   405   },
   407   get _UAID() {
   408     return prefs.get("userAgentID");
   409   },
   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   },
   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,
   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,
   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   },
   456   init: function() {
   457     debug("init()");
   458     if (!prefs.get("enabled"))
   459         return null;
   461     this._db = new PushDB();
   463     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
   464                  .getService(Ci.nsIMessageBroadcaster);
   466     kCHILD_PROCESS_MESSAGES.forEach(function addMessage(msgName) {
   467         ppmm.addMessageListener(msgName, this);
   468     }.bind(this));
   470     this._alarmID = null;
   472     this._requestTimeout = prefs.get("requestTimeout");
   474     this._startListeningIfChannelsPresent();
   476     Services.obs.addObserver(this, "xpcom-shutdown", false);
   477     Services.obs.addObserver(this, "webapps-clear-data", false);
   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);
   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);
   507     this._started = true;
   508   },
   510   _shutdownWS: function() {
   511     debug("shutdownWS()");
   512     this._currentState = STATE_SHUT_DOWN;
   513     this._willBeWokenUpByUDP = false;
   515     if (this._wsListener)
   516       this._wsListener._pushService = null;
   517     try {
   518         this._ws.close(0, null);
   519     } catch (e) {}
   520     this._ws = null;
   522     this._waitingForPong = false;
   523     this._stopAlarm();
   524   },
   526   uninit: function() {
   527     if (!this._started)
   528       return;
   530     debug("uninit()");
   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);
   539     if (this._db) {
   540       this._db.close();
   541       this._db = null;
   542     }
   544     if (this._udpServer) {
   545       this._udpServer.close();
   546       this._udpServer = null;
   547     }
   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();
   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();
   559     if (this._requestTimeoutTimer) {
   560       this._requestTimeoutTimer.cancel();
   561     }
   563     debug("shutdown complete!");
   564   },
   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()");
   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"));
   591     this._retryFailCount++;
   593     debug("Retry in " + retryTimeout + " Try number " + this._retryFailCount);
   594     this._setAlarm(retryTimeout);
   595   },
   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     }
   605     if (!prefs.get("connection.enabled")) {
   606       debug("_beginWSSetup: connection.enabled is not set to true. Aborting.");
   607       return;
   608     }
   610     // Stop any pending reconnects scheduled for the near future.
   611     this._stopAlarm();
   613     if (Services.io.offline) {
   614       debug("Network is offline.");
   615       return;
   616     }
   618     let serverURL = prefs.get("serverURL");
   619     if (!serverURL) {
   620       debug("No services.push.serverURL found!");
   621       return;
   622     }
   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     }
   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     }
   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   },
   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   },
   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     }
   675     // Stop any existing alarm.
   676     this._stopAlarm();
   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;
   690         if (this._waitingForAlarmSet) {
   691           this._waitingForAlarmSet = false;
   692           this._setAlarm(this._queuedAlarmDelay);
   693         }
   694       }.bind(this)
   695     )
   696   },
   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   },
   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       }
   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.
   769       // Websocket is shut down. Backoff interval expired, try to connect.
   770       this._beginWSSetup();
   771     }
   772   },
   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     }
   786     if (typeof reply.uaid !== "string") {
   787       debug("No UAID received or non string UAID received");
   788       this._shutdownWS();
   789       return;
   790     }
   792     if (reply.uaid === "") {
   793       debug("Empty UAID received!");
   794       this._shutdownWS();
   795       return;
   796     }
   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     }
   806     function finishHandshake() {
   807       this._UAID = reply.uaid;
   808       this._currentState = STATE_READY;
   809       this._processNextRequestInQueue();
   810     }
   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");
   820       this._notifyAllAppsRegister()
   821           .then(this._dropRegistrations.bind(this))
   822           .then(finishHandshake.bind(this));
   824       return;
   825     }
   827     // otherwise we are good to go
   828     finishHandshake.bind(this)();
   829   },
   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;
   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();
   846     if (reply.status == 200) {
   847       tmp.deferred.resolve(reply);
   848     } else {
   849       tmp.deferred.reject(reply);
   850     }
   851   },
   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     }
   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       }
   872       if (update.version === undefined) {
   873         debug("update.version does not exist");
   874         continue;
   875       }
   877       let version = update.version;
   879       if (typeof version === "string") {
   880         version = parseInt(version, 10);
   881       }
   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   },
   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   },
   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     }
   910     let deferred = Promise.defer();
   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     }
   922     this._pendingRequests[data.channelID] = { deferred: deferred,
   923                                               ctime: Date.now() };
   925     this._send(action, data);
   926     return deferred.promise;
   927   },
   929   _send: function(action, data) {
   930     debug("send()");
   931     this._requestQueue.push([action, data]);
   932     debug("Queued " + action);
   933     this._processNextRequestInQueue();
   934   },
   936   _processNextRequestInQueue: function() {
   937     debug("_processNextRequestInQueue()");
   939     if (this._requestQueue.length == 0) {
   940       debug("Request queue empty");
   941       return;
   942     }
   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     }
   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     }
   967     this._wsSendMessage(data);
   968     // Process the next one as soon as possible.
   969     setTimeout(this._processNextRequestInQueue.bind(this), 0);
   970   },
   972   _receivedUpdate: function(aChannelID, aLatestVersion) {
   973     debug("Updating: " + aChannelID + " -> " + aLatestVersion);
   975     let compareRecordVersionAndNotify = function(aPushRecord) {
   976       debug("compareRecordVersionAndNotify()");
   977       if (!aPushRecord) {
   978         debug("No record for channel ID " + aChannelID);
   979         return;
   980       }
   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     }
  1000     let recoverNoSuchChannelID = function(aChannelIDFromServer) {
  1001       debug("Could not get channelID " + aChannelIDFromServer + " from DB");
  1004     this._db.getByChannelID(aChannelID,
  1005                             compareRecordVersionAndNotify.bind(this),
  1006                             recoverNoSuchChannelID.bind(this));
  1007   },
  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();
  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] = [];
  1025         wakeupTable[record.manifestURL].push(record.pageURL);
  1028       let messenger = Cc["@mozilla.org/system-message-internal;1"]
  1029                         .getService(Ci.nsISystemMessagesInternal);
  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         });
  1039       deferred.resolve();
  1042     this._db.getAllChannelIDs(wakeupRegisteredApps, deferred.reject);
  1044     return deferred.promise;
  1045   },
  1047   _notifyApp: function(aPushRecord) {
  1048     if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.manifestURL) {
  1049       debug("notifyApp() something is undefined.  Dropping notification");
  1050       return;
  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   },
  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   },
  1073   _dropRegistrations: function() {
  1074     let deferred = Promise.defer();
  1075     this._db.drop(deferred.resolve, deferred.reject);
  1076     return deferred.promise;
  1077   },
  1079   receiveMessage: function(aMessage) {
  1080     debug("receiveMessage(): " + aMessage.name);
  1082     if (kCHILD_PROCESS_MESSAGES.indexOf(aMessage.name) == -1) {
  1083       debug("Invalid message from child " + aMessage.name);
  1084       return;
  1087     let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
  1088     let json = aMessage.data;
  1089     this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
  1090   },
  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()");
  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);
  1104     this._sendRequest("register", {channelID: channelID})
  1105       .then(
  1106         this._onRegisterSuccess.bind(this, aPageRecord, channelID),
  1107         this._onRegisterError.bind(this, aPageRecord, aMessageManager)
  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   },
  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 };
  1127     if (typeof data.channelID !== "string") {
  1128       debug("Invalid channelID " + message);
  1129       message["error"] = "Invalid channelID received";
  1130       throw message;
  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;
  1139     try {
  1140       Services.io.newURI(data.pushEndpoint, null, null);
  1142     catch (e) {
  1143       debug("Invalid pushEndpoint " + data.pushEndpoint);
  1144       message["error"] = "Invalid pushEndpoint " + data.pushEndpoint;
  1145       throw message;
  1148     let record = {
  1149       channelID: data.channelID,
  1150       pushEndpoint: data.pushEndpoint,
  1151       pageURL: aPageRecord.pageURL,
  1152       manifestURL: aPageRecord.manifestURL,
  1153       version: null
  1154     };
  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);
  1168       );
  1170     return deferred.promise;
  1171   },
  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!");
  1182     throw { requestID: aPageRecord.requestID, error: reply.error };
  1183   },
  1185   /**
  1186    * Called on message from the child process.
  1188    * Why is the record being deleted from the local database before the server
  1189    * is told?
  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.
  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()");
  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);
  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;
  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;
  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   },
  1246   /**
  1247    * Called on message from the child process
  1248    */
  1249   registrations: function(aPageRecord, aMessageManager) {
  1250     debug("registrations()");
  1252     if (aPageRecord.manifestURL) {
  1253       this._db.getAllByManifestURL(aPageRecord.manifestURL,
  1254         this._onRegistrationsSuccess.bind(this, aPageRecord, aMessageManager),
  1255         this._onRegistrationsError.bind(this, aPageRecord, aMessageManager));
  1257     else {
  1258       this._onRegistrationsError(aPageRecord, aMessageManager);
  1260   },
  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   },
  1279   _onRegistrationsError: function(aPageRecord, aMessageManager) {
  1280     aMessageManager.sendAsyncMessage("PushService:Registrations:KO", {
  1281       requestID: aPageRecord.requestID,
  1282       error: "Database error"
  1283     });
  1284   },
  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;
  1295     // Since we've had a successful connection reset the retry fail count.
  1296     this._retryFailCount = 0;
  1298     // Openning an available UDP port.
  1299     this._listenForUDPWakeup();
  1301     let data = {
  1302       messageType: "hello",
  1305     if (this._UAID)
  1306       data["uaid"] = this._UAID;
  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       };
  1316       data["mobilenetwork"] = {
  1317         mcc: networkState.mcc,
  1318         mnc: networkState.mnc
  1319       };
  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;
  1330     this._db.getAllChannelIDs(sendHelloMessage.bind(this),
  1331                               sendHelloMessage.bind(this));
  1332   },
  1334   /**
  1335    * This statusCode is not the websocket protocol status code, but the TCP
  1336    * connection close status code.
  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()");
  1344     if (statusCode != Cr.NS_OK &&
  1345         !(statusCode == Cr.NS_BASE_STREAM_CLOSED && this._willBeWokenUpByUDP)) {
  1346       debug("Socket error " + statusCode);
  1347       this._reconnectAfterBackoff();
  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   },
  1357   _wsOnMessageAvailable: function(context, message) {
  1358     debug("wsOnMessageAvailable() " + message);
  1360     this._waitingForPong = false;
  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"));
  1366     let reply = undefined;
  1367     try {
  1368       reply = JSON.parse(message);
  1369     } catch(e) {
  1370       debug("Parsing JSON failed. text : " + message);
  1371       return;
  1374     if (typeof reply.messageType != "string") {
  1375       debug("messageType not a string " + reply.messageType);
  1376       return;
  1379     // A whitelist of protocol handlers. Add to these if new messages are added
  1380     // in the protocol.
  1381     let handlers = ["Hello", "Register", "Notification"];
  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();
  1388     if (handlers.indexOf(handlerName) == -1) {
  1389       debug("No whitelisted handler " + handlerName + ". messageType: " +
  1390             reply.messageType);
  1391       return;
  1394     let handler = "_handle" + handlerName + "Reply";
  1396     if (typeof this[handler] !== "function") {
  1397       debug("Handler whitelisted but not implemented! " + handler);
  1398       return;
  1401     this[handler](reply);
  1402   },
  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.
  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);
  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
  1422   },
  1424   _listenForUDPWakeup: function() {
  1425     debug("listenForUDPWakeup()");
  1427     if (this._udpServer) {
  1428       debug("UDP Server already running");
  1429       return;
  1432     if (!this._getNetworkState().ip) {
  1433       debug("No IP");
  1434       return;
  1437     if (!prefs.get("udp.wakeupEnabled")) {
  1438       debug("UDP support disabled");
  1439       return;
  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);
  1448     return this._udpServer.port;
  1449   },
  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   },
  1460   /**
  1461    * Called by UDP Server Socket if the socket was closed for some reason.
  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   },
  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";
  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
  1504     } catch (e) {}
  1506     debug("Running on wifi");
  1508     return {
  1509       mcc: 0,
  1510       mnc: 0,
  1511       ip: undefined
  1512     };
  1513   },
  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";

mercurial