dom/push/src/PushService.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial