Bug 1074663: Register with PushServer for updates to rooms, r=MattN

This commit is contained in:
Paul Kerr 2014-10-23 09:50:12 -07:00
parent a37c975c1d
commit 2fd6df7067
16 changed files with 878 additions and 582 deletions

View File

@ -0,0 +1,383 @@
/* 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;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["LoopCalls"];
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
"resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
"resource:///modules/loop/MozLoopService.jsm");
/**
* 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 => {MozLoopService.logwarn("LoopCalls::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) {
MozLoopService.logerror("LoopCalls: 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) {
MozLoopService.logwarn("LoopCalls::_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 LoopCallsInternal = {
callsData: {inUse: false},
_mocks: {webSocket: undefined},
/**
* Callback from MozLoopPushHandler - A push notification has been received from
* the server.
*
* @param {String} version The version information from the server.
*/
onNotification: function(version, channelID) {
if (MozLoopService.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 (channelID == LoopCalls.channelIDs.FxA && MozLoopService.userProfile) {
this._getCalls(LOOP_SESSION_TYPE.FXA, version);
} else {
this._getCalls(LOOP_SESSION_TYPE.GUEST, version);
}
},
/**
* 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 MozLoopService.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 {
MozLoopService.logwarn("Error: missing calls[] in response");
}
} catch (err) {
MozLoopService.logwarn("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;
MozLoopService.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();});
}
};
Object.freeze(LoopCallsInternal);
/**
* Public API
*/
this.LoopCalls = {
// Channel ids that will be registered with the PushServer for notifications
channelIDs: {
FxA: "25389583-921f-4169-a426-a4673658944b",
Guest: "801f754b-686b-43ec-bd83-1419bbf58388",
},
/**
* Callback from MozLoopPushHandler - A push notification has been received from
* the server.
*
* @param {String} version The version information from the server.
*/
onNotification: function(version, channelID) {
LoopCallsInternal.onNotification(version, channelID);
},
/**
* 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 (LoopCallsInternal.callsData.data &&
LoopCallsInternal.callsData.data.callId == loopCallId) {
return LoopCallsInternal.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 (LoopCallsInternal.callsData.data &&
LoopCallsInternal.callsData.data.callId == loopCallId) {
LoopCallsInternal.callsData.data = undefined;
LoopCallsInternal.callsData.inUse = false;
}
},
/**
* 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) {
LoopCallsInternal.startDirectCall(contact, callType);
}
};
Object.freeze(LoopCalls);

View File

@ -0,0 +1,25 @@
/* 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} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["LoopRooms"];
/**
* Public Loop Rooms API
*/
this.LoopRooms = Object.freeze({
// Channel ids that will be registered with the PushServer for notifications
channelIDs: {
FxA: "6add272a-d316-477c-8335-f00f73dfde71",
Guest: "19d3f799-a8f3-4328-9822-b7cd02765832",
},
onNotification: function(version, channelID) {
return;
},
});

View File

@ -9,6 +9,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/loop/LoopCalls.jsm");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
@ -47,7 +48,7 @@ const cloneErrorObject = function(error, targetWindow) {
if (typeof value != "string" && typeof value != "number") {
value = String(value);
}
Object.defineProperty(Cu.waiveXrays(obj), prop, {
configurable: false,
enumerable: true,
@ -204,7 +205,7 @@ function injectLoopAPI(targetWindow) {
enumerable: true,
writable: true,
value: function(loopCallId) {
return Cu.cloneInto(MozLoopService.getCallData(loopCallId), targetWindow);
return Cu.cloneInto(LoopCalls.getCallData(loopCallId), targetWindow);
}
},
@ -219,7 +220,7 @@ function injectLoopAPI(targetWindow) {
enumerable: true,
writable: true,
value: function(loopCallId) {
MozLoopService.releaseCallData(loopCallId);
LoopCalls.releaseCallData(loopCallId);
}
},
@ -653,7 +654,7 @@ function injectLoopAPI(targetWindow) {
enumerable: true,
writable: true,
value: function(contact, callType) {
MozLoopService.startDirectCall(contact, callType);
LoopCalls.startDirectCall(contact, callType);
}
},
};

View File

@ -9,6 +9,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
this.EXPORTED_SYMBOLS = ["MozLoopPushHandler"];
@ -22,14 +23,15 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
let MozLoopPushHandler = {
// This is the uri of the push server.
pushServerUri: undefined,
// This is the channel id we're using for notifications
channelID: "8b1081ce-9b35-42b5-b8f5-3ff8cb813a50",
// Records containing the registration and notification callbacks indexed by channelID.
// Each channel will be registered with the PushServer.
channels: {},
// This is the UserAgent UUID assigned by the PushServer
uaID: undefined,
// Stores the push url if we're registered and we have one.
pushUrl: undefined,
// Set to true once the channelID has been registered with the PushServer.
registered: false,
// Each successfully registered channelID is used as a key to hold its pushEndpoint URL.
registeredChannels: {},
_channelsToRegister: {},
_minRetryDelay_ms: (() => {
try {
@ -50,34 +52,78 @@ let MozLoopPushHandler = {
})(),
/**
* Starts a connection to the push socket server. On
* Inializes the PushHandler and opens a socket with the PushServer.
* It will automatically say hello and register any channels
* that are found in the work queue at that point.
*
* @param {Object} options Set of configuration options. Currently,
* the only option is mocketWebSocket which will be
* used for testing.
*/
initialize: function(options = {}) {
if (Services.io.offline) {
console.warn("MozLoopPushHandler - IO offline");
return false;
}
if (this._initDone) {
return true;
}
this._initDone = true;
if ("mockWebSocket" in options) {
this._mockWebSocket = options.mockWebSocket;
}
this._openSocket();
return true;
},
/**
* Start registration of a PushServer notification channel.
* connection, it will automatically say hello and register the channel
* id with the server.
*
* Register callback parameters:
* onRegistered callback parameters:
* - {String|null} err: Encountered error, if any
* - {String} url: The push url obtained from the server
*
* Callback parameters:
* onNotification parameters:
* - {String} version The version string received from the push server for
* the notification.
* - {String} channelID The channelID on which the notification was sent.
*
* @param {Function} registerCallback Callback to be called once we are
* @param {String} channelID Channel ID to use in registration.
*
* @param {Function} onRegistered Callback to be called once we are
* registered.
* @param {Function} notificationCallback Callback to be called when a
* @param {Function} onNotification Callback to be called when a
* push notification is received (may be called multiple
* times).
* @param {Object} mockPushHandler Optional, test-only object, to allow
* the websocket to be mocked for tests.
*/
initialize: function(registerCallback, notificationCallback, mockPushHandler) {
if (mockPushHandler) {
this._mockPushHandler = mockPushHandler;
register: function(channelID, onRegistered, onNotification) {
if (!channelID || !onRegistered || !onNotification) {
throw new Error("missing required parameter(s):"
+ (channelID ? "" : " channelID")
+ (onRegistered ? "" : " onRegistered")
+ (onNotification ? "" : " onNotification"));
}
// Only register new channels
if (!(channelID in this.channels)) {
this.channels[channelID] = {
onRegistered: onRegistered,
onNotification: onNotification
};
this._registerCallback = registerCallback;
this._notificationCallback = notificationCallback;
this._openSocket();
// If registration is in progress, simply add to the work list.
// Else, re-start a registration cycle.
if (this._registrationID) {
this._channelsToRegister.push(channelID);
} else {
this._registerChannels();
}
}
},
/**
@ -91,9 +137,11 @@ let MozLoopPushHandler = {
// If a uaID has already been assigned, assume this is a re-connect
// and send the uaID in order to re-synch with the
// PushServer. If a registration has been completed, send the channelID.
let helloMsg = { messageType: "hello",
uaid: this.uaID,
channelIDs: this.registered ? [this.channelID] :[] };
let helloMsg = {
messageType: "hello",
uaid: this.uaID || "",
channelIDs: Object.keys(this.registeredChannels)};
this._retryOperation(() => this.onStart(), this._maxRetryDelay_ms);
try { // in case websocket has closed before this handler is run
this._websocket.sendMsg(JSON.stringify(helloMsg));
@ -138,10 +186,12 @@ let MozLoopPushHandler = {
switch(msg.messageType) {
case "hello":
this._retryEnd();
if (this.uaID !== msg.uaid) {
this.uaID = msg.uaid;
this._registerChannel();
}
this._isConnected = true;
if (this.uaID !== msg.uaid) {
this.uaID = msg.uaid;
this.registeredChannels = {};
this._registerChannels();
}
break;
case "register":
@ -150,8 +200,8 @@ let MozLoopPushHandler = {
case "notification":
msg.updates.forEach((update) => {
if (update.channelID === this.channelID) {
this._notificationCallback(update.version);
if (update.channelID in this.registeredChannels) {
this.channels[update.channelID].onNotification(update.version, update.channelID);
}
});
break;
@ -161,30 +211,40 @@ let MozLoopPushHandler = {
/**
* Handles the PushServer registration response.
*
* @param {} msg PushServer to UserAgent registration response (parsed from JSON).
* @param {Object} msg PushServer to UserAgent registration response (parsed from JSON).
*/
_onRegister: function(msg) {
let registerNext = () => {
this._registrationID = this._channelsToRegister.shift();
this._sendRegistration(this._registrationID);
}
switch (msg.status) {
case 200:
this._retryEnd(); // reset retry mechanism
this.registered = true;
if (this.pushUrl !== msg.pushEndpoint) {
this.pushUrl = msg.pushEndpoint;
this._registerCallback(null, this.pushUrl);
if (msg.channelID == this._registrationID) {
this._retryEnd(); // reset retry mechanism
this.registeredChannels[msg.channelID] = msg.pushEndpoint;
this.channels[msg.channelID].onRegistered(null, msg.pushEndpoint, msg.channelID);
registerNext();
}
break;
case 500:
// retry the registration request after a suitable delay
this._retryOperation(() => this._registerChannel());
this._retryOperation(() => this._sendRegistration(msg.channelID));
break;
case 409:
this._registerCallback("error: PushServer ChannelID already in use");
this.channels[this._registrationID].onRegistered(
"error: PushServer ChannelID already in use: " + msg.channelID);
registerNext();
break;
default:
this._registerCallback("error: PushServer registration failure, status = " + msg.status);
let id = this._channelsToRegister.shift();
this.channels[this._registrationID].onRegistered(
"error: PushServer registration failure, status = " + msg.status);
registerNext();
break;
}
},
@ -198,16 +258,14 @@ let MozLoopPushHandler = {
* is logically closed.
*/
_openSocket: function() {
if (this._mockPushHandler) {
this._isConnected = false;
if (this._mockWebSocket) {
// For tests, use the mock instance.
this._websocket = this._mockPushHandler;
} else if (!Services.io.offline) {
this._websocket = this._mockWebSocket;
} else {
this._websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
.createInstance(Ci.nsIWebSocketChannel);
} else {
this._registerCallback("offline");
console.warn("MozLoopPushHandler - IO offline");
return;
}
this._websocket.protocol = "push-notification";
@ -259,15 +317,39 @@ let MozLoopPushHandler = {
},
/**
* Handles registering a service
* Begins registering the channelIDs with the PushServer
*/
_registerChannel: function() {
this.registered = false;
try { // in case websocket has closed
this._websocket.sendMsg(JSON.stringify({messageType: "register",
channelID: this.channelID}));
_registerChannels: function() {
// Hold off registration operation until handshake is complete.
if (!this._isConnected) {
return;
}
// If a registration is pending, do not generate a work list.
// Assume registration is in progress.
if (!this._registrationID) {
// Generate a list of channelIDs that have not yet been registered.
this._channelsToRegister = Object.keys(this.channels).filter((id) => {
return !(id in this.registeredChannels);
});
this._registrationID = this._channelsToRegister.shift();
this._sendRegistration(this._registrationID);
}
},
/**
* Handles registering a service
*
* @param {string} channelID - identification token to use in registration for this channel.
*/
_sendRegistration: function(channelID) {
if (channelID) {
try { // in case websocket has closed
this._websocket.sendMsg(JSON.stringify({messageType: "register",
channelID: channelID}));
}
catch (e) {console.warn("MozLoopPushHandler::_registerChannel websocket.sendMsg() failure");}
}
catch (e) {console.warn("MozLoopPushHandler::_registerChannel websocket.sendMsg() failure");}
},
/**
@ -301,4 +383,3 @@ let MozLoopPushHandler = {
}
}
};

View File

@ -31,6 +31,7 @@ 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"];
@ -64,6 +65,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopCalls",
"resource:///modules/loop/LoopCalls.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
"resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
@ -108,160 +115,6 @@ 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
*
@ -270,7 +123,6 @@ CallProgressSocket.prototype = {
* and register with the Loop server.
*/
let MozLoopServiceInternal = {
callsData: {inUse: false},
_mocks: {webSocket: undefined},
// The uri of the Loop server.
@ -454,24 +306,73 @@ let MozLoopServiceInternal = {
*
* @param {Object} mockPushHandler Optional, test-only mock push handler. Used
* to allow mocking of the MozLoopPushHandler.
* @param {Object} mockWebSocket Optional, test-only mock webSocket. To be passed
* through to 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;
}
this._mocks.webSocket = mockWebSocket;
this._mocks.pushHandler = mockPushHandler;
// Wrap push notification registration call-back in a Promise.
let registerForNotification = function(channelID, onNotification) {
return new Promise((resolve, reject) => {
let onRegistered = (error, pushUrl) => {
if (error) {
reject(Error(error));
} else {
resolve(pushUrl);
}
};
gPushHandler.register(channelID, onRegistered, onNotification);
});
};
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));
let options = mockWebSocket ? {mockWebSocket: mockWebSocket} : {};
gPushHandler.initialize(options);
let callsRegGuest = registerForNotification(LoopCalls.channelIDs.Guest,
LoopCalls.onNotification);
let roomsRegGuest = registerForNotification(LoopRooms.channelIDs.Guest,
LoopRooms.onNotification);
let callsRegFxA = registerForNotification(LoopCalls.channelIDs.FxA,
LoopCalls.onNotification);
let roomsRegFxA = registerForNotification(LoopRooms.channelIDs.FxA,
LoopRooms.onNotification);
Promise.all([callsRegGuest, roomsRegGuest, callsRegFxA, roomsRegFxA])
.then((pushUrls) => {
return this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST,
{calls: pushUrls[0], rooms: pushUrls[1]}) })
.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;
});
return result;
},
@ -606,53 +507,23 @@ let MozLoopServiceInternal = {
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 {String} pushUrls 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})
registerWithLoopServer: function(sessionType, pushUrls, retry = true) {
return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURLs: pushUrls})
.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))
if (!this.storeSessionToken(sessionType, response.headers)) {
return;
}
log.debug("Successfully registered with server for sessionType", sessionType);
this.clearError("registration");
@ -662,7 +533,7 @@ let MozLoopServiceInternal = {
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);
return this.registerWithLoopServer(sessionType, pushUrls, false);
}
}
@ -681,8 +552,12 @@ let MozLoopServiceInternal = {
* This is normally only wanted for FxA users as we normally want to keep the
* guest session with the device.
*
* NOTE: It is the responsibiliy of the caller the clear the session token
* after all of the notification classes: calls and rooms, for either
* Guest or FxA have been unregistered with the LoopServer.
*
* @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.
* @param {String} pushURLs 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
*/
@ -696,11 +571,8 @@ let MozLoopServiceInternal = {
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;
@ -711,167 +583,6 @@ let MozLoopServiceInternal = {
});
},
/**
* 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 {String} conversationType Whether or not the call is "incoming"
* or "outgoing"
*/
_startCall: function(callData, conversationType) {
const openChat = () => {
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);
};
if (conversationType == "incoming" && ("callerId" in callData) &&
EMAIL_OR_PHONE_RE.test(callData.callerId)) {
LoopContacts.search({
q: callData.callerId,
field: callData.callerId.contains("@") ? "email" : "tel"
}, (err, contacts) => {
if (err) {
// Database error, helas!
openChat();
return;
}
for (let contact of contacts) {
if (contact.blocked) {
// Blocked! Send a busy signal back to the caller.
this._returnBusy(callData);
return;
}
}
openChat();
})
} else {
openChat();
}
},
/**
* 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.
@ -1167,8 +878,11 @@ let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSock
log.debug("MozLoopService: Initializing with already logged-in account");
let registeredPromise =
MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA,
gPushHandler.pushUrl);
MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {
calls: gPushHandler.registeredChannels[LoopCalls.channelIDs.FxA],
rooms: gPushHandler.registeredChannels[LoopRooms.channelIDs.FxA]
});
registeredPromise.then(() => {
deferredInitialization.resolve("initialized to logged-in status");
}, error => {
@ -1238,6 +952,18 @@ this.MozLoopService = {
});
}),
/**
* 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) {
MozLoopServiceInternal.openChatWindow(contentWindow, title, url);
},
/**
* 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.
@ -1457,39 +1183,6 @@ this.MozLoopService = {
}
},
/**
* 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.".
*
@ -1565,7 +1258,6 @@ this.MozLoopService = {
if (MozLoopServiceInternal.fxAOAuthTokenData) {
return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
}
return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
}).then(tokenData => {
@ -1573,10 +1265,13 @@ this.MozLoopService = {
return tokenData;
}).then(tokenData => {
return gRegisteredDeferred.promise.then(Task.async(function*() {
if (gPushHandler.pushUrl) {
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
let callsUrl = gPushHandler.registeredChannels[LoopCalls.channelIDs.FxA],
roomsUrl = gPushHandler.registeredChannels[LoopRooms.channelIDs.FxA];
if (callsUrl && roomsUrl) {
yield MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {calls: callsUrl, rooms: roomsUrl});
} else {
throw new Error("No pushUrl for FxA registration");
throw new Error("No pushUrls for FxA registration");
}
MozLoopServiceInternal.clearError("login");
MozLoopServiceInternal.clearError("profile");
@ -1617,10 +1312,23 @@ this.MozLoopService = {
*/
logOutFromFxA: Task.async(function*() {
log.debug("logOutFromFxA");
if (gPushHandler && gPushHandler.pushUrl) {
yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA,
gPushHandler.pushUrl);
} else {
let callsPushUrl, roomsPushUrl;
if (gPushHandler) {
callsPushUrl = gPushHandler.registeredChannels[LoopCalls.channelIDs.FxA];
roomsPushUrl = gPushHandler.registeredChannels[LoopRooms.channelIDs.FxA];
}
try {
if (callsPushUrl) {
yield MozLoopServiceInternal.unregisterFromLoopServer(
LOOP_SESSION_TYPE.FXA, callsPushUrl);
}
if (roomsPushUrl) {
yield MozLoopServiceInternal.unregisterFromLoopServer(
LOOP_SESSION_TYPE.FXA, roomsPushUrl);
}
}
catch (error) {throw error}
finally {
MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
}
@ -1646,7 +1354,6 @@ this.MozLoopService = {
log.error("Could not get the OAuth client");
return;
}
let url = new URL("/settings", fxAOAuthClient.parameters.content_uri);
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.switchToTabHavingURI(url.toString(), true);
@ -1674,15 +1381,4 @@ this.MozLoopService = {
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);
},
};

View File

@ -15,7 +15,9 @@ BROWSER_CHROME_MANIFESTS += [
EXTRA_JS_MODULES.loop += [
'CardDavImporter.jsm',
'GoogleImporter.jsm',
'LoopCalls.jsm',
'LoopContacts.jsm',
'LoopRooms.jsm',
'LoopStorage.jsm',
'MozLoopAPI.jsm',
'MozLoopPushHandler.jsm',

View File

@ -122,7 +122,7 @@ add_task(function* params_no_hawk_session() {
});
add_task(function* params_nonJSON() {
Services.prefs.setCharPref("loop.server", "https://loop.invalid");
Services.prefs.setCharPref("loop.server", "https://localhost:3000/invalid");
// Reset after changing the server so a new HawkClient is created
yield resetFxA();
@ -248,7 +248,7 @@ add_task(function* basicAuthorizationAndRegistration() {
yield promiseOAuthParamsSetup(BASE_URL, params);
info("registering");
mockPushHandler.pushUrl = "https://localhost/pushUrl/guest";
mockPushHandler.registrationPushURL = "https://localhost/pushUrl/guest";
// Notification observed due to the error being cleared upon successful registration.
let statusChangedPromise = promiseObserverNotified("loop-status-changed");
yield MozLoopService.register(mockPushHandler);
@ -256,7 +256,8 @@ add_task(function* basicAuthorizationAndRegistration() {
// Normally the same pushUrl would be registered but we change it in the test
// to be able to check for success on the second registration.
mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa";
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = "https://localhost/pushUrl/fxa-calls";
mockPushHandler.registeredChannels[LoopRooms.channelIDs.FxA] = "https://localhost/pushUrl/fxa-rooms";
statusChangedPromise = promiseObserverNotified("loop-status-changed");
yield loadLoopPanel({loopURL: BASE_URL, stayOnline: true});
@ -268,6 +269,7 @@ add_task(function* basicAuthorizationAndRegistration() {
let loopButton = document.getElementById("loop-call-button");
is(loopButton.getAttribute("state"), "", "state of loop button should be empty when not logged in");
info("Login");
let tokenData = yield MozLoopService.logInToFxA();
yield promiseObserverNotified("loop-status-changed", "login");
ise(tokenData.access_token, "code1_access_token", "Check access_token");
@ -280,7 +282,9 @@ add_task(function* basicAuthorizationAndRegistration() {
is(loopButton.getAttribute("state"), "active", "state of loop button should be active when logged in");
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa",
ise(registrationResponse.response.simplePushURLs.calls, "https://localhost/pushUrl/fxa-calls",
"Check registered push URL");
ise(registrationResponse.response.simplePushURLs.rooms, "https://localhost/pushUrl/fxa-rooms",
"Check registered push URL");
let loopPanel = document.getElementById("loop-notification-panel");
@ -329,16 +333,17 @@ add_task(function* loginWithParams401() {
add_task(function* logoutWithIncorrectPushURL() {
yield resetFxA();
let pushURL = "http://www.example.com/";
mockPushHandler.pushUrl = pushURL;
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = pushURL;
mockPushHandler.registeredChannels[LoopRooms.channelIDs.FxA] = pushURL;
// Create a fake FxA hawk session token
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, pushURL);
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, {calls: pushURL});
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL");
mockPushHandler.pushUrl = "http://www.example.com/invalid";
ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL");
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = "http://www.example.com/invalid";
let caught = false;
yield MozLoopService.logOutFromFxA().catch((error) => {
caught = true;
@ -346,26 +351,27 @@ add_task(function* logoutWithIncorrectPushURL() {
ok(caught, "Should have caught an error logging out with a mismatched push URL");
checkLoggedOutState();
registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL wasn't deleted");
ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL wasn't deleted");
});
add_task(function* logoutWithNoPushURL() {
yield resetFxA();
let pushURL = "http://www.example.com/";
mockPushHandler.pushUrl = pushURL;
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = pushURL;
// Create a fake FxA hawk session token
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, pushURL);
yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, {calls: pushURL});
let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL");
mockPushHandler.pushUrl = null;
ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL");
mockPushHandler.registeredChannels[LoopCalls.channelIDs.FxA] = null;
mockPushHandler.registeredChannels[LoopRooms.channelIDs.FxA] = null;
yield MozLoopService.logOutFromFxA();
checkLoggedOutState();
registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
ise(registrationResponse.response.simplePushURL, pushURL, "Check registered push URL wasn't deleted");
ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL wasn't deleted");
});
add_task(function* loginWithRegistration401() {

View File

@ -6,6 +6,8 @@ const {
LOOP_SESSION_TYPE,
MozLoopServiceInternal,
} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
const {LoopCalls} = Cu.import("resource:///modules/loop/LoopCalls.jsm", {});
const {LoopRooms} = Cu.import("resource:///modules/loop/LoopRooms.jsm", {});
// Cache this value only once, at the beginning of a
// test run, so that it doesn't pick up the offline=true
@ -120,6 +122,7 @@ function* resetFxA() {
global.gHawkClient = null;
global.gFxAOAuthClientPromise = null;
global.gFxAOAuthClient = null;
global.gRegisteredDeferred = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthTokenData = null;
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
@ -196,21 +199,30 @@ let mockPushHandler = {
// This sets the registration result to be returned when initialize
// is called. By default, it is equivalent to success.
registrationResult: null,
pushUrl: undefined,
registrationPushURL: null,
notificationCallback: {},
registeredChannels: {},
/**
* MozLoopPushHandler API
*/
initialize: function(registerCallback, notificationCallback) {
registerCallback(this.registrationResult, this.pushUrl);
this._notificationCallback = notificationCallback;
initialize: function(options = {}) {
if ("mockWebSocket" in options) {
this._mockWebSocket = options.mockWebSocket;
}
},
register: function(channelId, registerCallback, notificationCallback) {
this.notificationCallback[channelId] = notificationCallback;
this.registeredChannels[channelId] = this.registrationPushURL;
setTimeout(registerCallback(this.registrationResult, this.registrationPushURL, channelId), 0);
},
/**
* Test-only API to simplify notifying a push notification result.
*/
notify: function(version) {
this._notificationCallback(version);
notify: function(version, chanId) {
this.notificationCallback[chanId](version, chanId);
}
};

View File

@ -197,9 +197,11 @@ function registration(request, response) {
let body = NetUtil.readInputStreamToString(request.bodyInputStream,
request.bodyInputStream.available());
let payload = JSON.parse(body);
if (payload.simplePushURL == "https://localhost/pushUrl/fxa" &&
(!request.hasHeader("Authorization") ||
!request.getHeader("Authorization").startsWith("Hawk"))) {
if ((payload.simplePushURL == "https://localhost/pushUrl/fxa" ||
payload.simplePushURLs.calls == "https://localhost/pushUrl/fxa-calls" ||
payload.simplePushURLs.rooms == "https://localhost/pushUrl/fxa-rooms") &&
(!request.hasHeader("Authorization") ||
!request.getHeader("Authorization").startsWith("Hawk"))) {
response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
response.write("401 Missing Hawk Authorization header");
return;
@ -224,11 +226,14 @@ function delete_registration(request, response) {
// making the path become a query parameter. This is because we aren't actually
// registering endpoints at the root of the hostname e.g. /registration.
let url = new URL(request.queryString.replace(/%3F.*/,""), "http://www.example.com");
let registration = JSON.parse(getSharedState("/registration"));
if (registration.simplePushURL == url.searchParams.get("simplePushURL")) {
setSharedState("/registration", "");
} else {
response.setStatusLine(request.httpVersion, 400, "Bad Request");
let state = getSharedState("/registration");
if (state != "") { //Already set to empty value on a successful channel unregsitration.
let registration = JSON.parse(state);
if (registration.simplePushURLs.calls == url.searchParams.get("simplePushURL")) {
setSharedState("/registration", "");
} else {
response.setStatusLine(request.httpVersion, 400, "Bad Request");
}
}
}

View File

@ -9,6 +9,7 @@ Cu.import("resource://gre/modules/Http.jsm");
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource:///modules/loop/MozLoopService.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource:///modules/loop/LoopCalls.jsm");
const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
@ -18,6 +19,7 @@ const kMockWebSocketChannelName = "Mock WebSocket Channel";
const kWebSocketChannelContractID = "@mozilla.org/network/protocol;1?name=wss";
const kServerPushUrl = "http://localhost:3456";
const kLoopServerUrl = "http://localhost:3465";
const kEndPointUrl = "http://example.com/fake";
const kUAID = "f47ac11b-58ca-4372-9567-0e02b2c3d479";
@ -75,21 +77,30 @@ let mockPushHandler = {
// This sets the registration result to be returned when initialize
// is called. By default, it is equivalent to success.
registrationResult: null,
registrationPushURL: undefined,
registrationPushURL: null,
notificationCallback: {},
registeredChannels: {},
/**
* MozLoopPushHandler API
*/
initialize: function(registerCallback, notificationCallback) {
registerCallback(this.registrationResult, this.registrationPushURL);
this._notificationCallback = notificationCallback;
initialize: function(options = {}) {
if ("mockWebSocket" in options) {
this._mockWebSocket = options.mockWebSocket;
}
},
register: function(channelId, registerCallback, notificationCallback) {
this.notificationCallback[channelId] = notificationCallback;
this.registeredChannels[channelId] = this.registrationPushURL;
registerCallback(this.registrationResult, this.registrationPushURL, channelId);
},
/**
* Test-only API to simplify notifying a push notification result.
*/
notify: function(version) {
this._notificationCallback(version);
notify: function(version, chanId) {
this.notificationCallback[chanId](version, chanId);
}
};
@ -98,9 +109,8 @@ let mockPushHandler = {
* enables us to check parameters and return messages similar to the push
* server.
*/
let MockWebSocketChannel = function(options) {
let _options = options || {};
this.defaultMsgHandler = _options.defaultMsgHandler;
let MockWebSocketChannel = function(options = {}) {
this.defaultMsgHandler = options.defaultMsgHandler;
};
MockWebSocketChannel.prototype = {

View File

@ -1,24 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
{
add_test(function test_initalize_offline() {
Services.io.offline = true;
MozLoopPushHandler.initialize(function(err) {
Assert.equal(err, "offline", "Should error with 'offline' when offline");
Services.io.offline = false;
run_next_test();
});
});
let dummyCallback = () => {};
let mockWebSocket = new MockWebSocketChannel();
add_test(function test_initalize_offline() {
Services.io.offline = true;
do_check_false(MozLoopPushHandler.initialize());
Services.io.offline = false;
MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
run_next_test();
});
add_test(function test_initalize_missing_chanid() {
Assert.throws(() => {MozLoopPushHandler.register(null, dummyCallback, dummyCallback)});
run_next_test();
});
add_test(function test_initalize_missing_regcallback() {
Assert.throws(() => {MozLoopPushHandler.register("chan-1", null, dummyCallback)});
run_next_test();
});
add_test(function test_initalize_missing_notifycallback() {
Assert.throws(() => {MozLoopPushHandler.register("chan-1", dummyCallback, null)});
run_next_test();
});
add_test(function test_initalize_websocket() {
MozLoopPushHandler.initialize(
function(err, url) {
MozLoopPushHandler.register(
"chan-1",
function(err, url, id) {
Assert.equal(err, null, "Should return null for success");
Assert.equal(url, kEndPointUrl, "Should return push server application URL");
Assert.equal(id, "chan-1", "Should have channel id = chan-1");
Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
"Should have the url from preferences");
Assert.equal(mockWebSocket.origin, kServerPushUrl,
@ -27,28 +42,29 @@
"Should have the protocol set to push-notifications");
mockWebSocket.notify(15);
},
function(version) {
function(version, id) {
Assert.equal(version, 15, "Should have version number 15");
Assert.equal(id, "chan-1", "Should have channel id = chan-1");
run_next_test();
},
},
mockWebSocket);
});
add_test(function test_reconnect_websocket() {
MozLoopPushHandler.uaID = undefined;
MozLoopPushHandler.pushUrl = undefined; //Do this to force a new registration callback.
MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
mockWebSocket.stop();
});
add_test(function test_reopen_websocket() {
MozLoopPushHandler.uaID = undefined;
MozLoopPushHandler.pushUrl = undefined; //Do this to force a new registration callback.
MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
mockWebSocket.serverClose();
});
add_test(function test_retry_registration() {
MozLoopPushHandler.uaID = undefined;
MozLoopPushHandler.pushUrl = undefined; //Do this to force a new registration callback.
MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
mockWebSocket.initRegStatus = 500;
mockWebSocket.stop();
});
@ -57,6 +73,7 @@
setupFakeLoopServer();
Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl);
Services.prefs.setCharPref("loop.server", kLoopServerUrl);
Services.prefs.setIntPref("loop.retry_delay.start", 10); // 10 ms
Services.prefs.setIntPref("loop.retry_delay.limit", 20); // 20 ms

View File

@ -2,6 +2,8 @@
* 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/. */
const { LoopCallsInternal } = Cu.import("resource:///modules/loop/LoopCalls.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
"resource:///modules/Chat.jsm");
@ -10,20 +12,22 @@ let openChatOrig = Chat.open;
const firstCallId = 4444333221;
const secondCallId = 1001100101;
function test_send_busy_on_call() {
let actionReceived = false;
let msgHandler = function(msg) {
if (msg.messageType &&
msg.messageType === "action" &&
msg.event === "terminate" &&
msg.reason === "busy") {
actionReceived = true;
}
}
let msgHandler = function(msg) {
if (msg.messageType &&
msg.messageType === "action" &&
msg.event === "terminate" &&
msg.reason === "busy") {
actionReceived = true;
}
};
let mockWebSocket = new MockWebSocketChannel({defaultMsgHandler: msgHandler});
LoopCallsInternal._mocks.webSocket = mockWebSocket;
let mockWebSocket = new MockWebSocketChannel({defaultMsgHandler: msgHandler});
Services.io.offline = false;
Services.io.offline = false;
add_test(function test_busy_2guest_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
let opened = 0;
@ -31,22 +35,90 @@ function test_send_busy_on_call() {
opened++;
};
mockPushHandler.notify(1);
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
MozLoopService.releaseCallData(firstCallId);
LoopCalls.releaseCallData(firstCallId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
});
});
}
});
add_test(test_send_busy_on_call); //FXA call accepted, Guest call rejected
add_test(test_send_busy_on_call); //No FXA call, first Guest call accepted, second rejected
add_test(function test_busy_1fxa_1guest_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
let opened = 0;
Chat.open = function() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.FxA);
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
});
});
});
add_test(function test_busy_2fxa_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
let opened = 0;
Chat.open = function() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.FxA);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
});
});
});
add_test(function test_busy_1guest_1fxa_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
let opened = 0;
Chat.open = function() {
opened++;
};
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
mockPushHandler.notify(1, LoopCalls.channelIDs.FxA);
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
});
});
});
function run_test()
{
@ -64,20 +136,18 @@ function run_test()
let callsRespCount = 0;
let callsResponses = [
{calls: [{callId: firstCallId,
websocketToken: "0deadbeef0",
progressURL: "wss://localhost:5000/websocket"}]},
{calls: [{callId: secondCallId,
websocketToken: "1deadbeef1",
progressURL: "wss://localhost:5000/websocket"}]},
{calls: []},
{calls: [{callId: firstCallId,
websocketToken: "0deadbeef0",
progressURL: "wss://localhost:5000/websocket"},
{callId: secondCallId,
websocketToken: "1deadbeef1",
progressURL: "wss://localhost:5000/websocket"}]},
{calls: [{callId: firstCallId,
websocketToken: "0deadbeef0",
progressURL: "wss://localhost:5000/websocket"}]},
{calls: [{callId: secondCallId,
websocketToken: "1deadbeef1",
progressURL: "wss://localhost:5000/websocket"}]},
];
loopServer.registerPathHandler("/registration", (request, response) => {
@ -107,6 +177,8 @@ function run_test()
// clear test pref
Services.prefs.clearUserPref("loop.seenToS");
LoopCallsInternal._mocks.webSocket = undefined;
});
run_next_test();

View File

@ -21,13 +21,13 @@ add_task(function test_startDirectCall_opens_window() {
openedUrl = url;
};
MozLoopService.startDirectCall(contact, "audio-video");
LoopCalls.startDirectCall(contact, "audio-video");
do_check_true(!!openedUrl, "should open a chat window");
// Stop the busy kicking in for following tests.
let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
MozLoopService.releaseCallData(callId);
LoopCalls.releaseCallData(callId);
});
add_task(function test_startDirectCall_getCallData() {
@ -36,17 +36,17 @@ add_task(function test_startDirectCall_getCallData() {
openedUrl = url;
};
MozLoopService.startDirectCall(contact, "audio-video");
LoopCalls.startDirectCall(contact, "audio-video");
let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
let callData = MozLoopService.getCallData(callId);
let callData = LoopCalls.getCallData(callId);
do_check_eq(callData.callType, "audio-video", "should have the correct call type");
do_check_eq(callData.contact, contact, "should have the contact details");
// Stop the busy kicking in for following tests.
MozLoopService.releaseCallData(callId);
LoopCalls.releaseCallData(callId);
});
function run_test() {

View File

@ -37,7 +37,7 @@ add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
opened = true;
};
mockPushHandler.notify(1);
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
waitForCondition(function() opened).then(() => {
run_next_test();
@ -56,7 +56,7 @@ add_test(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
opened = true;
};
mockPushHandler.notify(1);
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
do_timeout(500, function() {
do_check_false(opened, "should not open a chat window");

View File

@ -16,7 +16,7 @@ add_test(function test_openChatWindow_on_notification() {
opened = true;
};
mockPushHandler.notify(1);
mockPushHandler.notify(1, LoopCalls.channelIDs.Guest);
waitForCondition(function() opened).then(() => {
do_check_true(opened, "should open a chat window");

View File

@ -10,37 +10,19 @@ Cu.import("resource://services-common/utils.js");
* other test files.
*/
/**
* Tests reported failures when we're in offline mode.
*/
add_test(function test_register_offline() {
mockPushHandler.registrationResult = "offline";
// It should callback with failure if in offline mode
Services.io.offline = true;
MozLoopService.register(mockPushHandler).then(() => {
do_throw("should not succeed when offline");
}, err => {
Assert.equal(err, "offline", "should reject with 'offline' when offline");
Services.io.offline = false;
run_next_test();
});
});
/**
* Test that the websocket can be fully registered, and that a Loop server
* failure is reported.
*/
add_test(function test_register_websocket_success_loop_server_fail() {
mockPushHandler.registrationResult = null;
mockPushHandler.registrationResult = "404";
MozLoopService.register(mockPushHandler).then(() => {
do_throw("should not succeed when loop server registration fails");
}, err => {
}, (err) => {
// 404 is an expected failure indicated by the lack of route being set
// up on the Loop server mock. This is added in the next test.
Assert.equal(err.errno, 404, "Expected no errors in websocket registration");
Assert.equal(err.message, "404", "Expected no errors in websocket registration");
run_next_test();
});
@ -50,14 +32,18 @@ add_test(function test_register_websocket_success_loop_server_fail() {
* Tests that we get a success response when both websocket and Loop server
* registration are complete.
*/
add_test(function test_register_success() {
mockPushHandler.registrationPushURL = kEndPointUrl;
mockPushHandler.registrationResult = null;
loopServer.registerPathHandler("/registration", (request, response) => {
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
let data = JSON.parse(body);
Assert.equal(data.simplePushURL, kEndPointUrl,
"Should send correct push url");
Assert.equal(data.simplePushURLs.calls, kEndPointUrl,
"Should send correct calls push url");
Assert.equal(data.simplePushURLs.rooms, kEndPointUrl,
"Should send correct rooms push url");
response.setStatusLine(null, 200, "OK");
response.processAsync();