forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1644 lines
		
	
	
	
		
			55 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1644 lines
		
	
	
	
		
			55 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 | |
| 
 | |
| // Invalid auth token as per
 | |
| // https://github.com/mozilla-services/loop-server/blob/45787d34108e2f0d87d74d4ddf4ff0dbab23501c/loop/errno.json#L6
 | |
| const INVALID_AUTH_TOKEN = 110;
 | |
| 
 | |
| // Ticket numbers are 24 bits in length.
 | |
| // The highest valid ticket number is 16777214 (2^24 - 2), so that a "now
 | |
| // serving" number of 2^24 - 1 is greater than it.
 | |
| const MAX_SOFT_START_TICKET_NUMBER = 16777214;
 | |
| 
 | |
| const LOOP_SESSION_TYPE = {
 | |
|   GUEST: 1,
 | |
|   FXA: 2,
 | |
| };
 | |
| 
 | |
| // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 | |
| const PREF_LOG_LEVEL = "loop.debug.loglevel";
 | |
| 
 | |
| Cu.import("resource://gre/modules/Services.jsm");
 | |
| Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| Cu.import("resource://gre/modules/Promise.jsm");
 | |
| Cu.import("resource://gre/modules/osfile.jsm", this);
 | |
| Cu.import("resource://gre/modules/Task.jsm");
 | |
| Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 | |
| Cu.importGlobalProperties(["URL"]);
 | |
| 
 | |
| this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE"];
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
 | |
|   "resource:///modules/loop/MozLoopAPI.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
 | |
|   "resource://gre/modules/media/RTCStatsReport.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
 | |
|                                   "resource://services-common/utils.js");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
 | |
|                                   "resource://services-crypto/utils.js");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
 | |
|                                   "resource://gre/modules/FxAccountsProfileClient.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
 | |
|                                   "resource://services-common/hawkclient.js");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
 | |
|                                   "resource://services-common/hawkrequest.js");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
 | |
|                                   "resource:///modules/loop/LoopStorage.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
 | |
|                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
 | |
|                                    "@mozilla.org/uuid-generator;1",
 | |
|                                    "nsIUUIDGenerator");
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
 | |
|                                    "@mozilla.org/network/dns-service;1",
 | |
|                                    "nsIDNSService");
 | |
| 
 | |
| // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
 | |
| XPCOMUtils.defineLazyGetter(this, "log", () => {
 | |
|   let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
 | |
|   let consoleOptions = {
 | |
|     maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
 | |
|     prefix: "Loop",
 | |
|   };
 | |
|   return new ConsoleAPI(consoleOptions);
 | |
| });
 | |
| 
 | |
| function setJSONPref(aName, aValue) {
 | |
|   let value = !!aValue ? JSON.stringify(aValue) : "";
 | |
|   Services.prefs.setCharPref(aName, value);
 | |
| }
 | |
| 
 | |
| function getJSONPref(aName) {
 | |
|   let value = Services.prefs.getCharPref(aName);
 | |
|   return !!value ? JSON.parse(value) : null;
 | |
| }
 | |
| 
 | |
| // The current deferred for the registration process. This is set if in progress
 | |
| // or the registration was successful. This is null if a registration attempt was
 | |
| // unsuccessful.
 | |
| let gRegisteredDeferred = null;
 | |
| let gPushHandler = null;
 | |
| let gHawkClient = null;
 | |
| let gLocalizedStrings = null;
 | |
| let gInitializeTimer = null;
 | |
| let gFxAEnabled = true;
 | |
| let gFxAOAuthClientPromise = null;
 | |
| let gFxAOAuthClient = null;
 | |
| let gErrors = new Map();
 | |
| 
 | |
|  /**
 | |
|  * Attempts to open a websocket.
 | |
|  *
 | |
|  * A new websocket interface is used each time. If an onStop callback
 | |
|  * was received, calling asyncOpen() on the same interface will
 | |
|  * trigger a "alreay open socket" exception even though the channel
 | |
|  * is logically closed.
 | |
|  */
 | |
| function CallProgressSocket(progressUrl, callId, token) {
 | |
|   if (!progressUrl || !callId || !token) {
 | |
|     throw new Error("missing required arguments");
 | |
|   }
 | |
| 
 | |
|   this._progressUrl = progressUrl;
 | |
|   this._callId = callId;
 | |
|   this._token = token;
 | |
| }
 | |
| 
 | |
