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 +}