Wed, 31 Dec 2014 06:09:35 +0100
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 | } |