| CallProgressSocket.prototype = {
 | |
|   /**
 | |
|    * Open websocket and run hello exchange.
 | |
|    * Sends a hello message to the server.
 | |
|    *
 | |
|    * @param {function} Callback used after a successful handshake
 | |
|    *                   over the progressUrl.
 | |
|    * @param {function} Callback used if an error is encountered
 | |
|    */
 | |
|   connect: function(onSuccess, onError) {
 | |
|     this._onSuccess = onSuccess;
 | |
|     this._onError = onError ||
 | |
|       (reason => {log.warn("MozLoopService::callProgessSocket - ", reason);});
 | |
| 
 | |
|     if (!onSuccess) {
 | |
|       this._onError("missing onSuccess argument");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (Services.io.offline) {
 | |
|       this._onError("IO offline");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let uri = Services.io.newURI(this._progressUrl, null, null);
 | |
| 
 | |
|     // Allow _websocket to be set for testing.
 | |
|     this._websocket = this._websocket ||
 | |
|       Cc["@mozilla.org/network/protocol;1?name=" + uri.scheme]
 | |
|         .createInstance(Ci.nsIWebSocketChannel);
 | |
| 
 | |
|     this._websocket.asyncOpen(uri, this._progressUrl, this, null);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listener method, handles the start of the websocket stream.
 | |
|    * Sends a hello message to the server.
 | |
|    *
 | |
|    * @param {nsISupports} aContext Not used
 | |
|    */
 | |
|   onStart: function() {
 | |
|     let helloMsg = {
 | |
|       messageType: "hello",
 | |
|       callId: this._callId,
 | |
|       auth: this._token,
 | |
|     };
 | |
|     try { // in case websocket has closed before this handler is run
 | |
|       this._websocket.sendMsg(JSON.stringify(helloMsg));
 | |
|     }
 | |
|     catch (error) {
 | |
|       this._onError(error);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listener method, called when the websocket is closed.
 | |
|    *
 | |
|    * @param {nsISupports} aContext Not used
 | |
|    * @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
 | |
|    */
 | |
|   onStop: function(aContext, aStatusCode) {
 | |
|     if (!this._handshakeComplete) {
 | |
|       this._onError("[" + aStatusCode + "]");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listener method, called when the websocket is closed by the server.
 | |
|    * If there are errors, onStop may be called without ever calling this
 | |
|    * method.
 | |
|    *
 | |
|    * @param {nsISupports} aContext Not used
 | |
|    * @param {integer} aCode the websocket closing handshake close code
 | |
|    * @param {String} aReason the websocket closing handshake close reason
 | |
|    */
 | |
|   onServerClose: function(aContext, aCode, aReason) {
 | |
|     if (!this._handshakeComplete) {
 | |
|       this._onError("[" + aCode + "]" + aReason);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listener method, called when the websocket receives a message.
 | |
|    *
 | |
|    * @param {nsISupports} aContext Not used
 | |
|    * @param {String} aMsg The message data
 | |
|    */
 | |
|   onMessageAvailable: function(aContext, aMsg) {
 | |
|     let msg = {};
 | |
|     try {
 | |
|       msg = JSON.parse(aMsg);
 | |
|     }
 | |
|     catch (error) {
 | |
|       log.error("MozLoopService: error parsing progress message - ", error);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (msg.messageType && msg.messageType === 'hello') {
 | |
|       this._handshakeComplete = true;
 | |
|       this._onSuccess();
 | |
|     }
 | |
|   },
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Create a JSON message payload and send on websocket.
 | |
|    *
 | |
|    * @param {Object} aMsg Message to send.
 | |
|    */
 | |
|   _send: function(aMsg) {
 | |
|     if (!this._handshakeComplete) {
 | |
|       log.warn("MozLoopService::_send error - handshake not complete");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       this._websocket.sendMsg(JSON.stringify(aMsg));
 | |
|     }
 | |
|     catch (error) {
 | |
|       this._onError(error);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Notifies the server that the user has declined the call
 | |
|    * with a reason of busy.
 | |
|    */
 | |
|   sendBusy: function() {
 | |
|     this._send({
 | |
|       messageType: "action",
 | |
|       event: "terminate",
 | |
|       reason: "busy"
 | |
|     });
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Internal helper methods and state
 | |
|  *
 | |
|  * The registration is a two-part process. First we need to connect to
 | |
|  * and register with the push server. Then we need to take the result of that
 | |
|  * and register with the Loop server.
 | |
|  */
 | |
| let MozLoopServiceInternal = {
 | |
|   callsData: {inUse: false},
 | |
|   _mocks: {webSocket: undefined},
 | |
| 
 | |
|   // The uri of the Loop server.
 | |
|   get loopServerUri() Services.prefs.getCharPref("loop.server"),
 | |
| 
 | |
|   /**
 | |
|    * The initial delay for push registration. This ensures we don't start
 | |
|    * kicking off straight after browser startup, just a few seconds later.
 | |
|    */
 | |
|   get initialRegistrationDelayMilliseconds() {
 | |
|     try {
 | |
|       // Let a pref override this for developer & testing use.
 | |
|       return Services.prefs.getIntPref("loop.initialDelay");
 | |
|     } catch (x) {
 | |
|       // Default to 5 seconds
 | |
|       return 5000;
 | |
|     }
 | |
|     return initialDelay;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the current latest expiry time for urls.
 | |
|    *
 | |
|    * In seconds since epoch.
 | |
|    */
 | |
|   get expiryTimeSeconds() {
 | |
|     try {
 | |
|       return Services.prefs.getIntPref("loop.urlsExpiryTimeSeconds");
 | |
|     } catch (x) {
 | |
|       // It is ok for the pref not to exist.
 | |
|       return 0;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets the expiry time to either the specified time, or keeps it the same
 | |
|    * depending on which is latest.
 | |
|    */
 | |
|   set expiryTimeSeconds(time) {
 | |
|     if (time > this.expiryTimeSeconds) {
 | |
|       Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", time);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns true if the expiry time is in the future.
 | |
|    */
 | |
|   urlExpiryTimeIsInFuture: function() {
 | |
|     return this.expiryTimeSeconds * 1000 > Date.now();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieves MozLoopService Firefox Accounts OAuth token.
 | |
|    *
 | |
|    * @return {Object} OAuth token
 | |
|    */
 | |
|   get fxAOAuthTokenData() {
 | |
|     return getJSONPref("loop.fxa_oauth.tokendata");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets MozLoopService Firefox Accounts OAuth token.
 | |
|    * If the tokenData is being cleared, will also clear the
 | |
|    * profile since the profile is dependent on the token data.
 | |
|    *
 | |
|    * @param {Object} aTokenData OAuth token
 | |
|    */
 | |
|   set fxAOAuthTokenData(aTokenData) {
 | |
|     setJSONPref("loop.fxa_oauth.tokendata", aTokenData);
 | |
|     if (!aTokenData) {
 | |
|       this.fxAOAuthProfile = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets MozLoopService Firefox Accounts Profile data.
 | |
|    *
 | |
|    * @param {Object} aProfileData Profile data
 | |
|    */
 | |
|   set fxAOAuthProfile(aProfileData) {
 | |
|     setJSONPref("loop.fxa_oauth.profile", aProfileData);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieves MozLoopService "do not disturb" pref value.
 | |
|    *
 | |
|    * @return {Boolean} aFlag
 | |
|    */
 | |
|   get doNotDisturb() {
 | |
|     return Services.prefs.getBoolPref("loop.do_not_disturb");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets MozLoopService "do not disturb" pref value.
 | |
|    *
 | |
|    * @param {Boolean} aFlag
 | |
|    */
 | |
|   set doNotDisturb(aFlag) {
 | |
|     Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
 | |
|     this.notifyStatusChanged();
 | |
|   },
 | |
| 
 | |
|   notifyStatusChanged: function(aReason = null) {
 | |
|     log.debug("notifyStatusChanged with reason:", aReason);
 | |
|     let profile = MozLoopService.userProfile;
 | |
|     LoopStorage.switchDatabase(profile ? profile.uid : null);
 | |
|     Services.obs.notifyObservers(null, "loop-status-changed", aReason);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Record an error and notify interested UI with the relevant user-facing strings attached.
 | |
|    *
 | |
|    * @param {String} errorType a key to identify the type of error. Only one
 | |
|    *                           error of a type will be saved at a time. This value may be used to
 | |
|    *                           determine user-facing (aka. friendly) strings.
 | |
|    * @param {Object} error     an object describing the error in the format from Hawk errors
 | |
|    */
 | |
|   setError: function(errorType, error) {
 | |
|     let messageString, detailsString, detailsButtonLabelString;
 | |
|     const NETWORK_ERRORS = [
 | |
|       Cr.NS_ERROR_CONNECTION_REFUSED,
 | |
|       Cr.NS_ERROR_NET_INTERRUPT,
 | |
|       Cr.NS_ERROR_NET_RESET,
 | |
|       Cr.NS_ERROR_NET_TIMEOUT,
 | |
|       Cr.NS_ERROR_OFFLINE,
 | |
|       Cr.NS_ERROR_PROXY_CONNECTION_REFUSED,
 | |
|       Cr.NS_ERROR_UNKNOWN_HOST,
 | |
|       Cr.NS_ERROR_UNKNOWN_PROXY_HOST,
 | |
|     ];
 | |
| 
 | |
|     if (error.code === null && error.errno === null &&
 | |
|         error.error instanceof Ci.nsIException &&
 | |
|         NETWORK_ERRORS.indexOf(error.error.result) != -1) {
 | |
|       // Network error. Override errorType so we can easily clear it on the next succesful request.
 | |
|       errorType = "network";
 | |
|       messageString = "could_not_connect";
 | |
|       detailsString = "check_internet_connection";
 | |
|       detailsButtonLabelString = "retry_button";
 | |
|     } else if (errorType == "profile" && error.code >= 500 && error.code < 600) {
 | |
|       messageString = "problem_accessing_account";
 | |
|     } else if (error.code == 401) {
 | |
|       if (errorType == "login") {
 | |
|         messageString = "could_not_authenticate"; // XXX: Bug 1076377
 | |
|         detailsString = "password_changed_question";
 | |
|         detailsButtonLabelString = "retry_button";
 | |
|       } else {
 | |
|         messageString = "session_expired_error_description";
 | |
|       }
 | |
|     } else if (error.code >= 500 && error.code < 600) {
 | |
|       messageString = "service_not_available";
 | |
|       detailsString = "try_again_later";
 | |
|       detailsButtonLabelString = "retry_button";
 | |
|     } else {
 | |
|       messageString = "generic_failure_title";
 | |
|     }
 | |
| 
 | |
|     error.friendlyMessage = this.localizedStrings[messageString].textContent;
 | |
|     error.friendlyDetails = detailsString ?
 | |
|                               this.localizedStrings[detailsString].textContent :
 | |
|                               null;
 | |
|     error.friendlyDetailsButtonLabel = detailsButtonLabelString ?
 | |
|                                          this.localizedStrings[detailsButtonLabelString].textContent :
 | |
|                                          null;
 | |
| 
 | |
|     gErrors.set(errorType, error);
 | |
|     this.notifyStatusChanged();
 | |
|   },
 | |
| 
 | |
|   clearError: function(errorType) {
 | |
|     gErrors.delete(errorType);
 | |
|     this.notifyStatusChanged();
 | |
|   },
 | |
| 
 | |
|   get errors() {
 | |
|     return gErrors;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Starts registration of Loop with the push server, and then will register
 | |
|    * with the Loop server. It will return early if already registered.
 | |
|    *
 | |
|    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
 | |
|    *                                 to allow mocking of the MozLoopPushHandler.
 | |
|    * @returns {Promise} a promise that is resolved with no params on completion, or
 | |
|    *          rejected with an error code or string.
 | |
|    */
 | |
|   promiseRegisteredWithServers: function(mockPushHandler, mockWebSocket) {
 | |
|     this._mocks.webSocket = mockWebSocket;
 | |
| 
 | |
|     if (gRegisteredDeferred) {
 | |
|       return gRegisteredDeferred.promise;
 | |
|     }
 | |
| 
 | |
|     gRegisteredDeferred = Promise.defer();
 | |
|     // We grab the promise early in case .initialize or its results sets
 | |
|     // it back to null on error.
 | |
|     let result = gRegisteredDeferred.promise;
 | |
| 
 | |
|     gPushHandler = mockPushHandler || MozLoopPushHandler;
 | |
|     gPushHandler.initialize(this.onPushRegistered.bind(this),
 | |
|                             this.onHandleNotification.bind(this));
 | |
| 
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Performs a hawk based request to the loop server.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        This is one of the LOOP_SESSION_TYPE members.
 | |
|    * @param {String} path The path to make the request to.
 | |
|    * @param {String} method The request method, e.g. 'POST', 'GET'.
 | |
|    * @param {Object} payloadObj An object which is converted to JSON and
 | |
|    *                            transmitted with the request.
 | |
|    * @returns {Promise}
 | |
|    *        Returns a promise that resolves to the response of the API call,
 | |
|    *        or is rejected with an error.  If the server response can be parsed
 | |
|    *        as JSON and contains an 'error' property, the promise will be
 | |
|    *        rejected with this JSON-parsed response.
 | |
|    */
 | |
|   hawkRequest: function(sessionType, path, method, payloadObj) {
 | |
|     if (!gHawkClient) {
 | |
|       gHawkClient = new HawkClient(this.loopServerUri);
 | |
|     }
 | |
| 
 | |
|     let sessionToken;
 | |
|     try {
 | |
|       sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType));
 | |
|     } catch (x) {
 | |
|       // It is ok for this not to exist, we'll default to sending no-creds
 | |
|     }
 | |
| 
 | |
|     let credentials;
 | |
|     if (sessionToken) {
 | |
|       // true = use a hex key, as required by the server (see bug 1032738).
 | |
|       credentials = deriveHawkCredentials(sessionToken, "sessionToken",
 | |
|                                           2 * 32, true);
 | |
|     }
 | |
| 
 | |
|     return gHawkClient.request(path, method, credentials, payloadObj).then((result) => {
 | |
|       this.clearError("network");
 | |
|       return result;
 | |
|     }, (error) => {
 | |
|       if (error.code == 401) {
 | |
|         this.clearSessionToken(sessionType);
 | |
| 
 | |
|         if (sessionType == LOOP_SESSION_TYPE.FXA) {
 | |
|           MozLoopService.logOutFromFxA().then(() => {
 | |
|             // Set a user-visible error after logOutFromFxA clears existing ones.
 | |
|             this.setError("login", error);
 | |
|           });
 | |
|         } else {
 | |
|           if (!this.urlExpiryTimeIsInFuture()) {
 | |
|             // If there are no Guest URLs in the future, don't use setError to notify the user since
 | |
|             // there isn't a need for a Guest registration at this time.
 | |
|             throw error;
 | |
|           }
 | |
| 
 | |
|           this.setError("registration", error);
 | |
|         }
 | |
|       }
 | |
|       throw error;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Generic hawkRequest onError handler for the hawkRequest promise.
 | |
|    *
 | |
|    * @param {Object} error - error reporting object
 | |
|    *
 | |
|    */
 | |
| 
 | |
|   _hawkRequestError: function(error) {
 | |
|     log.error("Loop hawkRequest error:", error);
 | |
|     throw error;
 | |
|   },
 | |
| 
 | |
|   getSessionTokenPrefName: function(sessionType) {
 | |
|     let suffix;
 | |
|     switch (sessionType) {
 | |
|       case LOOP_SESSION_TYPE.GUEST:
 | |
|         suffix = "";
 | |
|         break;
 | |
|       case LOOP_SESSION_TYPE.FXA:
 | |
|         suffix = ".fxa";
 | |
|         break;
 | |
|       default:
 | |
|         throw new Error("Unknown LOOP_SESSION_TYPE");
 | |
|         break;
 | |
|     }
 | |
|     return "loop.hawk-session-token" + suffix;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to store a session token from a request if it exists in the headers.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        One of the LOOP_SESSION_TYPE members.
 | |
|    * @param {Object} headers The request headers, which may include a
 | |
|    *                         "hawk-session-token" to be saved.
 | |
|    * @return true on success or no token, false on failure.
 | |
|    */
 | |
|   storeSessionToken: function(sessionType, headers) {
 | |
|     let sessionToken = headers["hawk-session-token"];
 | |
|     if (sessionToken) {
 | |
|       // XXX should do more validation here
 | |
|       if (sessionToken.length === 64) {
 | |
|         Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken);
 | |
|         log.debug("Stored a hawk session token for sessionType", sessionType);
 | |
|       } else {
 | |
|         // XXX Bubble the precise details up to the UI somehow (bug 1013248).
 | |
|         log.warn("Loop server sent an invalid session token");
 | |
|         gRegisteredDeferred.reject("session-token-wrong-size");
 | |
|         gRegisteredDeferred = null;
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Clear the loop session token so we don't use it for Hawk Requests anymore.
 | |
|    *
 | |
|    * This should normally be used after unregistering with the server so it can
 | |
|    * clean up session state first.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        One of the LOOP_SESSION_TYPE members.
 | |
|    */
 | |
|   clearSessionToken: function(sessionType) {
 | |
|     Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType));
 | |
|     log.debug("Cleared hawk session token for sessionType", sessionType);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Callback from MozLoopPushHandler - The push server has been registered
 | |
|    * and has given us a push url.
 | |
|    *
 | |
|    * @param {String} pushUrl The push url given by the push server.
 | |
|    */
 | |
|   onPushRegistered: function(err, pushUrl) {
 | |
|     if (err) {
 | |
|       gRegisteredDeferred.reject(err);
 | |
|       gRegisteredDeferred = null;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => {
 | |
|       // storeSessionToken could have rejected and nulled the promise if the token was malformed.
 | |
|       if (!gRegisteredDeferred) {
 | |
|         return;
 | |
|       }
 | |
|       gRegisteredDeferred.resolve("registered to guest status");
 | |
|       // No need to clear the promise here, everything was good, so we don't need
 | |
|       // to re-register.
 | |
|     }, error => {
 | |
|       log.error("Failed to register with Loop server: ", error);
 | |
|       // registerWithLoopServer may have already made this null.
 | |
|       if (gRegisteredDeferred) {
 | |
|         gRegisteredDeferred.reject(error);
 | |
|       }
 | |
|       gRegisteredDeferred = null;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Registers with the Loop server either as a guest or a FxA user.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
 | |
|    * @param {String} pushUrl The push url given by the push server.
 | |
|    * @param {Boolean} [retry=true] Whether to retry if authentication fails.
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   registerWithLoopServer: function(sessionType, pushUrl, retry = true) {
 | |
|     return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURL: pushUrl})
 | |
|       .then((response) => {
 | |
|         // If this failed we got an invalid token. storeSessionToken rejects
 | |
|         // the gRegisteredDeferred promise for us, so here we just need to
 | |
|         // early return.
 | |
|         if (!this.storeSessionToken(sessionType, response.headers))
 | |
|           return;
 | |
| 
 | |
|         log.debug("Successfully registered with server for sessionType", sessionType);
 | |
|         this.clearError("registration");
 | |
|       }, (error) => {
 | |
|         // There's other errors than invalid auth token, but we should only do the reset
 | |
|         // as a last resort.
 | |
|         if (error.code === 401) {
 | |
|           // Authorization failed, invalid token, we need to try again with a new token.
 | |
|           if (retry) {
 | |
|             return this.registerWithLoopServer(sessionType, pushUrl, false);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         log.error("Failed to register with the loop server. Error: ", error);
 | |
|         this.setError("registration", error);
 | |
|         gRegisteredDeferred.reject(error);
 | |
|         gRegisteredDeferred = null;
 | |
|         throw error;
 | |
|       }
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unregisters from the Loop server either as a guest or a FxA user.
 | |
|    *
 | |
|    * This is normally only wanted for FxA users as we normally want to keep the
 | |
|    * guest session with the device.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
 | |
|    * @param {String} pushURL The push URL previously given by the push server.
 | |
|    *                         This may not be necessary to unregister in the future.
 | |
|    * @return {Promise} resolving when the unregistration request finishes
 | |
|    */
 | |
|   unregisterFromLoopServer: function(sessionType, pushURL) {
 | |
|     let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(sessionType));
 | |
|     if (prefType == Services.prefs.PREF_INVALID) {
 | |
|       return Promise.resolve("already unregistered");
 | |
|     }
 | |
| 
 | |
|     let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
 | |
|     return this.hawkRequest(sessionType, unregisterURL, "DELETE")
 | |
|       .then(() => {
 | |
|         log.debug("Successfully unregistered from server for sessionType", sessionType);
 | |
|         MozLoopServiceInternal.clearSessionToken(sessionType);
 | |
|       },
 | |
|       error => {
 | |
|         // Always clear the registration token regardless of whether the server acknowledges the logout.
 | |
|         MozLoopServiceInternal.clearSessionToken(sessionType);
 | |
|         if (error.code === 401) {
 | |
|           // Authorization failed, invalid token. This is fine since it may mean we already logged out.
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         log.error("Failed to unregister with the loop server. Error: ", error);
 | |
|         throw error;
 | |
|       });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Callback from MozLoopPushHandler - A push notification has been received from
 | |
|    * the server.
 | |
|    *
 | |
|    * @param {String} version The version information from the server.
 | |
|    */
 | |
|   onHandleNotification: function(version) {
 | |
|     if (this.doNotDisturb) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // We set this here as it is assumed that once the user receives an incoming
 | |
|     // call, they'll have had enough time to see the terms of service. See
 | |
|     // bug 1046039 for background.
 | |
|     Services.prefs.setCharPref("loop.seenToS", "seen");
 | |
| 
 | |
|     // Request the information on the new call(s) associated with this version.
 | |
|     // The registered FxA session is checked first, then the anonymous session.
 | |
|     // Make the call to get the GUEST session regardless of whether the FXA
 | |
|     // request fails.
 | |
| 
 | |
|     if (MozLoopService.userProfile) {
 | |
|       this._getCalls(LOOP_SESSION_TYPE.FXA, version).catch(() => {});
 | |
|     }
 | |
|     this._getCalls(LOOP_SESSION_TYPE.GUEST, version).catch(
 | |
|       error => {this._hawkRequestError(error);});
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Make a hawkRequest to GET/calls?=version for this session type.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType - type of hawk token used
 | |
|    *        for the GET operation.
 | |
|    * @param {Object} version - LoopPushService notification version
 | |
|    *
 | |
|    * @returns {Promise}
 | |
|    *
 | |
|    */
 | |
| 
 | |
|   _getCalls: function(sessionType, version) {
 | |
|     return this.hawkRequest(sessionType, "/calls?version=" + version, "GET").then(
 | |
|       response => {this._processCalls(response, sessionType);}
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Process the calls array returned from a GET/calls?version request.
 | |
|    * Only one active call is permitted at this time.
 | |
|    *
 | |
|    * @param {Object} response - response payload from GET
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType - type of hawk token used
 | |
|    *        for the GET operation.
 | |
|    *
 | |
|    */
 | |
| 
 | |
|   _processCalls: function(response, sessionType) {
 | |
|     try {
 | |
|       let respData = JSON.parse(response.body);
 | |
|       if (respData.calls && Array.isArray(respData.calls)) {
 | |
|         respData.calls.forEach((callData) => {
 | |
|           if (!this.callsData.inUse) {
 | |
|             callData.sessionType = sessionType;
 | |
|             this._startCall(callData, "incoming");
 | |
|           } else {
 | |
|             this._returnBusy(callData);
 | |
|           }
 | |
|         });
 | |
|       } else {
 | |
|         log.warn("Error: missing calls[] in response");
 | |
|       }
 | |
|     } catch (err) {
 | |
|       log.warn("Error parsing calls info", err);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Starts a call, saves the call data, and opens a chat window.
 | |
|    *
 | |
|    * @param {Object} callData The data associated with the call including an id.
 | |
|    * @param {Boolean} conversationType Whether or not the call is "incoming"
 | |
|    *                                   or "outgoing"
 | |
|    */
 | |
|   _startCall: function(callData, conversationType) {
 | |
|     this.callsData.inUse = true;
 | |
|     this.callsData.data = callData;
 | |
|     this.openChatWindow(
 | |
|       null,
 | |
|       // No title, let the page set that, to avoid flickering.
 | |
|       "",
 | |
|       "about:loopconversation#" + conversationType + "/" + callData.callId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Starts a direct call to the contact addresses.
 | |
|    *
 | |
|    * @param {Object} contact The contact to call
 | |
|    * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
 | |
|    * @return true if the call is opened, false if it is not opened (i.e. busy)
 | |
|    */
 | |
|   startDirectCall: function(contact, callType) {
 | |
|     if (this.callsData.inUse)
 | |
|       return false;
 | |
| 
 | |
|     var callData = {
 | |
|       contact: contact,
 | |
|       callType: callType,
 | |
|       callId: Math.floor((Math.random() * 10))
 | |
|     };
 | |
| 
 | |
|     this._startCall(callData, "outgoing");
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|    /**
 | |
|    * Open call progress websocket and terminate with a reason of busy
 | |
|    * the server.
 | |
|    *
 | |
|    * @param {callData} Must contain the progressURL, callId and websocketToken
 | |
|    *                   returned by the LoopService.
 | |
|    */
 | |
|   _returnBusy: function(callData) {
 | |
|     let callProgress = new CallProgressSocket(
 | |
|       callData.progressURL,
 | |
|       callData.callId,
 | |
|       callData.websocketToken);
 | |
|     callProgress._websocket = this._mocks.webSocket;
 | |
|     // This instance of CallProgressSocket should stay alive until the underlying
 | |
|     // websocket is closed since it is passed to the websocket as the nsIWebSocketListener.
 | |
|     callProgress.connect(() => {callProgress.sendBusy();});
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * A getter to obtain and store the strings for loop. This is structured
 | |
|    * for use by l10n.js.
 | |
|    *
 | |
|    * @returns {Object} a map of element ids with attributes to set.
 | |
|    */
 | |
|   get localizedStrings() {
 | |
|     if (gLocalizedStrings)
 | |
|       return gLocalizedStrings;
 | |
| 
 | |
|     var stringBundle =
 | |
|       Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
 | |
| 
 | |
|     var map = {};
 | |
|     var enumerator = stringBundle.getSimpleEnumeration();
 | |
|     while (enumerator.hasMoreElements()) {
 | |
|       var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
 | |
| 
 | |
|       // 'textContent' is the default attribute to set if none are specified.
 | |
|       var key = string.key, property = 'textContent';
 | |
|       var i = key.lastIndexOf('.');
 | |
|       if (i >= 0) {
 | |
|         property = key.substring(i + 1);
 | |
|         key = key.substring(0, i);
 | |
|       }
 | |
|       if (!(key in map))
 | |
|         map[key] = {};
 | |
|       map[key][property] = string.value;
 | |
|     }
 | |
| 
 | |
|     return gLocalizedStrings = map;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Saves loop logs to the saved-telemetry-pings folder.
 | |
|    *
 | |
|    * @param {Object} pc The peerConnection in question.
 | |
|    */
 | |
|   stageForTelemetryUpload: function(window, pc) {
 | |
|     window.WebrtcGlobalInformation.getAllStats(allStats => {
 | |
|       let internalFormat = allStats.reports[0]; // filtered on pc.id
 | |
|       window.WebrtcGlobalInformation.getLogging('', logs => {
 | |
|         let report = convertToRTCStatsReport(internalFormat);
 | |
|         let logStr = "";
 | |
|         logs.forEach(s => { logStr += s + "\n"; });
 | |
| 
 | |
|         // We have stats and logs.
 | |
| 
 | |
|         // Create worker job. ping = saved telemetry ping file header + payload
 | |
|         //
 | |
|         // Prepare payload according to https://wiki.mozilla.org/Loop/Telemetry
 | |
| 
 | |
|         let ai = Services.appinfo;
 | |
|         let uuid = uuidgen.generateUUID().toString();
 | |
|         uuid = uuid.substr(1,uuid.length-2); // remove uuid curly braces
 | |
| 
 | |
|         let directory = OS.Path.join(OS.Constants.Path.profileDir,
 | |
|                                      "saved-telemetry-pings");
 | |
|         let job = {
 | |
|           directory: directory,
 | |
|           filename: uuid + ".json",
 | |
|           ping: {
 | |
|             reason: "loop",
 | |
|             slug: uuid,
 | |
|             payload: {
 | |
|               ver: 1,
 | |
|               info: {
 | |
|                 appUpdateChannel: ai.defaultUpdateChannel,
 | |
|                 appBuildID: ai.appBuildID,
 | |
|                 appName: ai.name,
 | |
|                 appVersion: ai.version,
 | |
|                 reason: "loop",
 | |
|                 OS: ai.OS,
 | |
|                 version: Services.sysinfo.getProperty("version")
 | |
|               },
 | |
|               report: "ice failure",
 | |
|               connectionstate: pc.iceConnectionState,
 | |
|               stats: report,
 | |
|               localSdp: internalFormat.localSdp,
 | |
|               remoteSdp: internalFormat.remoteSdp,
 | |
|               log: logStr
 | |
|             }
 | |
|           }
 | |
|         };
 | |
| 
 | |
|         // Send job to worker to do log sanitation, transcoding and saving to
 | |
|         // disk for pickup by telemetry on next startup, which then uploads it.
 | |
| 
 | |
|         let worker = new ChromeWorker("MozLoopWorker.js");
 | |
|         worker.onmessage = function(e) {
 | |
|           log.info(e.data.ok ?
 | |
|             "Successfully staged loop report for telemetry upload." :
 | |
|             ("Failed to stage loop report. Error: " + e.data.fail));
 | |
|         }
 | |
|         worker.postMessage(job);
 | |
|       });
 | |
|     }, pc.id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the chat window
 | |
|    *
 | |
|    * @param {Object} contentWindow The window to open the chat window in, may
 | |
|    *                               be null.
 | |
|    * @param {String} title The title of the chat window.
 | |
|    * @param {String} url The page to load in the chat window.
 | |
|    */
 | |
|   openChatWindow: function(contentWindow, title, url) {
 | |
|     // So I guess the origin is the loop server!?
 | |
|     let origin = this.loopServerUri;
 | |
|     url = url.spec || url;
 | |
| 
 | |
|     let callback = chatbox => {
 | |
|       // We need to use DOMContentLoaded as otherwise the injection will happen
 | |
|       // in about:blank and then get lost.
 | |
|       // Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
 | |
|       // involves event loop spins, which means it might be too late.
 | |
|       // Have we already done it?
 | |
|       if (chatbox.contentWindow.navigator.mozLoop) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       chatbox.setAttribute("dark", true);
 | |
| 
 | |
|       chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
 | |
|         if (event.target != chatbox.contentDocument) {
 | |
|           return;
 | |
|         }
 | |
|         chatbox.removeEventListener("DOMContentLoaded", loaded, true);
 | |
| 
 | |
|         let window = chatbox.contentWindow;
 | |
|         injectLoopAPI(window);
 | |
| 
 | |
|         let ourID = window.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
|             .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
 | |
| 
 | |
|         let onPCLifecycleChange = (pc, winID, type) => {
 | |
|           if (winID != ourID) {
 | |
|             return;
 | |
|           }
 | |
|           if (type == "iceconnectionstatechange") {
 | |
|             switch(pc.iceConnectionState) {
 | |
|               case "failed":
 | |
|               case "disconnected":
 | |
|                 if (Services.telemetry.canSend ||
 | |
|                     Services.prefs.getBoolPref("toolkit.telemetry.test")) {
 | |
|                   this.stageForTelemetryUpload(window, pc);
 | |
|                 }
 | |
|                 break;
 | |
|             }
 | |
|           }
 | |
|         };
 | |
| 
 | |
|         let pc_static = new window.mozRTCPeerConnectionStatic();
 | |
|         pc_static.registerPeerConnectionLifecycleCallback(onPCLifecycleChange);
 | |
|       }.bind(this), true);
 | |
|     };
 | |
| 
 | |
|     Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
 | |
|    *
 | |
|    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
 | |
|    */
 | |
|   promiseFxAOAuthParameters: function() {
 | |
|     const SESSION_TYPE = LOOP_SESSION_TYPE.FXA;
 | |
|     return this.hawkRequest(SESSION_TYPE, "/fxa-oauth/params", "POST").then(response => {
 | |
|       if (!this.storeSessionToken(SESSION_TYPE, response.headers)) {
 | |
|         throw new Error("Invalid FxA hawk token returned");
 | |
|       }
 | |
|       let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(SESSION_TYPE));
 | |
|       if (prefType == Services.prefs.PREF_INVALID) {
 | |
|         throw new Error("No FxA hawk token returned and we don't have one saved");
 | |
|       }
 | |
| 
 | |
|       return JSON.parse(response.body);
 | |
|     },
 | |
|     error => {this._hawkRequestError(error);});
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the OAuth client constructed with Loop OAauth parameters.
 | |
|    *
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   promiseFxAOAuthClient: Task.async(function* () {
 | |
|     // We must make sure to have only a single client otherwise they will have different states and
 | |
|     // multiple channels. This would happen if the user clicks the Login button more than once.
 | |
|     if (gFxAOAuthClientPromise) {
 | |
|       return gFxAOAuthClientPromise;
 | |
|     }
 | |
| 
 | |
|     gFxAOAuthClientPromise = this.promiseFxAOAuthParameters().then(
 | |
|       parameters => {
 | |
|         try {
 | |
|           gFxAOAuthClient = new FxAccountsOAuthClient({
 | |
|             parameters: parameters,
 | |
|           });
 | |
|         } catch (ex) {
 | |
|           gFxAOAuthClientPromise = null;
 | |
|           throw ex;
 | |
|         }
 | |
|         return gFxAOAuthClient;
 | |
|       },
 | |
|       error => {
 | |
|         gFxAOAuthClientPromise = null;
 | |
|         throw error;
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     return gFxAOAuthClientPromise;
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * Get the OAuth client and do the authorization web flow to get an OAuth code.
 | |
|    *
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   promiseFxAOAuthAuthorization: function() {
 | |
|     let deferred = Promise.defer();
 | |
|     this.promiseFxAOAuthClient().then(
 | |
|       client => {
 | |
|         client.onComplete = this._fxAOAuthComplete.bind(this, deferred);
 | |
|         client.launchWebFlow();
 | |
|       },
 | |
|       error => {
 | |
|         log.error(error);
 | |
|         deferred.reject(error);
 | |
|       }
 | |
|     );
 | |
|     return deferred.promise;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get the OAuth token using the OAuth code and state.
 | |
|    *
 | |
|    * The caller should approperiately handle 4xx errors (which should lead to a logout)
 | |
|    * and 5xx or connectivity issues with messaging to try again later.
 | |
|    *
 | |
|    * @param {String} code
 | |
|    * @param {String} state
 | |
|    *
 | |
|    * @return {Promise} resolving with OAuth token data.
 | |
|    */
 | |
|   promiseFxAOAuthToken: function(code, state) {
 | |
|     if (!code || !state) {
 | |
|       throw new Error("promiseFxAOAuthToken: code and state are required.");
 | |
|     }
 | |
| 
 | |
|     let payload = {
 | |
|       code: code,
 | |
|       state: state,
 | |
|     };
 | |
|     return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => {
 | |
|       return JSON.parse(response.body);
 | |
|     },
 | |
|     error => {this._hawkRequestError(error);});
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called once gFxAOAuthClient fires onComplete.
 | |
|    *
 | |
|    * @param {Deferred} deferred used to resolve or reject the gFxAOAuthClientPromise
 | |
|    * @param {Object} result (with code and state)
 | |
|    */
 | |
|   _fxAOAuthComplete: function(deferred, result) {
 | |
|     gFxAOAuthClientPromise = null;
 | |
| 
 | |
|     // Note: The state was already verified in FxAccountsOAuthClient.
 | |
|     if (result) {
 | |
|       deferred.resolve(result);
 | |
|     } else {
 | |
|       deferred.reject("Invalid token data");
 | |
|     }
 | |
|   },
 | |
| };
 | |
| Object.freeze(MozLoopServiceInternal);
 | |
| 
 | |
| let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSocket) => {
 | |
|   // Kick off the push notification service into registering after a timeout.
 | |
|   // This ensures we're not doing too much straight after the browser's finished
 | |
|   // starting up.
 | |
|   gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|   gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
 | |
|     yield MozLoopService.register(mockPushHandler, mockWebSocket).then(Task.async(function*() {
 | |
|       if (!MozLoopServiceInternal.fxAOAuthTokenData) {
 | |
|         log.debug("MozLoopService: Initialized without an already logged-in account");
 | |
|         deferredInitialization.resolve("initialized to guest status");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       log.debug("MozLoopService: Initializing with already logged-in account");
 | |
|       let registeredPromise =
 | |
|             MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA,
 | |
|                                                           gPushHandler.pushUrl);
 | |
|       registeredPromise.then(() => {
 | |
|         deferredInitialization.resolve("initialized to logged-in status");
 | |
|       }, error => {
 | |
|         log.debug("MozLoopService: error logging in using cached auth token");
 | |
|         MozLoopServiceInternal.setError("login", error);
 | |
|         deferredInitialization.reject("error logging in using cached auth token");
 | |
|       });
 | |
|     }), error => {
 | |
|       log.debug("MozLoopService: Failure of initial registration", error);
 | |
|       deferredInitialization.reject(error);
 | |
|     });
 | |
|     gInitializeTimer = null;
 | |
|   }),
 | |
|   MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Public API
 | |
|  */
 | |
| this.MozLoopService = {
 | |
|   _DNSService: gDNSService,
 | |
| 
 | |
|   set initializeTimerFunc(value) {
 | |
|     gInitializeTimerFunc = value;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Initialized the loop service, and starts registration with the
 | |
|    * push and loop servers.
 | |
|    *
 | |
|    * @return {Promise}
 | |
|    */
 | |
|   initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
 | |
|     // Do this here, rather than immediately after definition, so that we can
 | |
|     // stub out API functions for unit testing
 | |
|     Object.freeze(this);
 | |
| 
 | |
|     // Don't do anything if loop is not enabled.
 | |
|     if (!Services.prefs.getBoolPref("loop.enabled") ||
 | |
|         Services.prefs.getBoolPref("loop.throttled")) {
 | |
|       return Promise.reject("loop is not enabled");
 | |
|     }
 | |
| 
 | |
|     if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
 | |
|       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
 | |
|       if (!gFxAEnabled) {
 | |
|         yield this.logOutFromFxA();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If expiresTime is not in the future and the user hasn't
 | |
|     // previously authenticated then skip registration.
 | |
|     if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
 | |
|         !MozLoopServiceInternal.fxAOAuthTokenData) {
 | |
|       return Promise.resolve("registration not needed");
 | |
|     }
 | |
| 
 | |
|     let deferredInitialization = Promise.defer();
 | |
|     gInitializeTimerFunc(deferredInitialization, mockPushHandler, mockWebSocket);
 | |
| 
 | |
|     return deferredInitialization.promise.catch(error => {
 | |
|       if (typeof(error) == "object") {
 | |
|         // This never gets cleared since there is no UI to recover. Only restarting will work.
 | |
|         MozLoopServiceInternal.setError("initialization", error);
 | |
|       }
 | |
|       throw error;
 | |
|     });
 | |
|   }),
 | |
| 
 | |
|   /**
 | |
|    * If we're operating the service in "soft start" mode, and this browser
 | |
|    * isn't already activated, check whether it's time for it to become active.
 | |
|    * If so, activate the loop service.
 | |
|    *
 | |
|    * @param {Object} buttonNode DOM node representing the Loop button -- if we
 | |
|    *                            change from inactive to active, we need this
 | |
|    *                            in order to unhide the Loop button.
 | |
|    * @param {Function} doneCb   [optional] Callback that is called when the
 | |
|    *                            check has completed.
 | |
|    */
 | |
|   checkSoftStart(buttonNode, doneCb) {
 | |
|     if (!Services.prefs.getBoolPref("loop.throttled")) {
 | |
|       if (typeof(doneCb) == "function") {
 | |
|         doneCb(new Error("Throttling is not active"));
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (Services.io.offline) {
 | |
|       if (typeof(doneCb) == "function") {
 | |
|         doneCb(new Error("Cannot check soft-start value: browser is offline"));
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let ticket = Services.prefs.getIntPref("loop.soft_start_ticket_number");
 | |
|     if (!ticket || ticket > MAX_SOFT_START_TICKET_NUMBER || ticket < 0) {
 | |
|       // Ticket value isn't valid (probably isn't set up yet) -- pick a random
 | |
|       // number from 1 to MAX_SOFT_START_TICKET_NUMBER, inclusive, and write it
 | |
|       // into prefs.
 | |
|       ticket = Math.floor(Math.random() * MAX_SOFT_START_TICKET_NUMBER) + 1;
 | |
|       // Floating point numbers can be imprecise, so we need to deal with
 | |
|       // the case that Math.random() effectively rounds to 1.0
 | |
|       if (ticket > MAX_SOFT_START_TICKET_NUMBER) {
 | |
|         ticket = MAX_SOFT_START_TICKET_NUMBER;
 | |
|       }
 | |
|       Services.prefs.setIntPref("loop.soft_start_ticket_number", ticket);
 | |
|     }
 | |
| 
 | |
|     let onLookupComplete = (request, record, status) => {
 | |
|       // We don't bother checking errors -- if the DNS query fails,
 | |
|       // we just don't activate this time around. We'll check again on
 | |
|       // next startup.
 | |
|       if (!Components.isSuccessCode(status)) {
 | |
|         if (typeof(doneCb) == "function") {
 | |
|           doneCb(new Error("Error in DNS Lookup: " + status));
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let address = record.getNextAddrAsString().split(".");
 | |
|       if (address.length != 4) {
 | |
|         if (typeof(doneCb) == "function") {
 | |
|           doneCb(new Error("Invalid IP address"));
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (address[0] != 127) {
 | |
|         if (typeof(doneCb) == "function") {
 | |
|           doneCb(new Error("Throttling IP address is not on localhost subnet"));
 | |
|         }
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // Can't use bitwise operations here because JS treats all bitwise
 | |
|       // operations as 32-bit *signed* integers.
 | |
|       let now_serving = ((parseInt(address[1]) * 0x10000) +
 | |
|                          (parseInt(address[2]) * 0x100) +
 | |
|                          parseInt(address[3]));
 | |
| 
 | |
|       if (now_serving > ticket) {
 | |
|         // Hot diggity! It's our turn! Activate the service.
 | |
|         log.info("MozLoopService: Activating Loop via soft-start");
 | |
|         Services.prefs.setBoolPref("loop.throttled", false);
 | |
|         buttonNode.hidden = false;
 | |
|         this.initialize();
 | |
|       }
 | |
|       if (typeof(doneCb) == "function") {
 | |
|         doneCb(null);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // We use DNS to propagate the slow-start value, since it has well-known
 | |
|     // scaling properties. Ideally, this would use something more semantic,
 | |
|     // like a TXT record; but we don't support TXT in our DNS resolution (see
 | |
|     // Bug 14328), so we instead treat the lowest 24 bits of the IP address
 | |
|     // corresponding to our "slow start DNS name" as a 24-bit integer. To
 | |
|     // ensure that these addresses aren't routable, the highest 8 bits must
 | |
|     // be "127" (reserved for localhost).
 | |
|     let host = Services.prefs.getCharPref("loop.soft_start_hostname");
 | |
|     let task = this._DNSService.asyncResolve(host,
 | |
|                                              this._DNSService.RESOLVE_DISABLE_IPV6,
 | |
|                                              onLookupComplete,
 | |
|                                              Services.tm.mainThread);
 | |
|   },
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Starts registration of Loop with the push server, and then will register
 | |
|    * with the Loop server. It will return early if already registered.
 | |
|    *
 | |
|    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
 | |
|    *                                 to allow mocking of the MozLoopPushHandler.
 | |
|    * @returns {Promise} a promise that is resolved with no params on completion, or
 | |
|    *          rejected with an error code or string.
 | |
|    */
 | |
|   register: function(mockPushHandler, mockWebSocket) {
 | |
|     log.debug("registering");
 | |
|     // Don't do anything if loop is not enabled.
 | |
|     if (!Services.prefs.getBoolPref("loop.enabled")) {
 | |
|       throw new Error("Loop is not enabled");
 | |
|     }
 | |
| 
 | |
|     if (Services.prefs.getBoolPref("loop.throttled")) {
 | |
|       throw new Error("Loop is disabled by the soft-start mechanism");
 | |
|     }
 | |
| 
 | |
|     return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler, mockWebSocket);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Used to note a call url expiry time. If the time is later than the current
 | |
|    * latest expiry time, then the stored expiry time is increased. For times
 | |
|    * sooner, this function is a no-op; this ensures we always have the latest
 | |
|    * expiry time for a url.
 | |
|    *
 | |
|    * This is used to determine whether or not we should be registering with the
 | |
|    * push server on start.
 | |
|    *
 | |
|    * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
 | |
|    *                                    of the url.
 | |
|    */
 | |
|   noteCallUrlExpiry: function(expiryTimeSeconds) {
 | |
|     MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the strings for the specified element. Designed for use
 | |
|    * with l10n.js.
 | |
|    *
 | |
|    * @param {key} The element id to get strings for.
 | |
|    * @return {String} A JSON string containing the localized
 | |
|    *                  attribute/value pairs for the element.
 | |
|    */
 | |
|   getStrings: function(key) {
 | |
|       var stringData = MozLoopServiceInternal.localizedStrings;
 | |
|       if (!(key in stringData)) {
 | |
|         log.error("No string found for key: ", key);
 | |
|         return "";
 | |
|       }
 | |
| 
 | |
|       return JSON.stringify(stringData[key]);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns a new GUID (UUID) in curly braces format.
 | |
|    */
 | |
|   generateUUID: function() {
 | |
|     return uuidgen.generateUUID().toString();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieves MozLoopService "do not disturb" value.
 | |
|    *
 | |
|    * @return {Boolean}
 | |
|    */
 | |
|   get doNotDisturb() {
 | |
|     return MozLoopServiceInternal.doNotDisturb;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sets MozLoopService "do not disturb" value.
 | |
|    *
 | |
|    * @param {Boolean} aFlag
 | |
|    */
 | |
|   set doNotDisturb(aFlag) {
 | |
|     MozLoopServiceInternal.doNotDisturb = aFlag;
 | |
|   },
 | |
| 
 | |
|   get fxAEnabled() {
 | |
|     return gFxAEnabled;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gets the user profile, but only if there is
 | |
|    * tokenData present. Without tokenData, the
 | |
|    * profile is meaningless.
 | |
|    *
 | |
|    * @return {Object}
 | |
|    */
 | |
|   get userProfile() {
 | |
|     return getJSONPref("loop.fxa_oauth.tokendata") &&
 | |
|            getJSONPref("loop.fxa_oauth.profile");
 | |
|   },
 | |
| 
 | |
|   get errors() {
 | |
|     return MozLoopServiceInternal.errors;
 | |
|   },
 | |
| 
 | |
|   get log() {
 | |
|     return log;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the current locale
 | |
|    *
 | |
|    * @return {String} The code of the current locale.
 | |
|    */
 | |
|   get locale() {
 | |
|     try {
 | |
|       return Services.prefs.getComplexValue("general.useragent.locale",
 | |
|         Ci.nsISupportsString).data;
 | |
|     } catch (ex) {
 | |
|       return "en-US";
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns the callData for a specific loopCallId
 | |
|    *
 | |
|    * The data was retrieved from the LoopServer via a GET/calls/<version> request
 | |
|    * triggered by an incoming message from the LoopPushServer.
 | |
|    *
 | |
|    * @param {int} loopCallId
 | |
|    * @return {callData} The callData or undefined if error.
 | |
|    */
 | |
|   getCallData: function(loopCallId) {
 | |
|     if (MozLoopServiceInternal.callsData.data &&
 | |
|         MozLoopServiceInternal.callsData.data.callId == loopCallId) {
 | |
|       return MozLoopServiceInternal.callsData.data;
 | |
|     } else {
 | |
|       return undefined;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Releases the callData for a specific loopCallId
 | |
|    *
 | |
|    * The result of this call will be a free call session slot.
 | |
|    *
 | |
|    * @param {int} loopCallId
 | |
|    */
 | |
|   releaseCallData: function(loopCallId) {
 | |
|     if (MozLoopServiceInternal.callsData.data &&
 | |
|         MozLoopServiceInternal.callsData.data.callId == loopCallId) {
 | |
|       MozLoopServiceInternal.callsData.data = undefined;
 | |
|       MozLoopServiceInternal.callsData.inUse = false;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Set any character preference under "loop.".
 | |
|    *
 | |
|    * @param {String} prefName The name of the pref without the preceding "loop."
 | |
|    * @param {String} value The value to set.
 | |
|    *
 | |
|    * Any errors thrown by the Mozilla pref API are logged to the console.
 | |
|    */
 | |
|   setLoopCharPref: function(prefName, value) {
 | |
|     try {
 | |
|       Services.prefs.setCharPref("loop." + prefName, value);
 | |
|     } catch (ex) {
 | |
|       log.error("setLoopCharPref had trouble setting " + prefName +
 | |
|         "; exception: " + ex);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Return any preference under "loop." that's coercible to a character
 | |
|    * preference.
 | |
|    *
 | |
|    * @param {String} prefName The name of the pref without the preceding
 | |
|    * "loop."
 | |
|    *
 | |
|    * Any errors thrown by the Mozilla pref API are logged to the console
 | |
|    * and cause null to be returned. This includes the case of the preference
 | |
|    * not being found.
 | |
|    *
 | |
|    * @return {String} on success, null on error
 | |
|    */
 | |
|   getLoopCharPref: function(prefName) {
 | |
|     try {
 | |
|       return Services.prefs.getCharPref("loop." + prefName);
 | |
|     } catch (ex) {
 | |
|       log.error("getLoopCharPref had trouble getting " + prefName +
 | |
|         "; exception: " + ex);
 | |
|       return null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Return any preference under "loop." that's coercible to a character
 | |
|    * preference.
 | |
|    *
 | |
|    * @param {String} prefName The name of the pref without the preceding
 | |
|    * "loop."
 | |
|    *
 | |
|    * Any errors thrown by the Mozilla pref API are logged to the console
 | |
|    * and cause null to be returned. This includes the case of the preference
 | |
|    * not being found.
 | |
|    *
 | |
|    * @return {String} on success, null on error
 | |
|    */
 | |
|   getLoopBoolPref: function(prefName) {
 | |
|     try {
 | |
|       return Services.prefs.getBoolPref("loop." + prefName);
 | |
|     } catch (ex) {
 | |
|       log.error("getLoopBoolPref had trouble getting " + prefName +
 | |
|         "; exception: " + ex);
 | |
|       return null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Start the FxA login flow using the OAuth client and params from the Loop server.
 | |
|    *
 | |
|    * The caller should be prepared to handle rejections related to network, server or login errors.
 | |
|    *
 | |
|    * @return {Promise} that resolves when the FxA login flow is complete.
 | |
|    */
 | |
|   logInToFxA: function() {
 | |
|     log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
 | |
|     if (MozLoopServiceInternal.fxAOAuthTokenData) {
 | |
|       return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
 | |
|     }
 | |
| 
 | |
|     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
 | |
|       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
 | |
|     }).then(tokenData => {
 | |
|       MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
 | |
|       return tokenData;
 | |
|     }).then(tokenData => {
 | |
|       return gRegisteredDeferred.promise.then(Task.async(function*() {
 | |
|         if (gPushHandler.pushUrl) {
 | |
|           yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
 | |
|         } else {
 | |
|           throw new Error("No pushUrl for FxA registration");
 | |
|         }
 | |
|         MozLoopServiceInternal.clearError("login");
 | |
|         MozLoopServiceInternal.clearError("profile");
 | |
|         return MozLoopServiceInternal.fxAOAuthTokenData;
 | |
|       }));
 | |
|     }).then(tokenData => {
 | |
|       let client = new FxAccountsProfileClient({
 | |
|         serverURL: gFxAOAuthClient.parameters.profile_uri,
 | |
|         token: tokenData.access_token
 | |
|       });
 | |
|       client.fetchProfile().then(result => {
 | |
|         MozLoopServiceInternal.fxAOAuthProfile = result;
 | |
|         MozLoopServiceInternal.notifyStatusChanged("login");
 | |
|       }, error => {
 | |
|         log.error("Failed to retrieve profile", error);
 | |
|         this.setError("profile", error);
 | |
|         MozLoopServiceInternal.fxAOAuthProfile = null;
 | |
|         MozLoopServiceInternal.notifyStatusChanged();
 | |
|       });
 | |
|       return tokenData;
 | |
|     }).catch(error => {
 | |
|       MozLoopServiceInternal.fxAOAuthTokenData = null;
 | |
|       MozLoopServiceInternal.fxAOAuthProfile = null;
 | |
|       throw error;
 | |
|     }).catch((error) => {
 | |
|       MozLoopServiceInternal.setError("login", error);
 | |
|       // Re-throw for testing
 | |
|       throw error;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Logs the user out from FxA.
 | |
|    *
 | |
|    * Gracefully handles if the user is already logged out.
 | |
|    *
 | |
|    * @return {Promise} that resolves when the FxA logout flow is complete.
 | |
|    */
 | |
|   logOutFromFxA: Task.async(function*() {
 | |
|     log.debug("logOutFromFxA");
 | |
|     if (gPushHandler && gPushHandler.pushUrl) {
 | |
|       yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA,
 | |
|                                                             gPushHandler.pushUrl);
 | |
|     } else {
 | |
|       MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
 | |
|     }
 | |
| 
 | |
|     MozLoopServiceInternal.fxAOAuthTokenData = null;
 | |
|     MozLoopServiceInternal.fxAOAuthProfile = null;
 | |
| 
 | |
|     // Reset the client since the initial promiseFxAOAuthParameters() call is
 | |
|     // what creates a new session.
 | |
|     gFxAOAuthClient = null;
 | |
|     gFxAOAuthClientPromise = null;
 | |
| 
 | |
|     // clearError calls notifyStatusChanged so should be done last when the
 | |
|     // state is clean.
 | |
|     MozLoopServiceInternal.clearError("registration");
 | |
|     MozLoopServiceInternal.clearError("login");
 | |
|     MozLoopServiceInternal.clearError("profile");
 | |
|   }),
 | |
| 
 | |
|   openFxASettings: function() {
 | |
|     let url = new URL("/settings", gFxAOAuthClient.parameters.content_uri);
 | |
|     let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|     win.switchToTabHavingURI(url.toString(), true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Performs a hawk based request to the loop server.
 | |
|    *
 | |
|    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
 | |
|    *                                        One of the LOOP_SESSION_TYPE members.
 | |
|    * @param {String} path The path to make the request to.
 | |
|    * @param {String} method The request method, e.g. 'POST', 'GET'.
 | |
|    * @param {Object} payloadObj An object which is converted to JSON and
 | |
|    *                            transmitted with the request.
 | |
|    * @returns {Promise}
 | |
|    *        Returns a promise that resolves to the response of the API call,
 | |
|    *        or is rejected with an error.  If the server response can be parsed
 | |
|    *        as JSON and contains an 'error' property, the promise will be
 | |
|    *        rejected with this JSON-parsed response.
 | |
|    */
 | |
|   hawkRequest: function(sessionType, path, method, payloadObj) {
 | |
|     return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj).catch(
 | |
|       error => {MozLoopServiceInternal._hawkRequestError(error);});
 | |
|   },
 | |
| 
 | |
|     /**
 | |
|      * Starts a direct call to the contact addresses.
 | |
|      *
 | |
|      * @param {Object} contact The contact to call
 | |
|      * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
 | |
|      * @return true if the call is opened, false if it is not opened (i.e. busy)
 | |
|      */
 | |
|   startDirectCall: function(contact, callType) {
 | |
|     MozLoopServiceInternal.startDirectCall(contact, callType);
 | |
|   },
 | |
| };
 | 
