dom/push/src/PushService.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/push/src/PushService.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1524 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +// Don't modify this, instead set services.push.debug.
    1.11 +let gDebuggingEnabled = false;
    1.12 +
    1.13 +function debug(s) {
    1.14 +  if (gDebuggingEnabled)
    1.15 +    dump("-*- PushService.jsm: " + s + "\n");
    1.16 +}
    1.17 +
    1.18 +const Cc = Components.classes;
    1.19 +const Ci = Components.interfaces;
    1.20 +const Cu = Components.utils;
    1.21 +const Cr = Components.results;
    1.22 +
    1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.24 +Cu.import("resource://gre/modules/Services.jsm");
    1.25 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
    1.26 +Cu.import("resource://gre/modules/Timer.jsm");
    1.27 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.28 +Cu.import("resource://gre/modules/Promise.jsm");
    1.29 +Cu.importGlobalProperties(["indexedDB"]);
    1.30 +
    1.31 +XPCOMUtils.defineLazyModuleGetter(this, "AlarmService",
    1.32 +                                  "resource://gre/modules/AlarmService.jsm");
    1.33 +
    1.34 +this.EXPORTED_SYMBOLS = ["PushService"];
    1.35 +
    1.36 +const prefs = new Preferences("services.push.");
    1.37 +// Set debug first so that all debugging actually works.
    1.38 +gDebuggingEnabled = prefs.get("debug");
    1.39 +
    1.40 +const kPUSHDB_DB_NAME = "push";
    1.41 +const kPUSHDB_DB_VERSION = 1; // Change this if the IndexedDB format changes
    1.42 +const kPUSHDB_STORE_NAME = "push";
    1.43 +
    1.44 +const kUDP_WAKEUP_WS_STATUS_CODE = 4774;  // WebSocket Close status code sent
    1.45 +                                          // by server to signal that it can
    1.46 +                                          // wake client up using UDP.
    1.47 +
    1.48 +const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister",
    1.49 +                                 "Push:Registrations"];
    1.50 +
    1.51 +// This is a singleton
    1.52 +this.PushDB = function PushDB() {
    1.53 +  debug("PushDB()");
    1.54 +
    1.55 +  // set the indexeddb database
    1.56 +  this.initDBHelper(kPUSHDB_DB_NAME, kPUSHDB_DB_VERSION,
    1.57 +                    [kPUSHDB_STORE_NAME]);
    1.58 +};
    1.59 +
    1.60 +this.PushDB.prototype = {
    1.61 +  __proto__: IndexedDBHelper.prototype,
    1.62 +
    1.63 +  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
    1.64 +    debug("PushDB.upgradeSchema()")
    1.65 +
    1.66 +    let objectStore = aDb.createObjectStore(kPUSHDB_STORE_NAME,
    1.67 +                                            { keyPath: "channelID" });
    1.68 +
    1.69 +    // index to fetch records based on endpoints. used by unregister
    1.70 +    objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
    1.71 +
    1.72 +    // index to fetch records per manifest, so we can identify endpoints
    1.73 +    // associated with an app. Since an app can have multiple endpoints
    1.74 +    // uniqueness cannot be enforced
    1.75 +    objectStore.createIndex("manifestURL", "manifestURL", { unique: false });
    1.76 +  },
    1.77 +
    1.78 +  /*
    1.79 +   * @param aChannelRecord
    1.80 +   *        The record to be added.
    1.81 +   * @param aSuccessCb
    1.82 +   *        Callback function to invoke with result ID.
    1.83 +   * @param aErrorCb [optional]
    1.84 +   *        Callback function to invoke when there was an error.
    1.85 +   */
    1.86 +  put: function(aChannelRecord, aSuccessCb, aErrorCb) {
    1.87 +    debug("put()");
    1.88 +
    1.89 +    this.newTxn(
    1.90 +      "readwrite",
    1.91 +      kPUSHDB_STORE_NAME,
    1.92 +      function txnCb(aTxn, aStore) {
    1.93 +        debug("Going to put " + aChannelRecord.channelID);
    1.94 +        aStore.put(aChannelRecord).onsuccess = function setTxnResult(aEvent) {
    1.95 +          debug("Request successful. Updated record ID: " +
    1.96 +                aEvent.target.result);
    1.97 +        };
    1.98 +      },
    1.99 +      aSuccessCb,
   1.100 +      aErrorCb
   1.101 +    );
   1.102 +  },
   1.103 +
   1.104 +  /*
   1.105 +   * @param aChannelID
   1.106 +   *        The ID of record to be deleted.
   1.107 +   * @param aSuccessCb
   1.108 +   *        Callback function to invoke with result.
   1.109 +   * @param aErrorCb [optional]
   1.110 +   *        Callback function to invoke when there was an error.
   1.111 +   */
   1.112 +  delete: function(aChannelID, aSuccessCb, aErrorCb) {
   1.113 +    debug("delete()");
   1.114 +
   1.115 +    this.newTxn(
   1.116 +      "readwrite",
   1.117 +      kPUSHDB_STORE_NAME,
   1.118 +      function txnCb(aTxn, aStore) {
   1.119 +        debug("Going to delete " + aChannelID);
   1.120 +        aStore.delete(aChannelID);
   1.121 +      },
   1.122 +      aSuccessCb,
   1.123 +      aErrorCb
   1.124 +    );
   1.125 +  },
   1.126 +
   1.127 +  getByPushEndpoint: function(aPushEndpoint, aSuccessCb, aErrorCb) {
   1.128 +    debug("getByPushEndpoint()");
   1.129 +
   1.130 +    this.newTxn(
   1.131 +      "readonly",
   1.132 +      kPUSHDB_STORE_NAME,
   1.133 +      function txnCb(aTxn, aStore) {
   1.134 +        aTxn.result = undefined;
   1.135 +
   1.136 +        let index = aStore.index("pushEndpoint");
   1.137 +        index.get(aPushEndpoint).onsuccess = function setTxnResult(aEvent) {
   1.138 +          aTxn.result = aEvent.target.result;
   1.139 +          debug("Fetch successful " + aEvent.target.result);
   1.140 +        }
   1.141 +      },
   1.142 +      aSuccessCb,
   1.143 +      aErrorCb
   1.144 +    );
   1.145 +  },
   1.146 +
   1.147 +  getByChannelID: function(aChannelID, aSuccessCb, aErrorCb) {
   1.148 +    debug("getByChannelID()");
   1.149 +
   1.150 +    this.newTxn(
   1.151 +      "readonly",
   1.152 +      kPUSHDB_STORE_NAME,
   1.153 +      function txnCb(aTxn, aStore) {
   1.154 +        aTxn.result = undefined;
   1.155 +
   1.156 +        aStore.get(aChannelID).onsuccess = function setTxnResult(aEvent) {
   1.157 +          aTxn.result = aEvent.target.result;
   1.158 +          debug("Fetch successful " + aEvent.target.result);
   1.159 +        }
   1.160 +      },
   1.161 +      aSuccessCb,
   1.162 +      aErrorCb
   1.163 +    );
   1.164 +  },
   1.165 +
   1.166 +  getAllByManifestURL: function(aManifestURL, aSuccessCb, aErrorCb) {
   1.167 +    debug("getAllByManifestURL()");
   1.168 +    if (!aManifestURL) {
   1.169 +      if (typeof aErrorCb == "function") {
   1.170 +        aErrorCb("PushDB.getAllByManifestURL: Got undefined aManifestURL");
   1.171 +      }
   1.172 +      return;
   1.173 +    }
   1.174 +
   1.175 +    let self = this;
   1.176 +    this.newTxn(
   1.177 +      "readonly",
   1.178 +      kPUSHDB_STORE_NAME,
   1.179 +      function txnCb(aTxn, aStore) {
   1.180 +        let index = aStore.index("manifestURL");
   1.181 +        let range = IDBKeyRange.only(aManifestURL);
   1.182 +        aTxn.result = [];
   1.183 +        index.openCursor(range).onsuccess = function(event) {
   1.184 +          let cursor = event.target.result;
   1.185 +          if (cursor) {
   1.186 +            debug(cursor.value.manifestURL + " " + cursor.value.channelID);
   1.187 +            aTxn.result.push(cursor.value);
   1.188 +            cursor.continue();
   1.189 +          }
   1.190 +        }
   1.191 +      },
   1.192 +      aSuccessCb,
   1.193 +      aErrorCb
   1.194 +    );
   1.195 +  },
   1.196 +
   1.197 +  getAllChannelIDs: function(aSuccessCb, aErrorCb) {
   1.198 +    debug("getAllChannelIDs()");
   1.199 +
   1.200 +    this.newTxn(
   1.201 +      "readonly",
   1.202 +      kPUSHDB_STORE_NAME,
   1.203 +      function txnCb(aTxn, aStore) {
   1.204 +        aStore.mozGetAll().onsuccess = function(event) {
   1.205 +          aTxn.result = event.target.result;
   1.206 +        }
   1.207 +      },
   1.208 +      aSuccessCb,
   1.209 +      aErrorCb
   1.210 +    );
   1.211 +  },
   1.212 +
   1.213 +  drop: function(aSuccessCb, aErrorCb) {
   1.214 +    debug("drop()");
   1.215 +    this.newTxn(
   1.216 +      "readwrite",
   1.217 +      kPUSHDB_STORE_NAME,
   1.218 +      function txnCb(aTxn, aStore) {
   1.219 +        aStore.clear();
   1.220 +      },
   1.221 +      aSuccessCb(),
   1.222 +      aErrorCb()
   1.223 +    );
   1.224 +  }
   1.225 +};
   1.226 +
   1.227 +/**
   1.228 + * A proxy between the PushService and the WebSocket. The listener is used so
   1.229 + * that the PushService can silence messages from the WebSocket by setting
   1.230 + * PushWebSocketListener._pushService to null. This is required because
   1.231 + * a WebSocket can continue to send messages or errors after it has been
   1.232 + * closed but the PushService may not be interested in these. It's easier to
   1.233 + * stop listening than to have checks at specific points.
   1.234 + */
   1.235 +this.PushWebSocketListener = function(pushService) {
   1.236 +  this._pushService = pushService;
   1.237 +}
   1.238 +
   1.239 +this.PushWebSocketListener.prototype = {
   1.240 +  onStart: function(context) {
   1.241 +    if (!this._pushService)
   1.242 +        return;
   1.243 +    this._pushService._wsOnStart(context);
   1.244 +  },
   1.245 +
   1.246 +  onStop: function(context, statusCode) {
   1.247 +    if (!this._pushService)
   1.248 +        return;
   1.249 +    this._pushService._wsOnStop(context, statusCode);
   1.250 +  },
   1.251 +
   1.252 +  onAcknowledge: function(context, size) {
   1.253 +    // EMPTY
   1.254 +  },
   1.255 +
   1.256 +  onBinaryMessageAvailable: function(context, message) {
   1.257 +    // EMPTY
   1.258 +  },
   1.259 +
   1.260 +  onMessageAvailable: function(context, message) {
   1.261 +    if (!this._pushService)
   1.262 +        return;
   1.263 +    this._pushService._wsOnMessageAvailable(context, message);
   1.264 +  },
   1.265 +
   1.266 +  onServerClose: function(context, aStatusCode, aReason) {
   1.267 +    if (!this._pushService)
   1.268 +        return;
   1.269 +    this._pushService._wsOnServerClose(context, aStatusCode, aReason);
   1.270 +  }
   1.271 +}
   1.272 +
   1.273 +// websocket states
   1.274 +// websocket is off
   1.275 +const STATE_SHUT_DOWN = 0;
   1.276 +// Websocket has been opened on client side, waiting for successful open.
   1.277 +// (_wsOnStart)
   1.278 +const STATE_WAITING_FOR_WS_START = 1;
   1.279 +// Websocket opened, hello sent, waiting for server reply (_handleHelloReply).
   1.280 +const STATE_WAITING_FOR_HELLO = 2;
   1.281 +// Websocket operational, handshake completed, begin protocol messaging.
   1.282 +const STATE_READY = 3;
   1.283 +
   1.284 +/**
   1.285 + * The implementation of the SimplePush system. This runs in the B2G parent
   1.286 + * process and is started on boot. It uses WebSockets to communicate with the
   1.287 + * server and PushDB (IndexedDB) for persistence.
   1.288 + */
   1.289 +this.PushService = {
   1.290 +  observe: function observe(aSubject, aTopic, aData) {
   1.291 +    switch (aTopic) {
   1.292 +      /*
   1.293 +       * We need to call uninit() on shutdown to clean up things that modules aren't very good
   1.294 +       * at automatically cleaning up, so we don't get shutdown leaks on browser shutdown.
   1.295 +       */
   1.296 +      case "xpcom-shutdown":
   1.297 +        this.uninit();
   1.298 +      case "network-active-changed":         /* On B2G. */
   1.299 +      case "network:offline-status-changed": /* On desktop. */
   1.300 +        // In case of network-active-changed, always disconnect existing
   1.301 +        // connections. In case of offline-status changing from offline to
   1.302 +        // online, it is likely that these statements will be no-ops.
   1.303 +        if (this._udpServer) {
   1.304 +          this._udpServer.close();
   1.305 +          // Set to null since this is checked in _listenForUDPWakeup()
   1.306 +          this._udpServer = null;
   1.307 +        }
   1.308 +
   1.309 +        this._shutdownWS();
   1.310 +
   1.311 +        // Try to connect if network-active-changed or the offline-status
   1.312 +        // changed to online.
   1.313 +        if (aTopic === "network-active-changed" || aData === "online") {
   1.314 +          this._startListeningIfChannelsPresent();
   1.315 +        }
   1.316 +        break;
   1.317 +      case "nsPref:changed":
   1.318 +        if (aData == "services.push.serverURL") {
   1.319 +          debug("services.push.serverURL changed! websocket. new value " +
   1.320 +                prefs.get("serverURL"));
   1.321 +          this._shutdownWS();
   1.322 +        } else if (aData == "services.push.connection.enabled") {
   1.323 +          if (prefs.get("connection.enabled")) {
   1.324 +            this._startListeningIfChannelsPresent();
   1.325 +          } else {
   1.326 +            this._shutdownWS();
   1.327 +          }
   1.328 +        } else if (aData == "services.push.debug") {
   1.329 +          gDebuggingEnabled = prefs.get("debug");
   1.330 +        }
   1.331 +        break;
   1.332 +      case "timer-callback":
   1.333 +        if (aSubject == this._requestTimeoutTimer) {
   1.334 +          if (Object.keys(this._pendingRequests).length == 0) {
   1.335 +            this._requestTimeoutTimer.cancel();
   1.336 +          }
   1.337 +
   1.338 +          // Set to true if at least one request timed out.
   1.339 +          let requestTimedOut = false;
   1.340 +          for (let channelID in this._pendingRequests) {
   1.341 +            let duration = Date.now() - this._pendingRequests[channelID].ctime;
   1.342 +
   1.343 +            // If any of the registration requests time out, all the ones after it
   1.344 +            // also made to fail, since we are going to be disconnecting the socket.
   1.345 +            if (requestTimedOut || duration > this._requestTimeout) {
   1.346 +              debug("Request timeout: Removing " + channelID);
   1.347 +              requestTimedOut = true;
   1.348 +              this._pendingRequests[channelID]
   1.349 +                .deferred.reject({status: 0, error: "TimeoutError"});
   1.350 +
   1.351 +              delete this._pendingRequests[channelID];
   1.352 +              for (let i = this._requestQueue.length - 1; i >= 0; --i)
   1.353 +                if (this._requestQueue[i].channelID == channelID)
   1.354 +                  this._requestQueue.splice(i, 1);
   1.355 +            }
   1.356 +          }
   1.357 +
   1.358 +          // The most likely reason for a registration request timing out is
   1.359 +          // that the socket has disconnected. Best to reconnect.
   1.360 +          if (requestTimedOut) {
   1.361 +            this._shutdownWS();
   1.362 +            this._reconnectAfterBackoff();
   1.363 +          }
   1.364 +        }
   1.365 +        break;
   1.366 +      case "webapps-clear-data":
   1.367 +        debug("webapps-clear-data");
   1.368 +
   1.369 +        let data = aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
   1.370 +        if (!data) {
   1.371 +          debug("webapps-clear-data: Failed to get information about application");
   1.372 +          return;
   1.373 +        }
   1.374 +
   1.375 +        // Only remove push registrations for apps.
   1.376 +        if (data.browserOnly) {
   1.377 +          return;
   1.378 +        }
   1.379 +
   1.380 +        let appsService = Cc["@mozilla.org/AppsService;1"]
   1.381 +                            .getService(Ci.nsIAppsService);
   1.382 +        let manifestURL = appsService.getManifestURLByLocalId(data.appId);
   1.383 +        if (!manifestURL) {
   1.384 +          debug("webapps-clear-data: No manifest URL found for " + data.appId);
   1.385 +          return;
   1.386 +        }
   1.387 +
   1.388 +        this._db.getAllByManifestURL(manifestURL, function(records) {
   1.389 +          debug("Got " + records.length);
   1.390 +          for (let i = 0; i < records.length; i++) {
   1.391 +            this._db.delete(records[i].channelID, null, function() {
   1.392 +              debug("webapps-clear-data: " + manifestURL +
   1.393 +                    " Could not delete entry " + records[i].channelID);
   1.394 +            });
   1.395 +            // courtesy, but don't establish a connection
   1.396 +            // just for it
   1.397 +            if (this._ws) {
   1.398 +              debug("Had a connection, so telling the server");
   1.399 +              this._send("unregister", {channelID: records[i].channelID});
   1.400 +            }
   1.401 +          }
   1.402 +        }.bind(this), function() {
   1.403 +          debug("webapps-clear-data: Error in getAllByManifestURL(" + manifestURL + ")");
   1.404 +        });
   1.405 +
   1.406 +        break;
   1.407 +    }
   1.408 +  },
   1.409 +
   1.410 +  get _UAID() {
   1.411 +    return prefs.get("userAgentID");
   1.412 +  },
   1.413 +
   1.414 +  set _UAID(newID) {
   1.415 +    if (typeof(newID) !== "string") {
   1.416 +      debug("Got invalid, non-string UAID " + newID +
   1.417 +            ". Not updating userAgentID");
   1.418 +      return;
   1.419 +    }
   1.420 +    debug("New _UAID: " + newID);
   1.421 +    prefs.set("userAgentID", newID);
   1.422 +  },
   1.423 +
   1.424 +  // keeps requests buffered if the websocket disconnects or is not connected
   1.425 +  _requestQueue: [],
   1.426 +  _ws: null,
   1.427 +  _pendingRequests: {},
   1.428 +  _currentState: STATE_SHUT_DOWN,
   1.429 +  _requestTimeout: 0,
   1.430 +  _requestTimeoutTimer: null,
   1.431 +  _retryFailCount: 0,
   1.432 +
   1.433 +  /**
   1.434 +   * According to the WS spec, servers should immediately close the underlying
   1.435 +   * TCP connection after they close a WebSocket. This causes wsOnStop to be
   1.436 +   * called with error NS_BASE_STREAM_CLOSED. Since the client has to keep the
   1.437 +   * WebSocket up, it should try to reconnect. But if the server closes the
   1.438 +   * WebSocket because it will wake up the client via UDP, then the client
   1.439 +   * shouldn't re-establish the connection. If the server says that it will
   1.440 +   * wake up the client over UDP, this is set to true in wsOnServerClose. It is
   1.441 +   * checked in wsOnStop.
   1.442 +   */
   1.443 +  _willBeWokenUpByUDP: false,
   1.444 +
   1.445 +  /**
   1.446 +   * Sends a message to the Push Server through an open websocket.
   1.447 +   * typeof(msg) shall be an object
   1.448 +   */
   1.449 +  _wsSendMessage: function(msg) {
   1.450 +    if (!this._ws) {
   1.451 +      debug("No WebSocket initialized. Cannot send a message.");
   1.452 +      return;
   1.453 +    }
   1.454 +    msg = JSON.stringify(msg);
   1.455 +    debug("Sending message: " + msg);
   1.456 +    this._ws.sendMsg(msg);
   1.457 +  },
   1.458 +
   1.459 +  init: function() {
   1.460 +    debug("init()");
   1.461 +    if (!prefs.get("enabled"))
   1.462 +        return null;
   1.463 +
   1.464 +    this._db = new PushDB();
   1.465 +
   1.466 +    let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
   1.467 +                 .getService(Ci.nsIMessageBroadcaster);
   1.468 +
   1.469 +    kCHILD_PROCESS_MESSAGES.forEach(function addMessage(msgName) {
   1.470 +        ppmm.addMessageListener(msgName, this);
   1.471 +    }.bind(this));
   1.472 +
   1.473 +    this._alarmID = null;
   1.474 +
   1.475 +    this._requestTimeout = prefs.get("requestTimeout");
   1.476 +
   1.477 +    this._startListeningIfChannelsPresent();
   1.478 +
   1.479 +    Services.obs.addObserver(this, "xpcom-shutdown", false);
   1.480 +    Services.obs.addObserver(this, "webapps-clear-data", false);
   1.481 +
   1.482 +    // On B2G the NetworkManager interface fires a network-active-changed
   1.483 +    // event.
   1.484 +    //
   1.485 +    // The "active network" is based on priority - i.e. Wi-Fi has higher
   1.486 +    // priority than data. The PushService should just use the preferred
   1.487 +    // network, and not care about all interface changes.
   1.488 +    // network-active-changed is not fired when the network goes offline, but
   1.489 +    // socket connections time out. The check for Services.io.offline in
   1.490 +    // _beginWSSetup() prevents unnecessary retries.  When the network comes
   1.491 +    // back online, network-active-changed is fired.
   1.492 +    //
   1.493 +    // On non-B2G platforms, the offline-status-changed event is used to know
   1.494 +    // when to (dis)connect. It may not fire if the underlying OS changes
   1.495 +    // networks; in such a case we rely on timeout.
   1.496 +    //
   1.497 +    // On B2G both events fire, one after the other, when the network goes
   1.498 +    // online, so we explicitly check for the presence of NetworkManager and
   1.499 +    // don't add an observer for offline-status-changed on B2G.
   1.500 +    Services.obs.addObserver(this, this._getNetworkStateChangeEventName(), false);
   1.501 +
   1.502 +    // This is only used for testing. Different tests require connecting to
   1.503 +    // slightly different URLs.
   1.504 +    prefs.observe("serverURL", this);
   1.505 +    // Used to monitor if the user wishes to disable Push.
   1.506 +    prefs.observe("connection.enabled", this);
   1.507 +    // Debugging
   1.508 +    prefs.observe("debug", this);
   1.509 +
   1.510 +    this._started = true;
   1.511 +  },
   1.512 +
   1.513 +  _shutdownWS: function() {
   1.514 +    debug("shutdownWS()");
   1.515 +    this._currentState = STATE_SHUT_DOWN;
   1.516 +    this._willBeWokenUpByUDP = false;
   1.517 +
   1.518 +    if (this._wsListener)
   1.519 +      this._wsListener._pushService = null;
   1.520 +    try {
   1.521 +        this._ws.close(0, null);
   1.522 +    } catch (e) {}
   1.523 +    this._ws = null;
   1.524 +
   1.525 +    this._waitingForPong = false;
   1.526 +    this._stopAlarm();
   1.527 +  },
   1.528 +
   1.529 +  uninit: function() {
   1.530 +    if (!this._started)
   1.531 +      return;
   1.532 +
   1.533 +    debug("uninit()");
   1.534 +
   1.535 +    prefs.ignore("debug", this);
   1.536 +    prefs.ignore("connection.enabled", this);
   1.537 +    prefs.ignore("serverURL", this);
   1.538 +    Services.obs.removeObserver(this, this._getNetworkStateChangeEventName());
   1.539 +    Services.obs.removeObserver(this, "webapps-clear-data", false);
   1.540 +    Services.obs.removeObserver(this, "xpcom-shutdown", false);
   1.541 +
   1.542 +    if (this._db) {
   1.543 +      this._db.close();
   1.544 +      this._db = null;
   1.545 +    }
   1.546 +
   1.547 +    if (this._udpServer) {
   1.548 +      this._udpServer.close();
   1.549 +      this._udpServer = null;
   1.550 +    }
   1.551 +
   1.552 +    // All pending requests (ideally none) are dropped at this point. We
   1.553 +    // shouldn't have any applications performing registration/unregistration
   1.554 +    // or receiving notifications.
   1.555 +    this._shutdownWS();
   1.556 +
   1.557 +    // At this point, profile-change-net-teardown has already fired, so the
   1.558 +    // WebSocket has been closed with NS_ERROR_ABORT (if it was up) and will
   1.559 +    // try to reconnect. Stop the timer.
   1.560 +    this._stopAlarm();
   1.561 +
   1.562 +    if (this._requestTimeoutTimer) {
   1.563 +      this._requestTimeoutTimer.cancel();
   1.564 +    }
   1.565 +
   1.566 +    debug("shutdown complete!");
   1.567 +  },
   1.568 +
   1.569 +  /**
   1.570 +   * How retries work:  The goal is to ensure websocket is always up on
   1.571 +   * networks not supporting UDP. So the websocket should only be shutdown if
   1.572 +   * onServerClose indicates UDP wakeup.  If WS is closed due to socket error,
   1.573 +   * _reconnectAfterBackoff() is called.  The retry alarm is started and when
   1.574 +   * it times out, beginWSSetup() is called again.
   1.575 +   *
   1.576 +   * On a successful connection, the alarm is cancelled in
   1.577 +   * wsOnMessageAvailable() when the ping alarm is started.
   1.578 +   *
   1.579 +   * If we are in the middle of a timeout (i.e. waiting), but
   1.580 +   * a register/unregister is called, we don't want to wait around anymore.
   1.581 +   * _sendRequest will automatically call beginWSSetup(), which will cancel the
   1.582 +   * timer. In addition since the state will have changed, even if a pending
   1.583 +   * timer event comes in (because the timer fired the event before it was
   1.584 +   * cancelled), so the connection won't be reset.
   1.585 +   */
   1.586 +  _reconnectAfterBackoff: function() {
   1.587 +    debug("reconnectAfterBackoff()");
   1.588 +
   1.589 +    // Calculate new timeout, but cap it to pingInterval.
   1.590 +    let retryTimeout = prefs.get("retryBaseInterval") *
   1.591 +                       Math.pow(2, this._retryFailCount);
   1.592 +    retryTimeout = Math.min(retryTimeout, prefs.get("pingInterval"));
   1.593 +
   1.594 +    this._retryFailCount++;
   1.595 +
   1.596 +    debug("Retry in " + retryTimeout + " Try number " + this._retryFailCount);
   1.597 +    this._setAlarm(retryTimeout);
   1.598 +  },
   1.599 +
   1.600 +  _beginWSSetup: function() {
   1.601 +    debug("beginWSSetup()");
   1.602 +    if (this._currentState != STATE_SHUT_DOWN) {
   1.603 +      debug("_beginWSSetup: Not in shutdown state! Current state " +
   1.604 +            this._currentState);
   1.605 +      return;
   1.606 +    }
   1.607 +
   1.608 +    if (!prefs.get("connection.enabled")) {
   1.609 +      debug("_beginWSSetup: connection.enabled is not set to true. Aborting.");
   1.610 +      return;
   1.611 +    }
   1.612 +
   1.613 +    // Stop any pending reconnects scheduled for the near future.
   1.614 +    this._stopAlarm();
   1.615 +
   1.616 +    if (Services.io.offline) {
   1.617 +      debug("Network is offline.");
   1.618 +      return;
   1.619 +    }
   1.620 +
   1.621 +    let serverURL = prefs.get("serverURL");
   1.622 +    if (!serverURL) {
   1.623 +      debug("No services.push.serverURL found!");
   1.624 +      return;
   1.625 +    }
   1.626 +
   1.627 +    let uri;
   1.628 +    try {
   1.629 +      uri = Services.io.newURI(serverURL, null, null);
   1.630 +    } catch(e) {
   1.631 +      debug("Error creating valid URI from services.push.serverURL (" +
   1.632 +            serverURL + ")");
   1.633 +      return;
   1.634 +    }
   1.635 +
   1.636 +    if (uri.scheme === "wss") {
   1.637 +      this._ws = Cc["@mozilla.org/network/protocol;1?name=wss"]
   1.638 +                   .createInstance(Ci.nsIWebSocketChannel);
   1.639 +    }
   1.640 +    else if (uri.scheme === "ws") {
   1.641 +      debug("Push over an insecure connection (ws://) is not allowed!");
   1.642 +      return;
   1.643 +    }
   1.644 +    else {
   1.645 +      debug("Unsupported websocket scheme " + uri.scheme);
   1.646 +      return;
   1.647 +    }
   1.648 +
   1.649 +
   1.650 +    debug("serverURL: " + uri.spec);
   1.651 +    this._wsListener = new PushWebSocketListener(this);
   1.652 +    this._ws.protocol = "push-notification";
   1.653 +    this._ws.asyncOpen(uri, serverURL, this._wsListener, null);
   1.654 +    this._currentState = STATE_WAITING_FOR_WS_START;
   1.655 +  },
   1.656 +
   1.657 +  _startListeningIfChannelsPresent: function() {
   1.658 +    // Check to see if we need to do anything.
   1.659 +    this._db.getAllChannelIDs(function(channelIDs) {
   1.660 +      if (channelIDs.length > 0) {
   1.661 +        this._beginWSSetup();
   1.662 +      }
   1.663 +    }.bind(this));
   1.664 +  },
   1.665 +
   1.666 +  /** |delay| should be in milliseconds. */
   1.667 +  _setAlarm: function(delay) {
   1.668 +    // Bug 909270: Since calls to AlarmService.add() are async, calls must be
   1.669 +    // 'queued' to ensure only one alarm is ever active.
   1.670 +    if (this._settingAlarm) {
   1.671 +        // onSuccess will handle the set. Overwriting the variable enforces the
   1.672 +        // last-writer-wins semantics.
   1.673 +        this._queuedAlarmDelay = delay;
   1.674 +        this._waitingForAlarmSet = true;
   1.675 +        return;
   1.676 +    }
   1.677 +
   1.678 +    // Stop any existing alarm.
   1.679 +    this._stopAlarm();
   1.680 +
   1.681 +    this._settingAlarm = true;
   1.682 +    AlarmService.add(
   1.683 +      {
   1.684 +        date: new Date(Date.now() + delay),
   1.685 +        ignoreTimezone: true
   1.686 +      },
   1.687 +      this._onAlarmFired.bind(this),
   1.688 +      function onSuccess(alarmID) {
   1.689 +        this._alarmID = alarmID;
   1.690 +        debug("Set alarm " + delay + " in the future " + this._alarmID);
   1.691 +        this._settingAlarm = false;
   1.692 +
   1.693 +        if (this._waitingForAlarmSet) {
   1.694 +          this._waitingForAlarmSet = false;
   1.695 +          this._setAlarm(this._queuedAlarmDelay);
   1.696 +        }
   1.697 +      }.bind(this)
   1.698 +    )
   1.699 +  },
   1.700 +
   1.701 +  _stopAlarm: function() {
   1.702 +    if (this._alarmID !== null) {
   1.703 +      debug("Stopped existing alarm " + this._alarmID);
   1.704 +      AlarmService.remove(this._alarmID);
   1.705 +      this._alarmID = null;
   1.706 +    }
   1.707 +  },
   1.708 +
   1.709 +  /**
   1.710 +   * There is only one alarm active at any time. This alarm has 3 intervals
   1.711 +   * corresponding to 3 tasks.
   1.712 +   *
   1.713 +   * 1) Reconnect on ping timeout.
   1.714 +   *    If we haven't received any messages from the server by the time this
   1.715 +   *    alarm fires, the connection is closed and PushService tries to
   1.716 +   *    reconnect, repurposing the alarm for (3).
   1.717 +   *
   1.718 +   * 2) Send a ping.
   1.719 +   *    The protocol sends a ping ({}) on the wire every pingInterval ms. Once
   1.720 +   *    it sends the ping, the alarm goes to task (1) which is waiting for
   1.721 +   *    a pong. If data is received after the ping is sent,
   1.722 +   *    _wsOnMessageAvailable() will reset the ping alarm (which cancels
   1.723 +   *    waiting for the pong). So as long as the connection is fine, pong alarm
   1.724 +   *    never fires.
   1.725 +   *
   1.726 +   * 3) Reconnect after backoff.
   1.727 +   *    The alarm is set by _reconnectAfterBackoff() and increases in duration
   1.728 +   *    every time we try and fail to connect.  When it triggers, websocket
   1.729 +   *    setup begins again. On successful socket setup, the socket starts
   1.730 +   *    receiving messages. The alarm now goes to (2) where it monitors the
   1.731 +   *    WebSocket by sending a ping.  Since incoming data is a sign of the
   1.732 +   *    connection being up, the ping alarm is reset every time data is
   1.733 +   *    received.
   1.734 +   */
   1.735 +  _onAlarmFired: function() {
   1.736 +    // Conditions are arranged in decreasing specificity.
   1.737 +    // i.e. when _waitingForPong is true, other conditions are also true.
   1.738 +    if (this._waitingForPong) {
   1.739 +      debug("Did not receive pong in time. Reconnecting WebSocket.");
   1.740 +      this._shutdownWS();
   1.741 +      this._reconnectAfterBackoff();
   1.742 +    }
   1.743 +    else if (this._currentState == STATE_READY) {
   1.744 +      // Send a ping.
   1.745 +      // Bypass the queue; we don't want this to be kept pending.
   1.746 +      // Watch out for exception in case the socket has disconnected.
   1.747 +      // When this happens, we pretend the ping was sent and don't specially
   1.748 +      // handle the exception, as the lack of a pong will lead to the socket
   1.749 +      // being reset.
   1.750 +      try {
   1.751 +        this._wsSendMessage({});
   1.752 +      } catch (e) {
   1.753 +      }
   1.754 +
   1.755 +      this._waitingForPong = true;
   1.756 +      this._setAlarm(prefs.get("requestTimeout"));
   1.757 +    }
   1.758 +    else if (this._alarmID !== null) {
   1.759 +      debug("reconnect alarm fired.");
   1.760 +      // Reconnect after back-off.
   1.761 +      // The check for a non-null _alarmID prevents a situation where the alarm
   1.762 +      // fires, but _shutdownWS() is called from another code-path (e.g.
   1.763 +      // network state change) and we don't want to reconnect.
   1.764 +      //
   1.765 +      // It also handles the case where _beginWSSetup() is called from another
   1.766 +      // code-path.
   1.767 +      //
   1.768 +      // alarmID will be non-null only when no shutdown/connect is
   1.769 +      // called between _reconnectAfterBackoff() setting the alarm and the
   1.770 +      // alarm firing.
   1.771 +
   1.772 +      // Websocket is shut down. Backoff interval expired, try to connect.
   1.773 +      this._beginWSSetup();
   1.774 +    }
   1.775 +  },
   1.776 +
   1.777 +  /**
   1.778 +   * Protocol handler invoked by server message.
   1.779 +   */
   1.780 +  _handleHelloReply: function(reply) {
   1.781 +    debug("handleHelloReply()");
   1.782 +    if (this._currentState != STATE_WAITING_FOR_HELLO) {
   1.783 +      debug("Unexpected state " + this._currentState +
   1.784 +            "(expected STATE_WAITING_FOR_HELLO)");
   1.785 +      this._shutdownWS();
   1.786 +      return;
   1.787 +    }
   1.788 +
   1.789 +    if (typeof reply.uaid !== "string") {
   1.790 +      debug("No UAID received or non string UAID received");
   1.791 +      this._shutdownWS();
   1.792 +      return;
   1.793 +    }
   1.794 +
   1.795 +    if (reply.uaid === "") {
   1.796 +      debug("Empty UAID received!");
   1.797 +      this._shutdownWS();
   1.798 +      return;
   1.799 +    }
   1.800 +
   1.801 +    // To avoid sticking extra large values sent by an evil server into prefs.
   1.802 +    if (reply.uaid.length > 128) {
   1.803 +      debug("UAID received from server was too long: " +
   1.804 +            reply.uaid);
   1.805 +      this._shutdownWS();
   1.806 +      return;
   1.807 +    }
   1.808 +
   1.809 +    function finishHandshake() {
   1.810 +      this._UAID = reply.uaid;
   1.811 +      this._currentState = STATE_READY;
   1.812 +      this._processNextRequestInQueue();
   1.813 +    }
   1.814 +
   1.815 +    // By this point we've got a UAID from the server that we are ready to
   1.816 +    // accept.
   1.817 +    //
   1.818 +    // If we already had a valid UAID before, we have to ask apps to
   1.819 +    // re-register.
   1.820 +    if (this._UAID && this._UAID != reply.uaid) {
   1.821 +      debug("got new UAID: all re-register");
   1.822 +
   1.823 +      this._notifyAllAppsRegister()
   1.824 +          .then(this._dropRegistrations.bind(this))
   1.825 +          .then(finishHandshake.bind(this));
   1.826 +
   1.827 +      return;
   1.828 +    }
   1.829 +
   1.830 +    // otherwise we are good to go
   1.831 +    finishHandshake.bind(this)();
   1.832 +  },
   1.833 +
   1.834 +  /**
   1.835 +   * Protocol handler invoked by server message.
   1.836 +   */
   1.837 +  _handleRegisterReply: function(reply) {
   1.838 +    debug("handleRegisterReply()");
   1.839 +    if (typeof reply.channelID !== "string" ||
   1.840 +        typeof this._pendingRequests[reply.channelID] !== "object")
   1.841 +      return;
   1.842 +
   1.843 +    let tmp = this._pendingRequests[reply.channelID];
   1.844 +    delete this._pendingRequests[reply.channelID];
   1.845 +    if (Object.keys(this._pendingRequests).length == 0 &&
   1.846 +        this._requestTimeoutTimer)
   1.847 +      this._requestTimeoutTimer.cancel();
   1.848 +
   1.849 +    if (reply.status == 200) {
   1.850 +      tmp.deferred.resolve(reply);
   1.851 +    } else {
   1.852 +      tmp.deferred.reject(reply);
   1.853 +    }
   1.854 +  },
   1.855 +
   1.856 +  /**
   1.857 +   * Protocol handler invoked by server message.
   1.858 +   */
   1.859 +  _handleNotificationReply: function(reply) {
   1.860 +    debug("handleNotificationReply()");
   1.861 +    if (typeof reply.updates !== 'object') {
   1.862 +      debug("No 'updates' field in response. Type = " + typeof reply.updates);
   1.863 +      return;
   1.864 +    }
   1.865 +
   1.866 +    debug("Reply updates: " + reply.updates.length);
   1.867 +    for (let i = 0; i < reply.updates.length; i++) {
   1.868 +      let update = reply.updates[i];
   1.869 +      debug("Update: " + update.channelID + ": " + update.version);
   1.870 +      if (typeof update.channelID !== "string") {
   1.871 +        debug("Invalid update literal at index " + i);
   1.872 +        continue;
   1.873 +      }
   1.874 +
   1.875 +      if (update.version === undefined) {
   1.876 +        debug("update.version does not exist");
   1.877 +        continue;
   1.878 +      }
   1.879 +
   1.880 +      let version = update.version;
   1.881 +
   1.882 +      if (typeof version === "string") {
   1.883 +        version = parseInt(version, 10);
   1.884 +      }
   1.885 +
   1.886 +      if (typeof version === "number" && version >= 0) {
   1.887 +        // FIXME(nsm): this relies on app update notification being infallible!
   1.888 +        // eventually fix this
   1.889 +        this._receivedUpdate(update.channelID, version);
   1.890 +        this._sendAck(update.channelID, version);
   1.891 +      }
   1.892 +    }
   1.893 +  },
   1.894 +
   1.895 +  // FIXME(nsm): batch acks for efficiency reasons.
   1.896 +  _sendAck: function(channelID, version) {
   1.897 +    debug("sendAck()");
   1.898 +    this._send('ack', {
   1.899 +      updates: [{channelID: channelID, version: version}]
   1.900 +    });
   1.901 +  },
   1.902 +
   1.903 +  /*
   1.904 +   * Must be used only by request/response style calls over the websocket.
   1.905 +   */
   1.906 +  _sendRequest: function(action, data) {
   1.907 +    debug("sendRequest() " + action);
   1.908 +    if (typeof data.channelID !== "string") {
   1.909 +      debug("Received non-string channelID");
   1.910 +      return Promise.reject("Received non-string channelID");
   1.911 +    }
   1.912 +
   1.913 +    let deferred = Promise.defer();
   1.914 +
   1.915 +    if (Object.keys(this._pendingRequests).length == 0) {
   1.916 +      // start the timer since we now have at least one request
   1.917 +      if (!this._requestTimeoutTimer)
   1.918 +        this._requestTimeoutTimer = Cc["@mozilla.org/timer;1"]
   1.919 +                                      .createInstance(Ci.nsITimer);
   1.920 +      this._requestTimeoutTimer.init(this,
   1.921 +                                     this._requestTimeout,
   1.922 +                                     Ci.nsITimer.TYPE_REPEATING_SLACK);
   1.923 +    }
   1.924 +
   1.925 +    this._pendingRequests[data.channelID] = { deferred: deferred,
   1.926 +                                              ctime: Date.now() };
   1.927 +
   1.928 +    this._send(action, data);
   1.929 +    return deferred.promise;
   1.930 +  },
   1.931 +
   1.932 +  _send: function(action, data) {
   1.933 +    debug("send()");
   1.934 +    this._requestQueue.push([action, data]);
   1.935 +    debug("Queued " + action);
   1.936 +    this._processNextRequestInQueue();
   1.937 +  },
   1.938 +
   1.939 +  _processNextRequestInQueue: function() {
   1.940 +    debug("_processNextRequestInQueue()");
   1.941 +
   1.942 +    if (this._requestQueue.length == 0) {
   1.943 +      debug("Request queue empty");
   1.944 +      return;
   1.945 +    }
   1.946 +
   1.947 +    if (this._currentState != STATE_READY) {
   1.948 +      if (!this._ws) {
   1.949 +        // This will end up calling processNextRequestInQueue().
   1.950 +        this._beginWSSetup();
   1.951 +      }
   1.952 +      else {
   1.953 +        // We have a socket open so we are just waiting for hello to finish.
   1.954 +        // That will call processNextRequestInQueue().
   1.955 +      }
   1.956 +      return;
   1.957 +    }
   1.958 +
   1.959 +    let [action, data] = this._requestQueue.shift();
   1.960 +    data.messageType = action;
   1.961 +    if (!this._ws) {
   1.962 +      // If our websocket is not ready and our state is STATE_READY we may as
   1.963 +      // well give up all assumptions about the world and start from scratch
   1.964 +      // again.  Discard the message itself, let the timeout notify error to
   1.965 +      // the app.
   1.966 +      debug("This should never happen!");
   1.967 +      this._shutdownWS();
   1.968 +    }
   1.969 +
   1.970 +    this._wsSendMessage(data);
   1.971 +    // Process the next one as soon as possible.
   1.972 +    setTimeout(this._processNextRequestInQueue.bind(this), 0);
   1.973 +  },
   1.974 +
   1.975 +  _receivedUpdate: function(aChannelID, aLatestVersion) {
   1.976 +    debug("Updating: " + aChannelID + " -> " + aLatestVersion);
   1.977 +
   1.978 +    let compareRecordVersionAndNotify = function(aPushRecord) {
   1.979 +      debug("compareRecordVersionAndNotify()");
   1.980 +      if (!aPushRecord) {
   1.981 +        debug("No record for channel ID " + aChannelID);
   1.982 +        return;
   1.983 +      }
   1.984 +
   1.985 +      if (aPushRecord.version == null ||
   1.986 +          aPushRecord.version < aLatestVersion) {
   1.987 +        debug("Version changed, notifying app and updating DB");
   1.988 +        aPushRecord.version = aLatestVersion;
   1.989 +        this._notifyApp(aPushRecord);
   1.990 +        this._updatePushRecord(aPushRecord)
   1.991 +          .then(
   1.992 +            null,
   1.993 +            function(e) {
   1.994 +              debug("Error updating push record");
   1.995 +            }
   1.996 +          );
   1.997 +      }
   1.998 +      else {
   1.999 +        debug("No significant version change: " + aLatestVersion);
  1.1000 +      }
  1.1001 +    }
  1.1002 +
  1.1003 +    let recoverNoSuchChannelID = function(aChannelIDFromServer) {
  1.1004 +      debug("Could not get channelID " + aChannelIDFromServer + " from DB");
  1.1005 +    }
  1.1006 +
  1.1007 +    this._db.getByChannelID(aChannelID,
  1.1008 +                            compareRecordVersionAndNotify.bind(this),
  1.1009 +                            recoverNoSuchChannelID.bind(this));
  1.1010 +  },
  1.1011 +
  1.1012 +  // Fires a push-register system message to all applications that have
  1.1013 +  // registrations.
  1.1014 +  _notifyAllAppsRegister: function() {
  1.1015 +    debug("notifyAllAppsRegister()");
  1.1016 +    let deferred = Promise.defer();
  1.1017 +
  1.1018 +    // records are objects describing the registrations as stored in IndexedDB.
  1.1019 +    function wakeupRegisteredApps(records) {
  1.1020 +      // Pages to be notified.
  1.1021 +      // wakeupTable[manifestURL] -> [ pageURL ]
  1.1022 +      let wakeupTable = {};
  1.1023 +      for (let i = 0; i < records.length; i++) {
  1.1024 +        let record = records[i];
  1.1025 +        if (!(record.manifestURL in wakeupTable))
  1.1026 +          wakeupTable[record.manifestURL] = [];
  1.1027 +
  1.1028 +        wakeupTable[record.manifestURL].push(record.pageURL);
  1.1029 +      }
  1.1030 +
  1.1031 +      let messenger = Cc["@mozilla.org/system-message-internal;1"]
  1.1032 +                        .getService(Ci.nsISystemMessagesInternal);
  1.1033 +
  1.1034 +      for (let manifestURL in wakeupTable) {
  1.1035 +        wakeupTable[manifestURL].forEach(function(pageURL) {
  1.1036 +          messenger.sendMessage('push-register', {},
  1.1037 +                                Services.io.newURI(pageURL, null, null),
  1.1038 +                                Services.io.newURI(manifestURL, null, null));
  1.1039 +        });
  1.1040 +      }
  1.1041 +
  1.1042 +      deferred.resolve();
  1.1043 +    }
  1.1044 +
  1.1045 +    this._db.getAllChannelIDs(wakeupRegisteredApps, deferred.reject);
  1.1046 +
  1.1047 +    return deferred.promise;
  1.1048 +  },
  1.1049 +
  1.1050 +  _notifyApp: function(aPushRecord) {
  1.1051 +    if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.manifestURL) {
  1.1052 +      debug("notifyApp() something is undefined.  Dropping notification");
  1.1053 +      return;
  1.1054 +    }
  1.1055 +
  1.1056 +    debug("notifyApp() " + aPushRecord.pageURL +
  1.1057 +          "  " + aPushRecord.manifestURL);
  1.1058 +    let pageURI = Services.io.newURI(aPushRecord.pageURL, null, null);
  1.1059 +    let manifestURI = Services.io.newURI(aPushRecord.manifestURL, null, null);
  1.1060 +    let message = {
  1.1061 +      pushEndpoint: aPushRecord.pushEndpoint,
  1.1062 +      version: aPushRecord.version
  1.1063 +    };
  1.1064 +    let messenger = Cc["@mozilla.org/system-message-internal;1"]
  1.1065 +                      .getService(Ci.nsISystemMessagesInternal);
  1.1066 +    messenger.sendMessage('push', message, pageURI, manifestURI);
  1.1067 +  },
  1.1068 +
  1.1069 +  _updatePushRecord: function(aPushRecord) {
  1.1070 +    debug("updatePushRecord()");
  1.1071 +    let deferred = Promise.defer();
  1.1072 +    this._db.put(aPushRecord, deferred.resolve, deferred.reject);
  1.1073 +    return deferred.promise;
  1.1074 +  },
  1.1075 +
  1.1076 +  _dropRegistrations: function() {
  1.1077 +    let deferred = Promise.defer();
  1.1078 +    this._db.drop(deferred.resolve, deferred.reject);
  1.1079 +    return deferred.promise;
  1.1080 +  },
  1.1081 +
  1.1082 +  receiveMessage: function(aMessage) {
  1.1083 +    debug("receiveMessage(): " + aMessage.name);
  1.1084 +
  1.1085 +    if (kCHILD_PROCESS_MESSAGES.indexOf(aMessage.name) == -1) {
  1.1086 +      debug("Invalid message from child " + aMessage.name);
  1.1087 +      return;
  1.1088 +    }
  1.1089 +
  1.1090 +    let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
  1.1091 +    let json = aMessage.data;
  1.1092 +    this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
  1.1093 +  },
  1.1094 +
  1.1095 +  /**
  1.1096 +   * Called on message from the child process. aPageRecord is an object sent by
  1.1097 +   * navigator.push, identifying the sending page and other fields.
  1.1098 +   */
  1.1099 +  register: function(aPageRecord, aMessageManager) {
  1.1100 +    debug("register()");
  1.1101 +
  1.1102 +    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
  1.1103 +                          .getService(Ci.nsIUUIDGenerator);
  1.1104 +    // generateUUID() gives a UUID surrounded by {...}, slice them off.
  1.1105 +    let channelID = uuidGenerator.generateUUID().toString().slice(1, -1);
  1.1106 +
  1.1107 +    this._sendRequest("register", {channelID: channelID})
  1.1108 +      .then(
  1.1109 +        this._onRegisterSuccess.bind(this, aPageRecord, channelID),
  1.1110 +        this._onRegisterError.bind(this, aPageRecord, aMessageManager)
  1.1111 +      )
  1.1112 +      .then(
  1.1113 +        function(message) {
  1.1114 +          aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
  1.1115 +        },
  1.1116 +        function(message) {
  1.1117 +          aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
  1.1118 +      });
  1.1119 +  },
  1.1120 +
  1.1121 +  /**
  1.1122 +   * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
  1.1123 +   * from _sendRequest, causing the promise to be rejected instead.
  1.1124 +   */
  1.1125 +  _onRegisterSuccess: function(aPageRecord, generatedChannelID, data) {
  1.1126 +    debug("_onRegisterSuccess()");
  1.1127 +    let deferred = Promise.defer();
  1.1128 +    let message = { requestID: aPageRecord.requestID };
  1.1129 +
  1.1130 +    if (typeof data.channelID !== "string") {
  1.1131 +      debug("Invalid channelID " + message);
  1.1132 +      message["error"] = "Invalid channelID received";
  1.1133 +      throw message;
  1.1134 +    }
  1.1135 +    else if (data.channelID != generatedChannelID) {
  1.1136 +      debug("Server replied with different channelID " + data.channelID +
  1.1137 +            " than what UA generated " + generatedChannelID);
  1.1138 +      message["error"] = "Server sent 200 status code but different channelID";
  1.1139 +      throw message;
  1.1140 +    }
  1.1141 +
  1.1142 +    try {
  1.1143 +      Services.io.newURI(data.pushEndpoint, null, null);
  1.1144 +    }
  1.1145 +    catch (e) {
  1.1146 +      debug("Invalid pushEndpoint " + data.pushEndpoint);
  1.1147 +      message["error"] = "Invalid pushEndpoint " + data.pushEndpoint;
  1.1148 +      throw message;
  1.1149 +    }
  1.1150 +
  1.1151 +    let record = {
  1.1152 +      channelID: data.channelID,
  1.1153 +      pushEndpoint: data.pushEndpoint,
  1.1154 +      pageURL: aPageRecord.pageURL,
  1.1155 +      manifestURL: aPageRecord.manifestURL,
  1.1156 +      version: null
  1.1157 +    };
  1.1158 +
  1.1159 +    this._updatePushRecord(record)
  1.1160 +      .then(
  1.1161 +        function() {
  1.1162 +          message["pushEndpoint"] = data.pushEndpoint;
  1.1163 +          deferred.resolve(message);
  1.1164 +        },
  1.1165 +        function(error) {
  1.1166 +          // Unable to save.
  1.1167 +          this._send("unregister", {channelID: record.channelID});
  1.1168 +          message["error"] = error;
  1.1169 +          deferred.reject(message);
  1.1170 +        }
  1.1171 +      );
  1.1172 +
  1.1173 +    return deferred.promise;
  1.1174 +  },
  1.1175 +
  1.1176 +  /**
  1.1177 +   * Exceptions thrown in _onRegisterError are caught by the promise obtained
  1.1178 +   * from _sendRequest, causing the promise to be rejected instead.
  1.1179 +   */
  1.1180 +  _onRegisterError: function(aPageRecord, aMessageManager, reply) {
  1.1181 +    debug("_onRegisterError()");
  1.1182 +    if (!reply.error) {
  1.1183 +      debug("Called without valid error message!");
  1.1184 +    }
  1.1185 +    throw { requestID: aPageRecord.requestID, error: reply.error };
  1.1186 +  },
  1.1187 +
  1.1188 +  /**
  1.1189 +   * Called on message from the child process.
  1.1190 +   *
  1.1191 +   * Why is the record being deleted from the local database before the server
  1.1192 +   * is told?
  1.1193 +   *
  1.1194 +   * Unregistration is for the benefit of the app and the AppServer
  1.1195 +   * so that the AppServer does not keep pinging a channel the UserAgent isn't
  1.1196 +   * watching The important part of the transaction in this case is left to the
  1.1197 +   * app, to tell its server of the unregistration.  Even if the request to the
  1.1198 +   * PushServer were to fail, it would not affect correctness of the protocol,
  1.1199 +   * and the server GC would just clean up the channelID eventually.  Since the
  1.1200 +   * appserver doesn't ping it, no data is lost.
  1.1201 +   *
  1.1202 +   * If rather we were to unregister at the server and update the database only
  1.1203 +   * on success: If the server receives the unregister, and deletes the
  1.1204 +   * channelID, but the response is lost because of network failure, the
  1.1205 +   * application is never informed. In addition the application may retry the
  1.1206 +   * unregister when it fails due to timeout at which point the server will say
  1.1207 +   * it does not know of this unregistration.  We'll have to make the
  1.1208 +   * registration/unregistration phases have retries and attempts to resend
  1.1209 +   * messages from the server, and have the client acknowledge. On a server,
  1.1210 +   * data is cheap, reliable notification is not.
  1.1211 +   */
  1.1212 +  unregister: function(aPageRecord, aMessageManager) {
  1.1213 +    debug("unregister()");
  1.1214 +
  1.1215 +    let fail = function(error) {
  1.1216 +      debug("unregister() fail() error " + error);
  1.1217 +      let message = {requestID: aPageRecord.requestID, error: error};
  1.1218 +      aMessageManager.sendAsyncMessage("PushService:Unregister:KO", message);
  1.1219 +    }
  1.1220 +
  1.1221 +    this._db.getByPushEndpoint(aPageRecord.pushEndpoint, function(record) {
  1.1222 +      // If the endpoint didn't exist, let's just fail.
  1.1223 +      if (record === undefined) {
  1.1224 +        fail("NotFoundError");
  1.1225 +        return;
  1.1226 +      }
  1.1227 +
  1.1228 +      // Non-owner tried to unregister, say success, but don't do anything.
  1.1229 +      if (record.manifestURL !== aPageRecord.manifestURL) {
  1.1230 +        aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
  1.1231 +          requestID: aPageRecord.requestID,
  1.1232 +          pushEndpoint: aPageRecord.pushEndpoint
  1.1233 +        });
  1.1234 +        return;
  1.1235 +      }
  1.1236 +
  1.1237 +      this._db.delete(record.channelID, function() {
  1.1238 +        // Let's be nice to the server and try to inform it, but we don't care
  1.1239 +        // about the reply.
  1.1240 +        this._send("unregister", {channelID: record.channelID});
  1.1241 +        aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
  1.1242 +          requestID: aPageRecord.requestID,
  1.1243 +          pushEndpoint: aPageRecord.pushEndpoint
  1.1244 +        });
  1.1245 +      }.bind(this), fail);
  1.1246 +    }.bind(this), fail);
  1.1247 +  },
  1.1248 +
  1.1249 +  /**
  1.1250 +   * Called on message from the child process
  1.1251 +   */
  1.1252 +  registrations: function(aPageRecord, aMessageManager) {
  1.1253 +    debug("registrations()");
  1.1254 +
  1.1255 +    if (aPageRecord.manifestURL) {
  1.1256 +      this._db.getAllByManifestURL(aPageRecord.manifestURL,
  1.1257 +        this._onRegistrationsSuccess.bind(this, aPageRecord, aMessageManager),
  1.1258 +        this._onRegistrationsError.bind(this, aPageRecord, aMessageManager));
  1.1259 +    }
  1.1260 +    else {
  1.1261 +      this._onRegistrationsError(aPageRecord, aMessageManager);
  1.1262 +    }
  1.1263 +  },
  1.1264 +
  1.1265 +  _onRegistrationsSuccess: function(aPageRecord,
  1.1266 +                                    aMessageManager,
  1.1267 +                                    pushRecords) {
  1.1268 +    let registrations = [];
  1.1269 +    pushRecords.forEach(function(pushRecord) {
  1.1270 +      registrations.push({
  1.1271 +          __exposedProps__: { pushEndpoint: 'r', version: 'r' },
  1.1272 +          pushEndpoint: pushRecord.pushEndpoint,
  1.1273 +          version: pushRecord.version
  1.1274 +      });
  1.1275 +    });
  1.1276 +    aMessageManager.sendAsyncMessage("PushService:Registrations:OK", {
  1.1277 +      requestID: aPageRecord.requestID,
  1.1278 +      registrations: registrations
  1.1279 +    });
  1.1280 +  },
  1.1281 +
  1.1282 +  _onRegistrationsError: function(aPageRecord, aMessageManager) {
  1.1283 +    aMessageManager.sendAsyncMessage("PushService:Registrations:KO", {
  1.1284 +      requestID: aPageRecord.requestID,
  1.1285 +      error: "Database error"
  1.1286 +    });
  1.1287 +  },
  1.1288 +
  1.1289 +  // begin Push protocol handshake
  1.1290 +  _wsOnStart: function(context) {
  1.1291 +    debug("wsOnStart()");
  1.1292 +    if (this._currentState != STATE_WAITING_FOR_WS_START) {
  1.1293 +      debug("NOT in STATE_WAITING_FOR_WS_START. Current state " +
  1.1294 +            this._currentState + ". Skipping");
  1.1295 +      return;
  1.1296 +    }
  1.1297 +
  1.1298 +    // Since we've had a successful connection reset the retry fail count.
  1.1299 +    this._retryFailCount = 0;
  1.1300 +
  1.1301 +    // Openning an available UDP port.
  1.1302 +    this._listenForUDPWakeup();
  1.1303 +
  1.1304 +    let data = {
  1.1305 +      messageType: "hello",
  1.1306 +    }
  1.1307 +
  1.1308 +    if (this._UAID)
  1.1309 +      data["uaid"] = this._UAID;
  1.1310 +
  1.1311 +    let networkState = this._getNetworkState();
  1.1312 +    if (networkState.ip) {
  1.1313 +      // Hostport is apparently a thing.
  1.1314 +      data["wakeup_hostport"] = {
  1.1315 +        ip: networkState.ip,
  1.1316 +        port: this._udpServer && this._udpServer.port
  1.1317 +      };
  1.1318 +
  1.1319 +      data["mobilenetwork"] = {
  1.1320 +        mcc: networkState.mcc,
  1.1321 +        mnc: networkState.mnc
  1.1322 +      };
  1.1323 +    }
  1.1324 +
  1.1325 +    function sendHelloMessage(ids) {
  1.1326 +      // On success, ids is an array, on error its not.
  1.1327 +      data["channelIDs"] = ids.map ?
  1.1328 +                           ids.map(function(el) { return el.channelID; }) : [];
  1.1329 +      this._wsSendMessage(data);
  1.1330 +      this._currentState = STATE_WAITING_FOR_HELLO;
  1.1331 +    }
  1.1332 +
  1.1333 +    this._db.getAllChannelIDs(sendHelloMessage.bind(this),
  1.1334 +                              sendHelloMessage.bind(this));
  1.1335 +  },
  1.1336 +
  1.1337 +  /**
  1.1338 +   * This statusCode is not the websocket protocol status code, but the TCP
  1.1339 +   * connection close status code.
  1.1340 +   *
  1.1341 +   * If we do not explicitly call ws.close() then statusCode is always
  1.1342 +   * NS_BASE_STREAM_CLOSED, even on a successful close.
  1.1343 +   */
  1.1344 +  _wsOnStop: function(context, statusCode) {
  1.1345 +    debug("wsOnStop()");
  1.1346 +
  1.1347 +    if (statusCode != Cr.NS_OK &&
  1.1348 +        !(statusCode == Cr.NS_BASE_STREAM_CLOSED && this._willBeWokenUpByUDP)) {
  1.1349 +      debug("Socket error " + statusCode);
  1.1350 +      this._reconnectAfterBackoff();
  1.1351 +    }
  1.1352 +
  1.1353 +    // Bug 896919. We always shutdown the WebSocket, even if we need to
  1.1354 +    // reconnect. This works because _reconnectAfterBackoff() is "async"
  1.1355 +    // (there is a minimum delay of the pref retryBaseInterval, which by default
  1.1356 +    // is 5000ms), so that function will open the WebSocket again.
  1.1357 +    this._shutdownWS();
  1.1358 +  },
  1.1359 +
  1.1360 +  _wsOnMessageAvailable: function(context, message) {
  1.1361 +    debug("wsOnMessageAvailable() " + message);
  1.1362 +
  1.1363 +    this._waitingForPong = false;
  1.1364 +
  1.1365 +    // Reset the ping timer.  Note: This path is executed at every step of the
  1.1366 +    // handshake, so this alarm does not need to be set explicitly at startup.
  1.1367 +    this._setAlarm(prefs.get("pingInterval"));
  1.1368 +
  1.1369 +    let reply = undefined;
  1.1370 +    try {
  1.1371 +      reply = JSON.parse(message);
  1.1372 +    } catch(e) {
  1.1373 +      debug("Parsing JSON failed. text : " + message);
  1.1374 +      return;
  1.1375 +    }
  1.1376 +
  1.1377 +    if (typeof reply.messageType != "string") {
  1.1378 +      debug("messageType not a string " + reply.messageType);
  1.1379 +      return;
  1.1380 +    }
  1.1381 +
  1.1382 +    // A whitelist of protocol handlers. Add to these if new messages are added
  1.1383 +    // in the protocol.
  1.1384 +    let handlers = ["Hello", "Register", "Notification"];
  1.1385 +
  1.1386 +    // Build up the handler name to call from messageType.
  1.1387 +    // e.g. messageType == "register" -> _handleRegisterReply.
  1.1388 +    let handlerName = reply.messageType[0].toUpperCase() +
  1.1389 +                      reply.messageType.slice(1).toLowerCase();
  1.1390 +
  1.1391 +    if (handlers.indexOf(handlerName) == -1) {
  1.1392 +      debug("No whitelisted handler " + handlerName + ". messageType: " +
  1.1393 +            reply.messageType);
  1.1394 +      return;
  1.1395 +    }
  1.1396 +
  1.1397 +    let handler = "_handle" + handlerName + "Reply";
  1.1398 +
  1.1399 +    if (typeof this[handler] !== "function") {
  1.1400 +      debug("Handler whitelisted but not implemented! " + handler);
  1.1401 +      return;
  1.1402 +    }
  1.1403 +
  1.1404 +    this[handler](reply);
  1.1405 +  },
  1.1406 +
  1.1407 +  /**
  1.1408 +   * The websocket should never be closed. Since we don't call ws.close(),
  1.1409 +   * _wsOnStop() receives error code NS_BASE_STREAM_CLOSED (see comment in that
  1.1410 +   * function), which calls reconnect and re-establishes the WebSocket
  1.1411 +   * connection.
  1.1412 +   *
  1.1413 +   * If the server said it'll use UDP for wakeup, we set _willBeWokenUpByUDP
  1.1414 +   * and stop reconnecting in _wsOnStop().
  1.1415 +   */
  1.1416 +  _wsOnServerClose: function(context, aStatusCode, aReason) {
  1.1417 +    debug("wsOnServerClose() " + aStatusCode + " " + aReason);
  1.1418 +
  1.1419 +    // Switch over to UDP.
  1.1420 +    if (aStatusCode == kUDP_WAKEUP_WS_STATUS_CODE) {
  1.1421 +      debug("Server closed with promise to wake up");
  1.1422 +      this._willBeWokenUpByUDP = true;
  1.1423 +      // TODO: there should be no pending requests
  1.1424 +    }
  1.1425 +  },
  1.1426 +
  1.1427 +  _listenForUDPWakeup: function() {
  1.1428 +    debug("listenForUDPWakeup()");
  1.1429 +
  1.1430 +    if (this._udpServer) {
  1.1431 +      debug("UDP Server already running");
  1.1432 +      return;
  1.1433 +    }
  1.1434 +
  1.1435 +    if (!this._getNetworkState().ip) {
  1.1436 +      debug("No IP");
  1.1437 +      return;
  1.1438 +    }
  1.1439 +
  1.1440 +    if (!prefs.get("udp.wakeupEnabled")) {
  1.1441 +      debug("UDP support disabled");
  1.1442 +      return;
  1.1443 +    }
  1.1444 +
  1.1445 +    this._udpServer = Cc["@mozilla.org/network/udp-socket;1"]
  1.1446 +                        .createInstance(Ci.nsIUDPSocket);
  1.1447 +    this._udpServer.init(-1, false);
  1.1448 +    this._udpServer.asyncListen(this);
  1.1449 +    debug("listenForUDPWakeup listening on " + this._udpServer.port);
  1.1450 +
  1.1451 +    return this._udpServer.port;
  1.1452 +  },
  1.1453 +
  1.1454 +  /**
  1.1455 +   * Called by UDP Server Socket. As soon as a ping is recieved via UDP,
  1.1456 +   * reconnect the WebSocket and get the actual data.
  1.1457 +   */
  1.1458 +  onPacketReceived: function(aServ, aMessage) {
  1.1459 +    debug("Recv UDP datagram on port: " + this._udpServer.port);
  1.1460 +    this._beginWSSetup();
  1.1461 +  },
  1.1462 +
  1.1463 +  /**
  1.1464 +   * Called by UDP Server Socket if the socket was closed for some reason.
  1.1465 +   *
  1.1466 +   * If this happens, we reconnect the WebSocket to not miss out on
  1.1467 +   * notifications.
  1.1468 +   */
  1.1469 +  onStopListening: function(aServ, aStatus) {
  1.1470 +    debug("UDP Server socket was shutdown. Status: " + aStatus);
  1.1471 +    this._udpServer = undefined;
  1.1472 +    this._beginWSSetup();
  1.1473 +  },
  1.1474 +
  1.1475 +  /**
  1.1476 +   * Get mobile network information to decide if the client is capable of being
  1.1477 +   * woken up by UDP (which currently just means having an mcc and mnc along
  1.1478 +   * with an IP).
  1.1479 +   */
  1.1480 +  _getNetworkState: function() {
  1.1481 +    debug("getNetworkState()");
  1.1482 +    try {
  1.1483 +      if (!prefs.get("udp.wakeupEnabled")) {
  1.1484 +        debug("UDP support disabled, we do not send any carrier info");
  1.1485 +        throw "UDP disabled";
  1.1486 +      }
  1.1487 +
  1.1488 +      let nm = Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager);
  1.1489 +      if (nm.active && nm.active.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) {
  1.1490 +        let icc = Cc["@mozilla.org/ril/content-helper;1"].getService(Ci.nsIIccProvider);
  1.1491 +        // TODO: Bug 927721 - PushService for multi-sim
  1.1492 +        // In Multi-sim, there is more than one client in iccProvider. Each
  1.1493 +        // client represents a icc service. To maintain backward compatibility
  1.1494 +        // with single sim, we always use client 0 for now. Adding support
  1.1495 +        // for multiple sim will be addressed in bug 927721, if needed.
  1.1496 +        let clientId = 0;
  1.1497 +        let iccInfo = icc.getIccInfo(clientId);
  1.1498 +        if (iccInfo) {
  1.1499 +          debug("Running on mobile data");
  1.1500 +          return {
  1.1501 +            mcc: iccInfo.mcc,
  1.1502 +            mnc: iccInfo.mnc,
  1.1503 +            ip:  nm.active.ip
  1.1504 +          }
  1.1505 +        }
  1.1506 +      }
  1.1507 +    } catch (e) {}
  1.1508 +
  1.1509 +    debug("Running on wifi");
  1.1510 +
  1.1511 +    return {
  1.1512 +      mcc: 0,
  1.1513 +      mnc: 0,
  1.1514 +      ip: undefined
  1.1515 +    };
  1.1516 +  },
  1.1517 +
  1.1518 +  // utility function used to add/remove observers in init() and shutdown()
  1.1519 +  _getNetworkStateChangeEventName: function() {
  1.1520 +    try {
  1.1521 +      Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager);
  1.1522 +      return "network-active-changed";
  1.1523 +    } catch (e) {
  1.1524 +      return "network:offline-status-changed";
  1.1525 +    }
  1.1526 +  }
  1.1527 +}

mercurial