mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1020876 Route desktop client XHRs though the mozLoop API to share hawk implementation with MozLoopService. r=ttaubert
--HG-- rename : browser/components/loop/content/shared/js/client.js => browser/components/loop/content/js/client.js rename : browser/components/loop/test/shared/client_test.js => browser/components/loop/test/desktop-local/client_test.js
This commit is contained in:
parent
ec8cfcf01a
commit
06898007b2
@ -42,19 +42,6 @@ function injectLoopAPI(targetWindow) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the url for the Loop server from preferences.
|
||||
*
|
||||
* @return {String} The Loop server url
|
||||
*/
|
||||
serverUrl: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return Services.prefs.getCharPref("loop.server");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current locale of the browser.
|
||||
*
|
||||
@ -214,7 +201,40 @@ function injectLoopAPI(targetWindow) {
|
||||
ringer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a hawk based request to the loop server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - {Object|null} null if success. Otherwise an object:
|
||||
* {
|
||||
* code: 401,
|
||||
* errno: 401,
|
||||
* error: "Request failed",
|
||||
* message: "invalid token"
|
||||
* }
|
||||
* - {String} The body of the response.
|
||||
*
|
||||
* @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.
|
||||
* @param {Function} callback Called when the request completes.
|
||||
*/
|
||||
hawkRequest: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(path, method, payloadObj, callback) {
|
||||
// XXX Should really return a DOM promise here.
|
||||
return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => {
|
||||
callback(null, response.body);
|
||||
}, (error) => {
|
||||
callback(Cu.cloneInto(error, targetWindow));
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let contentObj = Cu.createObjectIn(targetWindow);
|
||||
|
@ -147,6 +147,11 @@ let MozLoopServiceInternal = {
|
||||
* @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(path, method, payloadObj) {
|
||||
if (!this._hawkClient) {
|
||||
@ -481,5 +486,22 @@ this.MozLoopService = {
|
||||
"; exception: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a hawk based request to the loop server.
|
||||
*
|
||||
* @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(path, method, payloadObj) {
|
||||
return MozLoopServiceInternal.hawkRequest(path, method, payloadObj);
|
||||
},
|
||||
};
|
||||
|
@ -21,14 +21,11 @@
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/token.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
||||
</body>
|
||||
|
189
browser/components/loop/content/js/client.js
Normal file
189
browser/components/loop/content/js/client.js
Normal file
@ -0,0 +1,189 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.Client = (function($) {
|
||||
"use strict";
|
||||
|
||||
// The expected properties to be returned from the POST /call-url/ request.
|
||||
const expectedCallUrlProperties = ["call_url", "expiresAt"];
|
||||
|
||||
// The expected properties to be returned from the GET /calls request.
|
||||
const expectedCallProperties = ["calls"];
|
||||
|
||||
/**
|
||||
* Loop server client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function Client(settings = {}) {
|
||||
|
||||
// allowing an |in| test rather than a more type || allows us to dependency
|
||||
// inject a non-existent mozLoop
|
||||
if ("mozLoop" in settings) {
|
||||
this.mozLoop = settings.mozLoop;
|
||||
} else {
|
||||
this.mozLoop = navigator.mozLoop;
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
Client.prototype = {
|
||||
/**
|
||||
* Converts from hours to seconds
|
||||
*/
|
||||
_hoursToSeconds: function(value) {
|
||||
return value * 60 * 60;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length == 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param {Object} error See MozLoopAPI.hawkRequest
|
||||
*/
|
||||
_failureHandler: function(cb, error) {
|
||||
var message = "HTTP " + error.code + " " + error.error + "; " + error.message;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the client is registered with the push server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
*/
|
||||
_ensureRegistered: function(cb) {
|
||||
this.mozLoop.ensureRegistered(cb);
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal handler for requesting a call url from the server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - callUrlData an object of the obtained call url data if successful:
|
||||
* -- call_url: The url of the call
|
||||
* -- expiresAt: The amount of hours until expiry of the url
|
||||
*
|
||||
* @param {String} simplepushUrl a registered Simple Push URL
|
||||
* @param {string} nickname the nickname of the future caller
|
||||
* @param {Function} cb Callback(err, callUrlData)
|
||||
*/
|
||||
_requestCallUrlInternal: function(nickname, cb) {
|
||||
this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname},
|
||||
(error, responseText) => {
|
||||
if (error) {
|
||||
this._failureHandler(cb, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var urlData = JSON.parse(responseText);
|
||||
cb(null, this._validate(urlData, expectedCallUrlProperties));
|
||||
|
||||
var expiresHours = this._hoursToSeconds(urlData.expiresAt);
|
||||
this.mozLoop.noteCallUrlExpiry(expiresHours);
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests a call URL from the Loop server. It will note the
|
||||
* expiry time for the url with the mozLoop api.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - callUrlData an object of the obtained call url data if successful:
|
||||
* -- call_url: The url of the call
|
||||
* -- expiresAt: The amount of hours until expiry of the url
|
||||
*
|
||||
* @param {String} simplepushUrl a registered Simple Push URL
|
||||
* @param {string} nickname the nickname of the future caller
|
||||
* @param {Function} cb Callback(err, callUrlData)
|
||||
*/
|
||||
requestCallUrl: function(nickname, cb) {
|
||||
this._ensureRegistered(function(err) {
|
||||
if (err) {
|
||||
console.log("Error registering with Loop server, code: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
this._requestCallUrlInternal(nickname, cb);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests call information from the server for all calls since the
|
||||
* given version.
|
||||
*
|
||||
* @param {String} version the version identifier from the push
|
||||
* notification
|
||||
* @param {Function} cb Callback(err, calls)
|
||||
*/
|
||||
requestCallsInfo: function(version, cb) {
|
||||
// XXX It is likely that we'll want to move some of this to whatever
|
||||
// opens the chat window, but we'll need to decide on this in bug 1002418
|
||||
if (!version) {
|
||||
throw new Error("missing required parameter version");
|
||||
}
|
||||
|
||||
this.mozLoop.hawkRequest("/calls?version=" + version, "GET", null,
|
||||
(error, responseText) => {
|
||||
if (error) {
|
||||
this._failureHandler(cb, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var callsData = JSON.parse(responseText);
|
||||
|
||||
cb(null, this._validate(callsData, expectedCallProperties));
|
||||
} catch (err) {
|
||||
console.log("Error requesting calls info", err);
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return Client;
|
||||
})(jQuery);
|
@ -137,7 +137,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
accept: function() {
|
||||
window.navigator.mozLoop.stopAlerting();
|
||||
this._conversation.initiate({
|
||||
baseServerUrl: window.navigator.mozLoop.serverUrl,
|
||||
client: new loop.Client(),
|
||||
outgoing: false
|
||||
});
|
||||
},
|
||||
|
@ -91,9 +91,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
throw new Error("missing required notifier");
|
||||
}
|
||||
this.notifier = options.notifier;
|
||||
this.client = new loop.shared.Client({
|
||||
baseServerUrl: navigator.mozLoop.serverUrl
|
||||
});
|
||||
this.client = new loop.Client();
|
||||
},
|
||||
|
||||
getNickname: function() {
|
||||
|
@ -19,14 +19,11 @@
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/token.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/panel.js"></script>
|
||||
</body>
|
||||
|
@ -1,362 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.Client = (function($) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Loop server client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function Client(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.hasOwnProperty("baseServerUrl") ||
|
||||
!settings.baseServerUrl) {
|
||||
throw new Error("missing required baseServerUrl");
|
||||
}
|
||||
|
||||
// allowing an |in| test rather than a more type || allows us to dependency
|
||||
// inject a non-existent mozLoop
|
||||
if ("mozLoop" in settings) {
|
||||
this.mozLoop = settings.mozLoop;
|
||||
} else {
|
||||
this.mozLoop = navigator.mozLoop;
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
Client.prototype = {
|
||||
/**
|
||||
* Converts from hours to seconds
|
||||
*/
|
||||
_hoursToSeconds: function(value) {
|
||||
return value * 60 * 60;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length <= 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param jqXHR See jQuery docs
|
||||
* @param textStatus See jQuery docs
|
||||
* @param errorThrown See jQuery docs
|
||||
*/
|
||||
_failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
|
||||
var error = "Unknown error.",
|
||||
jsonRes = jqXHR && jqXHR.responseJSON || {};
|
||||
// Received error response format:
|
||||
// { "status": "errors",
|
||||
// "errors": [{
|
||||
// "location": "url",
|
||||
// "name": "token",
|
||||
// "description": "invalid token"
|
||||
// }]}
|
||||
if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
|
||||
error = "Details: " + jsonRes.errors.map(function(err) {
|
||||
return Object.keys(err).map(function(field) {
|
||||
return field + ": " + err[field];
|
||||
}).join(", ");
|
||||
}).join("; ");
|
||||
}
|
||||
var message = "HTTP " + jqXHR.status + " " + errorThrown +
|
||||
"; " + error;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the client is registered with the push server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
*/
|
||||
_ensureRegistered: function(cb) {
|
||||
navigator.mozLoop.ensureRegistered(function(err) {
|
||||
cb(err);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures that the client picks up the hawk-session-token
|
||||
* put in preferences by the LoopService registration code,
|
||||
* derives hawk credentials from them, and saves them in
|
||||
* this._credentials.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* if err is set to null in the callback, that indicates that the
|
||||
* credentials have been successfully attached to this object.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @note That as currently written, this is only ever expected to be called
|
||||
* from browser UI code (ie it relies on mozLoop).
|
||||
*/
|
||||
_ensureCredentials: function(cb) {
|
||||
if (this._credentials) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var hawkSessionToken =
|
||||
this.mozLoop.getLoopCharPref("hawk-session-token");
|
||||
if (!hawkSessionToken) {
|
||||
var msg = "loop.hawk-session-token pref not found";
|
||||
console.warn(msg);
|
||||
cb(new Error(msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX do we want to use any of the other hawk params (eg to track clock
|
||||
// skew, etc)?
|
||||
var serverDerivedKeyLengthInBytes = 2 * 32;
|
||||
deriveHawkCredentials(hawkSessionToken, "sessionToken",
|
||||
serverDerivedKeyLengthInBytes, function (hawkCredentials) {
|
||||
this._credentials = hawkCredentials;
|
||||
cb(null);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal handler for requesting a call url from the server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - callUrlData an object of the obtained call url data if successful:
|
||||
* -- call_url: The url of the call
|
||||
* -- expiresAt: The amount of hours until expiry of the url
|
||||
*
|
||||
* @param {String} simplepushUrl a registered Simple Push URL
|
||||
* @param {string} nickname the nickname of the future caller
|
||||
* @param {Function} cb Callback(err, callUrlData)
|
||||
*/
|
||||
_requestCallUrlInternal: function(nickname, cb) {
|
||||
var endpoint = this.settings.baseServerUrl + "/call-url/",
|
||||
reqData = {callerId: nickname};
|
||||
|
||||
var req = $.ajax({
|
||||
type: "POST",
|
||||
url: endpoint,
|
||||
data: reqData,
|
||||
xhrFields: {
|
||||
withCredentials: false
|
||||
},
|
||||
crossDomain: true,
|
||||
beforeSend: function (xhr, settings) {
|
||||
try {
|
||||
this._attachAnyServerCreds(xhr, settings);
|
||||
} catch (ex) {
|
||||
cb(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}.bind(this),
|
||||
success: function(callUrlData) {
|
||||
// XXX split this out into two functions for better readability
|
||||
try {
|
||||
cb(null, this._validate(callUrlData, ["call_url", "expiresAt"]));
|
||||
|
||||
var expiresHours = this._hoursToSeconds(callUrlData.expiresAt);
|
||||
navigator.mozLoop.noteCallUrlExpiry(expiresHours);
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests a call URL from the Loop server. It will note the
|
||||
* expiry time for the url with the mozLoop api.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - callUrlData an object of the obtained call url data if successful:
|
||||
* -- call_url: The url of the call
|
||||
* -- expiresAt: The amount of hours until expiry of the url
|
||||
*
|
||||
* @param {String} simplepushUrl a registered Simple Push URL
|
||||
* @param {string} nickname the nickname of the future caller
|
||||
* @param {Function} cb Callback(err, callUrlData)
|
||||
*/
|
||||
requestCallUrl: function(nickname, cb) {
|
||||
this._ensureRegistered(function(err) {
|
||||
if (err) {
|
||||
console.log("Error registering with Loop server, code: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._ensureCredentials(function (err) {
|
||||
if (err) {
|
||||
console.log("Error setting up credentials: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._requestCallUrlInternal(nickname, cb);
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests call information from the server for all calls since the
|
||||
* given version.
|
||||
*
|
||||
* @param {String} version the version identifier from the push
|
||||
* notification
|
||||
* @param {Function} cb Callback(err, calls)
|
||||
*/
|
||||
requestCallsInfo: function(version, cb) {
|
||||
this._ensureCredentials(function (err) {
|
||||
if (err) {
|
||||
console.log("Error setting up credentials: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._requestCallsInfoInternal(version, cb);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_requestCallsInfoInternal: function(version, cb) {
|
||||
if (!version) {
|
||||
throw new Error("missing required parameter version");
|
||||
}
|
||||
|
||||
var endpoint = this.settings.baseServerUrl + "/calls";
|
||||
|
||||
// XXX It is likely that we'll want to move some of this to whatever
|
||||
// opens the chat window, but we'll need to decide that once we make a
|
||||
// decision on chrome versus content, and know if we're going with
|
||||
// LoopService or a frameworker.
|
||||
var req = $.ajax({
|
||||
type: "GET",
|
||||
url: endpoint + "?version=" + version,
|
||||
xhrFields: {
|
||||
withCredentials: false
|
||||
},
|
||||
crossDomain: true,
|
||||
beforeSend: function (xhr, settings) {
|
||||
try {
|
||||
this._attachAnyServerCreds(xhr, settings);
|
||||
} catch (ex) {
|
||||
cb(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}.bind(this),
|
||||
success: function(callsData) {
|
||||
try {
|
||||
cb(null, this._validate(callsData, ["calls"]));
|
||||
} catch (err) {
|
||||
console.log("Error requesting calls info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts a call request to the server for a call represented by the
|
||||
* loopToken. Will return the session data for the call.
|
||||
*
|
||||
* @param {String} loopToken The loopToken representing the call
|
||||
* @param {Function} cb Callback(err, sessionData)
|
||||
*/
|
||||
requestCallInfo: function(loopToken, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("missing required parameter loopToken");
|
||||
}
|
||||
|
||||
var req = $.ajax({
|
||||
url: this.settings.baseServerUrl + "/calls/" + loopToken,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({}),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.done(function(sessionData) {
|
||||
try {
|
||||
cb(null, this._validate(sessionData, [
|
||||
"sessionId", "sessionToken", "apiKey"
|
||||
]));
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* If this._credentials is set, adds a hawk Authorization header based
|
||||
* based on those credentials to the passed-in XHR.
|
||||
*
|
||||
* @param xhr request to add any header to
|
||||
* @param settings settings object passed to jQuery.ajax()
|
||||
* @private
|
||||
*/
|
||||
_attachAnyServerCreds: function(xhr, settings) {
|
||||
// if the server needs credentials and didn't get them, it will
|
||||
// return failure for us, so if we don't have any creds, don't try to
|
||||
// attach them.
|
||||
if (!this._credentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
var header = hawk.client.header(settings.url, settings.type,
|
||||
{ credentials: this._credentials });
|
||||
if (header.err) {
|
||||
throw new Error(header.err);
|
||||
}
|
||||
|
||||
xhr.setRequestHeader("Authorization", header.field);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return Client;
|
||||
})(jQuery);
|
@ -62,9 +62,11 @@ loop.shared.models = (function() {
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* - {String} baseServerUrl The server URL
|
||||
* - {Boolean} outgoing Set to true if this model represents the
|
||||
* outgoing call.
|
||||
* - {loop.shared.Client} client A client object to request call information
|
||||
* from. Expects requestCallInfo for outgoing
|
||||
* calls, requestCallsInfo for incoming calls.
|
||||
*
|
||||
* Triggered events:
|
||||
*
|
||||
@ -75,10 +77,6 @@ loop.shared.models = (function() {
|
||||
* @param {Object} options Options object
|
||||
*/
|
||||
initiate: function(options) {
|
||||
var client = new loop.shared.Client({
|
||||
baseServerUrl: options.baseServerUrl
|
||||
});
|
||||
|
||||
function handleResult(err, sessionData) {
|
||||
/*jshint validthis:true */
|
||||
if (err) {
|
||||
@ -99,10 +97,10 @@ loop.shared.models = (function() {
|
||||
}
|
||||
|
||||
if (options.outgoing) {
|
||||
client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
|
||||
options.client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
|
||||
}
|
||||
else {
|
||||
client.requestCallsInfo(this.get("loopVersion"),
|
||||
options.client.requestCallsInfo(this.get("loopVersion"),
|
||||
handleResult.bind(this));
|
||||
}
|
||||
},
|
||||
|
@ -1,556 +0,0 @@
|
||||
/*
|
||||
HTTP Hawk Authentication Scheme
|
||||
Copyright (c) 2012-2014, Eran Hammer <eran@hammer.io>
|
||||
BSD Licensed
|
||||
*/
|
||||
|
||||
|
||||
// Declare namespace
|
||||
|
||||
var hawk = {
|
||||
internals: {}
|
||||
};
|
||||
|
||||
|
||||
hawk.client = {
|
||||
|
||||
// Generate an Authorization header for a given request
|
||||
|
||||
/*
|
||||
uri: 'http://example.com/resource?a=b' or object generated by hawk.utils.parseUri()
|
||||
method: HTTP verb (e.g. 'GET', 'POST')
|
||||
options: {
|
||||
|
||||
// Required
|
||||
|
||||
credentials: {
|
||||
id: 'dh37fgj492je',
|
||||
key: 'aoijedoaijsdlaksjdl',
|
||||
algorithm: 'sha256' // 'sha1', 'sha256'
|
||||
},
|
||||
|
||||
// Optional
|
||||
|
||||
ext: 'application-specific', // Application specific data sent via the ext attribute
|
||||
timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds
|
||||
nonce: '2334f34f', // A pre-generated nonce
|
||||
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
|
||||
payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
|
||||
contentType: 'application/json', // Payload content-type (ignored if hash provided)
|
||||
hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash
|
||||
app: '24s23423f34dx', // Oz application id
|
||||
dlg: '234sz34tww3sd' // Oz delegated-by application id
|
||||
}
|
||||
*/
|
||||
|
||||
header: function (uri, method, options) {
|
||||
|
||||
var result = {
|
||||
field: '',
|
||||
artifacts: {}
|
||||
};
|
||||
|
||||
// Validate inputs
|
||||
|
||||
if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
|
||||
!method || typeof method !== 'string' ||
|
||||
!options || typeof options !== 'object') {
|
||||
|
||||
result.err = 'Invalid argument type';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Application time
|
||||
|
||||
var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
|
||||
|
||||
// Validate credentials
|
||||
|
||||
var credentials = options.credentials;
|
||||
if (!credentials ||
|
||||
!credentials.id ||
|
||||
!credentials.key ||
|
||||
!credentials.algorithm) {
|
||||
|
||||
result.err = 'Invalid credentials object';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
|
||||
result.err = 'Unknown algorithm';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse URI
|
||||
|
||||
if (typeof uri === 'string') {
|
||||
uri = hawk.utils.parseUri(uri);
|
||||
}
|
||||
|
||||
// Calculate signature
|
||||
|
||||
var artifacts = {
|
||||
ts: timestamp,
|
||||
nonce: options.nonce || hawk.utils.randomString(6),
|
||||
method: method,
|
||||
resource: uri.relative,
|
||||
host: uri.hostname,
|
||||
port: uri.port,
|
||||
hash: options.hash,
|
||||
ext: options.ext,
|
||||
app: options.app,
|
||||
dlg: options.dlg
|
||||
};
|
||||
|
||||
result.artifacts = artifacts;
|
||||
|
||||
// Calculate payload hash
|
||||
|
||||
if (!artifacts.hash &&
|
||||
(options.payload || options.payload === '')) {
|
||||
|
||||
artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
|
||||
}
|
||||
|
||||
var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
|
||||
|
||||
// Construct header
|
||||
|
||||
var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed
|
||||
var header = 'Hawk id="' + credentials.id +
|
||||
'", ts="' + artifacts.ts +
|
||||
'", nonce="' + artifacts.nonce +
|
||||
(artifacts.hash ? '", hash="' + artifacts.hash : '') +
|
||||
(hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') +
|
||||
'", mac="' + mac + '"';
|
||||
|
||||
if (artifacts.app) {
|
||||
header += ', app="' + artifacts.app +
|
||||
(artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
|
||||
}
|
||||
|
||||
result.field = header;
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
|
||||
// Validate server response
|
||||
|
||||
/*
|
||||
request: object created via 'new XMLHttpRequest()' after response received
|
||||
artifacts: object received from header().artifacts
|
||||
options: {
|
||||
payload: optional payload received
|
||||
required: specifies if a Server-Authorization header is required. Defaults to 'false'
|
||||
}
|
||||
*/
|
||||
|
||||
authenticate: function (request, credentials, artifacts, options) {
|
||||
|
||||
options = options || {};
|
||||
|
||||
var getHeader = function (name) {
|
||||
|
||||
return request.getResponseHeader ? request.getResponseHeader(name) : request.getHeader(name);
|
||||
};
|
||||
|
||||
var wwwAuthenticate = getHeader('www-authenticate');
|
||||
if (wwwAuthenticate) {
|
||||
|
||||
// Parse HTTP WWW-Authenticate header
|
||||
|
||||
var attributes = hawk.utils.parseAuthorizationHeader(wwwAuthenticate, ['ts', 'tsm', 'error']);
|
||||
if (!attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attributes.ts) {
|
||||
var tsm = hawk.crypto.calculateTsMac(attributes.ts, credentials);
|
||||
if (tsm !== attributes.tsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hawk.utils.setNtpOffset(attributes.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
|
||||
}
|
||||
}
|
||||
|
||||
// Parse HTTP Server-Authorization header
|
||||
|
||||
var serverAuthorization = getHeader('server-authorization');
|
||||
if (!serverAuthorization &&
|
||||
!options.required) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var attributes = hawk.utils.parseAuthorizationHeader(serverAuthorization, ['mac', 'ext', 'hash']);
|
||||
if (!attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var modArtifacts = {
|
||||
ts: artifacts.ts,
|
||||
nonce: artifacts.nonce,
|
||||
method: artifacts.method,
|
||||
resource: artifacts.resource,
|
||||
host: artifacts.host,
|
||||
port: artifacts.port,
|
||||
hash: attributes.hash,
|
||||
ext: attributes.ext,
|
||||
app: artifacts.app,
|
||||
dlg: artifacts.dlg
|
||||
};
|
||||
|
||||
var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts);
|
||||
if (mac !== attributes.mac) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.payload &&
|
||||
options.payload !== '') {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!attributes.hash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, getHeader('content-type'));
|
||||
return (calculatedHash === attributes.hash);
|
||||
},
|
||||
|
||||
message: function (host, port, message, options) {
|
||||
|
||||
// Validate inputs
|
||||
|
||||
if (!host || typeof host !== 'string' ||
|
||||
!port || typeof port !== 'number' ||
|
||||
message === null || message === undefined || typeof message !== 'string' ||
|
||||
!options || typeof options !== 'object') {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Application time
|
||||
|
||||
var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
|
||||
|
||||
// Validate credentials
|
||||
|
||||
var credentials = options.credentials;
|
||||
if (!credentials ||
|
||||
!credentials.id ||
|
||||
!credentials.key ||
|
||||
!credentials.algorithm) {
|
||||
|
||||
// Invalid credential object
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate signature
|
||||
|
||||
var artifacts = {
|
||||
ts: timestamp,
|
||||
nonce: options.nonce || hawk.utils.randomString(6),
|
||||
host: host,
|
||||
port: port,
|
||||
hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm)
|
||||
};
|
||||
|
||||
// Construct authorization
|
||||
|
||||
var result = {
|
||||
id: credentials.id,
|
||||
ts: artifacts.ts,
|
||||
nonce: artifacts.nonce,
|
||||
hash: artifacts.hash,
|
||||
mac: hawk.crypto.calculateMac('message', credentials, artifacts)
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true
|
||||
|
||||
var tsm = hawk.crypto.calculateTsMac(message.ts, credentials);
|
||||
if (tsm !== message.tsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updateClock !== false) {
|
||||
hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
hawk.crypto = {
|
||||
|
||||
headerVersion: '1',
|
||||
|
||||
algorithms: ['sha1', 'sha256'],
|
||||
|
||||
calculateMac: function (type, credentials, options) {
|
||||
|
||||
var normalized = hawk.crypto.generateNormalizedString(type, options);
|
||||
|
||||
var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key);
|
||||
return hmac.toString(CryptoJS.enc.Base64);
|
||||
},
|
||||
|
||||
generateNormalizedString: function (type, options) {
|
||||
|
||||
var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' +
|
||||
options.ts + '\n' +
|
||||
options.nonce + '\n' +
|
||||
(options.method || '').toUpperCase() + '\n' +
|
||||
(options.resource || '') + '\n' +
|
||||
options.host.toLowerCase() + '\n' +
|
||||
options.port + '\n' +
|
||||
(options.hash || '') + '\n';
|
||||
|
||||
if (options.ext) {
|
||||
normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n');
|
||||
}
|
||||
|
||||
normalized += '\n';
|
||||
|
||||
if (options.app) {
|
||||
normalized += options.app + '\n' +
|
||||
(options.dlg || '') + '\n';
|
||||
}
|
||||
|
||||
return normalized;
|
||||
},
|
||||
|
||||
calculatePayloadHash: function (payload, algorithm, contentType) {
|
||||
|
||||
var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
|
||||
hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
|
||||
hash.update(hawk.utils.parseContentType(contentType) + '\n');
|
||||
hash.update(payload);
|
||||
hash.update('\n');
|
||||
return hash.finalize().toString(CryptoJS.enc.Base64);
|
||||
},
|
||||
|
||||
calculateTsMac: function (ts, credentials) {
|
||||
|
||||
var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key);
|
||||
return hash.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// localStorage compatible interface
|
||||
|
||||
hawk.internals.LocalStorage = function () {
|
||||
|
||||
this._cache = {};
|
||||
this.length = 0;
|
||||
|
||||
this.getItem = function (key) {
|
||||
|
||||
return this._cache.hasOwnProperty(key) ? String(this._cache[key]) : null;
|
||||
};
|
||||
|
||||
this.setItem = function (key, value) {
|
||||
|
||||
this._cache[key] = String(value);
|
||||
this.length = Object.keys(this._cache).length;
|
||||
};
|
||||
|
||||
this.removeItem = function (key) {
|
||||
|
||||
delete this._cache[key];
|
||||
this.length = Object.keys(this._cache).length;
|
||||
};
|
||||
|
||||
this.clear = function () {
|
||||
|
||||
this._cache = {};
|
||||
this.length = 0;
|
||||
};
|
||||
|
||||
this.key = function (i) {
|
||||
|
||||
return Object.keys(this._cache)[i || 0];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
hawk.utils = {
|
||||
|
||||
storage: new hawk.internals.LocalStorage(),
|
||||
|
||||
setStorage: function (storage) {
|
||||
|
||||
var ntpOffset = hawk.utils.storage.getItem('hawk_ntp_offset');
|
||||
hawk.utils.storage = storage;
|
||||
if (ntpOffset) {
|
||||
hawk.utils.setNtpOffset(ntpOffset);
|
||||
}
|
||||
},
|
||||
|
||||
setNtpOffset: function (offset) {
|
||||
|
||||
try {
|
||||
hawk.utils.storage.setItem('hawk_ntp_offset', offset);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[hawk] could not write to storage.');
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
getNtpOffset: function () {
|
||||
|
||||
var offset = hawk.utils.storage.getItem('hawk_ntp_offset');
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parseInt(offset, 10);
|
||||
},
|
||||
|
||||
now: function (localtimeOffsetMsec) {
|
||||
|
||||
return Math.floor(((new Date()).getTime() + (localtimeOffsetMsec || 0)) / 1000) + hawk.utils.getNtpOffset();
|
||||
},
|
||||
|
||||
escapeHeaderAttribute: function (attribute) {
|
||||
|
||||
return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
|
||||
},
|
||||
|
||||
parseContentType: function (header) {
|
||||
|
||||
if (!header) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase();
|
||||
},
|
||||
|
||||
parseAuthorizationHeader: function (header, keys) {
|
||||
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something]
|
||||
if (!headerParts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var scheme = headerParts[1];
|
||||
if (scheme.toLowerCase() !== 'hawk') {
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributesString = headerParts[2];
|
||||
if (!attributesString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributes = {};
|
||||
var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) {
|
||||
|
||||
// Check valid attribute names
|
||||
|
||||
if (keys.indexOf($1) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
|
||||
|
||||
if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
|
||||
if (attributes.hasOwnProperty($1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
attributes[$1] = $2;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (verify !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
|
||||
randomString: function (size) {
|
||||
|
||||
var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
var len = randomSource.length;
|
||||
|
||||
var result = [];
|
||||
for (var i = 0; i < size; ++i) {
|
||||
result[i] = randomSource[Math.floor(Math.random() * len)];
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
},
|
||||
|
||||
parseUri: function (input) {
|
||||
|
||||
// Based on: parseURI 1.2.2
|
||||
// http://blog.stevenlevithan.com/archives/parseuri
|
||||
// (c) Steven Levithan <stevenlevithan.com>
|
||||
// MIT License
|
||||
|
||||
var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment'];
|
||||
|
||||
var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/;
|
||||
var uriByNumber = input.match(uriRegex);
|
||||
var uri = {};
|
||||
|
||||
for (var i = 0, il = keys.length; i < il; ++i) {
|
||||
uri[keys[i]] = uriByNumber[i] || '';
|
||||
}
|
||||
|
||||
if (uri.port === '') {
|
||||
uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : ''));
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// $lab:coverage:off$
|
||||
|
||||
// Based on: Crypto-JS v3.1.2
|
||||
// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
|
||||
// http://code.google.com/p/crypto-js/
|
||||
// http://code.google.com/p/crypto-js/wiki/License
|
||||
|
||||
var CryptoJS = CryptoJS || function (h, r) { var k = {}, l = k.lib = {}, n = function () { }, f = l.Base = { extend: function (a) { n.prototype = this; var b = new n; a && b.mixIn(a); b.hasOwnProperty("init") || (b.init = function () { b.$super.init.apply(this, arguments) }); b.init.prototype = b; b.$super = this; return b }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (var b in a) a.hasOwnProperty(b) && (this[b] = a[b]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } }, j = l.WordArray = f.extend({ init: function (a, b) { a = this.words = a || []; this.sigBytes = b != r ? b : 4 * a.length }, toString: function (a) { return (a || s).stringify(this) }, concat: function (a) { var b = this.words, d = a.words, c = this.sigBytes; a = a.sigBytes; this.clamp(); if (c % 4) for (var e = 0; e < a; e++) b[c + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((c + e) % 4); else if (65535 < d.length) for (e = 0; e < a; e += 4) b[c + e >>> 2] = d[e >>> 2]; else b.push.apply(b, d); this.sigBytes += a; return this }, clamp: function () { var a = this.words, b = this.sigBytes; a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4); a.length = h.ceil(b / 4) }, clone: function () { var a = f.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (var b = [], d = 0; d < a; d += 4) b.push(4294967296 * h.random() | 0); return new j.init(b, a) } }), m = k.enc = {}, s = m.Hex = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) { var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255; d.push((e >>> 4).toString(16)); d.push((e & 15).toString(16)) } return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c += 2) d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8); return new j.init(d, b / 2) } }, p = m.Latin1 = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255)); return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c++) d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4); return new j.init(d, b) } }, t = m.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(p.stringify(a))) } catch (b) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return p.parse(unescape(encodeURIComponent(a))) } }, q = l.BufferedBlockAlgorithm = f.extend({ reset: function () { this._data = new j.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = t.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var b = this._data, d = b.words, c = b.sigBytes, e = this.blockSize, f = c / (4 * e), f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0); a = f * e; c = h.min(4 * a, c); if (a) { for (var g = 0; g < a; g += e) this._doProcessBlock(d, g); g = d.splice(0, a); b.sigBytes -= c } return new j.init(g, c) }, clone: function () { var a = f.clone.call(this); a._data = this._data.clone(); return a }, _minBufferSize: 0 }); l.Hasher = q.extend({ cfg: f.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { q.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (b, d) { return (new a.init(d)).finalize(b) } }, _createHmacHelper: function (a) { return function (b, d) { return (new u.HMAC.init(a, d)).finalize(b) } } }); var u = k.algo = {}; return k }(Math);
|
||||
(function () { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })();
|
||||
(function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math);
|
||||
(function () { var c = CryptoJS, k = c.enc.Utf8; c.algo.HMAC = c.lib.Base.extend({ init: function (a, b) { a = this._hasher = new a.init; "string" == typeof b && (b = k.parse(b)); var c = a.blockSize, e = 4 * c; b.sigBytes > e && (b = a.finalize(b)); b.clamp(); for (var f = this._oKey = b.clone(), g = this._iKey = b.clone(), h = f.words, j = g.words, d = 0; d < c; d++) h[d] ^= 1549556828, j[d] ^= 909522486; f.sigBytes = g.sigBytes = e; this.reset() }, reset: function () { var a = this._hasher; a.reset(); a.update(this._iKey) }, update: function (a) { this._hasher.update(a); return this }, finalize: function (a) { var b = this._hasher; a = b.finalize(a); b.reset(); return b.finalize(this._oKey.clone().concat(a)) } }) })();
|
||||
(function () { var h = CryptoJS, j = h.lib.WordArray; h.enc.Base64 = { stringify: function (b) { var e = b.words, f = b.sigBytes, c = this._map; b.clamp(); b = []; for (var a = 0; a < f; a += 3) for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) b.push(c.charAt(d >>> 6 * (3 - g) & 63)); if (e = c.charAt(64)) for (; b.length % 4;) b.push(e); return b.join("") }, parse: function (b) { var e = b.length, f = this._map, c = f.charAt(64); c && (c = b.indexOf(c), -1 != c && (e = c)); for (var c = [], a = 0, d = 0; d < e; d++) if (d % 4) { var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4); c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4); a++ } return j.create(c, a) }, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" } })();
|
||||
|
||||
hawk.crypto.internals = CryptoJS;
|
||||
|
||||
|
||||
// Export if used as a module
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = hawk;
|
||||
}
|
||||
|
||||
// $lab:coverage:on$
|
@ -1,606 +0,0 @@
|
||||
/** @fileOverview Javascript cryptography implementation.
|
||||
*
|
||||
* Crush to remove comments, shorten variable names and
|
||||
* generally reduce transmission size.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
|
||||
/*global document, window, escape, unescape, module, require, Uint32Array */
|
||||
|
||||
/** @namespace The Stanford Javascript Crypto Library, top-level namespace. */
|
||||
var sjcl = {
|
||||
/** @namespace Symmetric ciphers. */
|
||||
cipher: {},
|
||||
|
||||
/** @namespace Hash functions. Right now only SHA256 is implemented. */
|
||||
hash: {},
|
||||
|
||||
/** @namespace Key exchange functions. Right now only SRP is implemented. */
|
||||
keyexchange: {},
|
||||
|
||||
/** @namespace Block cipher modes of operation. */
|
||||
mode: {},
|
||||
|
||||
/** @namespace Miscellaneous. HMAC and PBKDF2. */
|
||||
misc: {},
|
||||
|
||||
/**
|
||||
* @namespace Bit array encoders and decoders.
|
||||
*
|
||||
* @description
|
||||
* The members of this namespace are functions which translate between
|
||||
* SJCL's bitArrays and other objects (usually strings). Because it
|
||||
* isn't always clear which direction is encoding and which is decoding,
|
||||
* the method names are "fromBits" and "toBits".
|
||||
*/
|
||||
codec: {},
|
||||
|
||||
/** @namespace Exceptions. */
|
||||
exception: {
|
||||
/** @constructor Ciphertext is corrupt. */
|
||||
corrupt: function(message) {
|
||||
this.toString = function() { return "CORRUPT: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Invalid parameter. */
|
||||
invalid: function(message) {
|
||||
this.toString = function() { return "INVALID: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Bug or missing feature in SJCL. @constructor */
|
||||
bug: function(message) {
|
||||
this.toString = function() { return "BUG: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Something isn't ready. */
|
||||
notReady: function(message) {
|
||||
this.toString = function() { return "NOT READY: "+this.message; };
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(typeof module !== 'undefined' && module.exports){
|
||||
module.exports = sjcl;
|
||||
}
|
||||
/** @fileOverview Arrays of bits, encoded as arrays of Numbers.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace Arrays of bits, encoded as arrays of Numbers.
|
||||
*
|
||||
* @description
|
||||
* <p>
|
||||
* These objects are the currency accepted by SJCL's crypto functions.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Most of our crypto primitives operate on arrays of 4-byte words internally,
|
||||
* but many of them can take arguments that are not a multiple of 4 bytes.
|
||||
* This library encodes arrays of bits (whose size need not be a multiple of 8
|
||||
* bits) as arrays of 32-bit words. The bits are packed, big-endian, into an
|
||||
* array of words, 32 bits at a time. Since the words are double-precision
|
||||
* floating point numbers, they fit some extra data. We use this (in a private,
|
||||
* possibly-changing manner) to encode the number of bits actually present
|
||||
* in the last word of the array.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Because bitwise ops clear this out-of-band data, these arrays can be passed
|
||||
* to ciphers like AES which want arrays of words.
|
||||
* </p>
|
||||
*/
|
||||
sjcl.bitArray = {
|
||||
/**
|
||||
* Array slices in units of bits.
|
||||
* @param {bitArray} a The array to slice.
|
||||
* @param {Number} bstart The offset to the start of the slice, in bits.
|
||||
* @param {Number} bend The offset to the end of the slice, in bits. If this is undefined,
|
||||
* slice until the end of the array.
|
||||
* @return {bitArray} The requested slice.
|
||||
*/
|
||||
bitSlice: function (a, bstart, bend) {
|
||||
a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1);
|
||||
return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart);
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract a number packed into a bit array.
|
||||
* @param {bitArray} a The array to slice.
|
||||
* @param {Number} bstart The offset to the start of the slice, in bits.
|
||||
* @param {Number} length The length of the number to extract.
|
||||
* @return {Number} The requested slice.
|
||||
*/
|
||||
extract: function(a, bstart, blength) {
|
||||
// FIXME: this Math.floor is not necessary at all, but for some reason
|
||||
// seems to suppress a bug in the Chromium JIT.
|
||||
var x, sh = Math.floor((-bstart-blength) & 31);
|
||||
if ((bstart + blength - 1 ^ bstart) & -32) {
|
||||
// it crosses a boundary
|
||||
x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh);
|
||||
} else {
|
||||
// within a single word
|
||||
x = a[bstart/32|0] >>> sh;
|
||||
}
|
||||
return x & ((1<<blength) - 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Concatenate two bit arrays.
|
||||
* @param {bitArray} a1 The first array.
|
||||
* @param {bitArray} a2 The second array.
|
||||
* @return {bitArray} The concatenation of a1 and a2.
|
||||
*/
|
||||
concat: function (a1, a2) {
|
||||
if (a1.length === 0 || a2.length === 0) {
|
||||
return a1.concat(a2);
|
||||
}
|
||||
|
||||
var last = a1[a1.length-1], shift = sjcl.bitArray.getPartial(last);
|
||||
if (shift === 32) {
|
||||
return a1.concat(a2);
|
||||
} else {
|
||||
return sjcl.bitArray._shiftRight(a2, shift, last|0, a1.slice(0,a1.length-1));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the length of an array of bits.
|
||||
* @param {bitArray} a The array.
|
||||
* @return {Number} The length of a, in bits.
|
||||
*/
|
||||
bitLength: function (a) {
|
||||
var l = a.length, x;
|
||||
if (l === 0) { return 0; }
|
||||
x = a[l - 1];
|
||||
return (l-1) * 32 + sjcl.bitArray.getPartial(x);
|
||||
},
|
||||
|
||||
/**
|
||||
* Truncate an array.
|
||||
* @param {bitArray} a The array.
|
||||
* @param {Number} len The length to truncate to, in bits.
|
||||
* @return {bitArray} A new array, truncated to len bits.
|
||||
*/
|
||||
clamp: function (a, len) {
|
||||
if (a.length * 32 < len) { return a; }
|
||||
a = a.slice(0, Math.ceil(len / 32));
|
||||
var l = a.length;
|
||||
len = len & 31;
|
||||
if (l > 0 && len) {
|
||||
a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1);
|
||||
}
|
||||
return a;
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a partial word for a bit array.
|
||||
* @param {Number} len The number of bits in the word.
|
||||
* @param {Number} x The bits.
|
||||
* @param {Number} [0] _end Pass 1 if x has already been shifted to the high side.
|
||||
* @return {Number} The partial word.
|
||||
*/
|
||||
partial: function (len, x, _end) {
|
||||
if (len === 32) { return x; }
|
||||
return (_end ? x|0 : x << (32-len)) + len * 0x10000000000;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the number of bits used by a partial word.
|
||||
* @param {Number} x The partial word.
|
||||
* @return {Number} The number of bits used by the partial word.
|
||||
*/
|
||||
getPartial: function (x) {
|
||||
return Math.round(x/0x10000000000) || 32;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare two arrays for equality in a predictable amount of time.
|
||||
* @param {bitArray} a The first array.
|
||||
* @param {bitArray} b The second array.
|
||||
* @return {boolean} true if a == b; false otherwise.
|
||||
*/
|
||||
equal: function (a, b) {
|
||||
if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) {
|
||||
return false;
|
||||
}
|
||||
var x = 0, i;
|
||||
for (i=0; i<a.length; i++) {
|
||||
x |= a[i]^b[i];
|
||||
}
|
||||
return (x === 0);
|
||||
},
|
||||
|
||||
/** Shift an array right.
|
||||
* @param {bitArray} a The array to shift.
|
||||
* @param {Number} shift The number of bits to shift.
|
||||
* @param {Number} [carry=0] A byte to carry in
|
||||
* @param {bitArray} [out=[]] An array to prepend to the output.
|
||||
* @private
|
||||
*/
|
||||
_shiftRight: function (a, shift, carry, out) {
|
||||
var i, last2=0, shift2;
|
||||
if (out === undefined) { out = []; }
|
||||
|
||||
for (; shift >= 32; shift -= 32) {
|
||||
out.push(carry);
|
||||
carry = 0;
|
||||
}
|
||||
if (shift === 0) {
|
||||
return out.concat(a);
|
||||
}
|
||||
|
||||
for (i=0; i<a.length; i++) {
|
||||
out.push(carry | a[i]>>>shift);
|
||||
carry = a[i] << (32-shift);
|
||||
}
|
||||
last2 = a.length ? a[a.length-1] : 0;
|
||||
shift2 = sjcl.bitArray.getPartial(last2);
|
||||
out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1));
|
||||
return out;
|
||||
},
|
||||
|
||||
/** xor a block of 4 words together.
|
||||
* @private
|
||||
*/
|
||||
_xor4: function(x,y) {
|
||||
return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]];
|
||||
}
|
||||
};
|
||||
/** @fileOverview Bit array codec implementations.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace UTF-8 strings */
|
||||
sjcl.codec.utf8String = {
|
||||
/** Convert from a bitArray to a UTF-8 string. */
|
||||
fromBits: function (arr) {
|
||||
var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp;
|
||||
for (i=0; i<bl/8; i++) {
|
||||
if ((i&3) === 0) {
|
||||
tmp = arr[i/4];
|
||||
}
|
||||
out += String.fromCharCode(tmp >>> 24);
|
||||
tmp <<= 8;
|
||||
}
|
||||
return decodeURIComponent(escape(out));
|
||||
},
|
||||
|
||||
/** Convert from a UTF-8 string to a bitArray. */
|
||||
toBits: function (str) {
|
||||
str = unescape(encodeURIComponent(str));
|
||||
var out = [], i, tmp=0;
|
||||
for (i=0; i<str.length; i++) {
|
||||
tmp = tmp << 8 | str.charCodeAt(i);
|
||||
if ((i&3) === 3) {
|
||||
out.push(tmp);
|
||||
tmp = 0;
|
||||
}
|
||||
}
|
||||
if (i&3) {
|
||||
out.push(sjcl.bitArray.partial(8*(i&3), tmp));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
/** @fileOverview Bit array codec implementations.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace Hexadecimal */
|
||||
sjcl.codec.hex = {
|
||||
/** Convert from a bitArray to a hex string. */
|
||||
fromBits: function (arr) {
|
||||
var out = "", i;
|
||||
for (i=0; i<arr.length; i++) {
|
||||
out += ((arr[i]|0)+0xF00000000000).toString(16).substr(4);
|
||||
}
|
||||
return out.substr(0, sjcl.bitArray.bitLength(arr)/4);//.replace(/(.{8})/g, "$1 ");
|
||||
},
|
||||
/** Convert from a hex string to a bitArray. */
|
||||
toBits: function (str) {
|
||||
var i, out=[], len;
|
||||
str = str.replace(/\s|0x/g, "");
|
||||
len = str.length;
|
||||
str = str + "00000000";
|
||||
for (i=0; i<str.length; i+=8) {
|
||||
out.push(parseInt(str.substr(i,8),16)^0);
|
||||
}
|
||||
return sjcl.bitArray.clamp(out, len*4);
|
||||
}
|
||||
};
|
||||
|
||||
/** @fileOverview Javascript SHA-256 implementation.
|
||||
*
|
||||
* An older version of this implementation is available in the public
|
||||
* domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh,
|
||||
* Stanford University 2008-2010 and BSD-licensed for liability
|
||||
* reasons.
|
||||
*
|
||||
* Special thanks to Aldo Cortesi for pointing out several bugs in
|
||||
* this code.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/**
|
||||
* Context for a SHA-256 operation in progress.
|
||||
* @constructor
|
||||
* @class Secure Hash Algorithm, 256 bits.
|
||||
*/
|
||||
sjcl.hash.sha256 = function (hash) {
|
||||
if (!this._key[0]) { this._precompute(); }
|
||||
if (hash) {
|
||||
this._h = hash._h.slice(0);
|
||||
this._buffer = hash._buffer.slice(0);
|
||||
this._length = hash._length;
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash a string or an array of words.
|
||||
* @static
|
||||
* @param {bitArray|String} data the data to hash.
|
||||
* @return {bitArray} The hash value, an array of 16 big-endian words.
|
||||
*/
|
||||
sjcl.hash.sha256.hash = function (data) {
|
||||
return (new sjcl.hash.sha256()).update(data).finalize();
|
||||
};
|
||||
|
||||
sjcl.hash.sha256.prototype = {
|
||||
/**
|
||||
* The hash's block size, in bits.
|
||||
* @constant
|
||||
*/
|
||||
blockSize: 512,
|
||||
|
||||
/**
|
||||
* Reset the hash state.
|
||||
* @return this
|
||||
*/
|
||||
reset:function () {
|
||||
this._h = this._init.slice(0);
|
||||
this._buffer = [];
|
||||
this._length = 0;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Input several words to the hash.
|
||||
* @param {bitArray|String} data the data to hash.
|
||||
* @return this
|
||||
*/
|
||||
update: function (data) {
|
||||
if (typeof data === "string") {
|
||||
data = sjcl.codec.utf8String.toBits(data);
|
||||
}
|
||||
var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data),
|
||||
ol = this._length,
|
||||
nl = this._length = ol + sjcl.bitArray.bitLength(data);
|
||||
for (i = 512+ol & -512; i <= nl; i+= 512) {
|
||||
this._block(b.splice(0,16));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Complete hashing and output the hash value.
|
||||
* @return {bitArray} The hash value, an array of 8 big-endian words.
|
||||
*/
|
||||
finalize:function () {
|
||||
var i, b = this._buffer, h = this._h;
|
||||
|
||||
// Round out and push the buffer
|
||||
b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]);
|
||||
|
||||
// Round out the buffer to a multiple of 16 words, less the 2 length words.
|
||||
for (i = b.length + 2; i & 15; i++) {
|
||||
b.push(0);
|
||||
}
|
||||
|
||||
// append the length
|
||||
b.push(Math.floor(this._length / 0x100000000));
|
||||
b.push(this._length | 0);
|
||||
|
||||
while (b.length) {
|
||||
this._block(b.splice(0,16));
|
||||
}
|
||||
|
||||
this.reset();
|
||||
return h;
|
||||
},
|
||||
|
||||
/**
|
||||
* The SHA-256 initialization vector, to be precomputed.
|
||||
* @private
|
||||
*/
|
||||
_init:[],
|
||||
/*
|
||||
_init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],
|
||||
*/
|
||||
|
||||
/**
|
||||
* The SHA-256 hash key, to be precomputed.
|
||||
* @private
|
||||
*/
|
||||
_key:[],
|
||||
/*
|
||||
_key:
|
||||
[0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2],
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Function to precompute _init and _key.
|
||||
* @private
|
||||
*/
|
||||
_precompute: function () {
|
||||
var i = 0, prime = 2, factor;
|
||||
|
||||
function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; }
|
||||
|
||||
outer: for (; i<64; prime++) {
|
||||
for (factor=2; factor*factor <= prime; factor++) {
|
||||
if (prime % factor === 0) {
|
||||
// not a prime
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
if (i<8) {
|
||||
this._init[i] = frac(Math.pow(prime, 1/2));
|
||||
}
|
||||
this._key[i] = frac(Math.pow(prime, 1/3));
|
||||
i++;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform one cycle of SHA-256.
|
||||
* @param {bitArray} words one block of words.
|
||||
* @private
|
||||
*/
|
||||
_block:function (words) {
|
||||
var i, tmp, a, b,
|
||||
w = words.slice(0),
|
||||
h = this._h,
|
||||
k = this._key,
|
||||
h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3],
|
||||
h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7];
|
||||
|
||||
/* Rationale for placement of |0 :
|
||||
* If a value can overflow is original 32 bits by a factor of more than a few
|
||||
* million (2^23 ish), there is a possibility that it might overflow the
|
||||
* 53-bit mantissa and lose precision.
|
||||
*
|
||||
* To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that
|
||||
* propagates around the loop, and on the hash state h[]. I don't believe
|
||||
* that the clamps on h4 and on h0 are strictly necessary, but it's close
|
||||
* (for h4 anyway), and better safe than sorry.
|
||||
*
|
||||
* The clamps on h[] are necessary for the output to be correct even in the
|
||||
* common case and for short inputs.
|
||||
*/
|
||||
for (i=0; i<64; i++) {
|
||||
// load up the input word for this round
|
||||
if (i<16) {
|
||||
tmp = w[i];
|
||||
} else {
|
||||
a = w[(i+1 ) & 15];
|
||||
b = w[(i+14) & 15];
|
||||
tmp = w[i&15] = ((a>>>7 ^ a>>>18 ^ a>>>3 ^ a<<25 ^ a<<14) +
|
||||
(b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) +
|
||||
w[i&15] + w[(i+9) & 15]) | 0;
|
||||
}
|
||||
|
||||
tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) + (h6 ^ h4&(h5^h6)) + k[i]); // | 0;
|
||||
|
||||
// shift register
|
||||
h7 = h6; h6 = h5; h5 = h4;
|
||||
h4 = h3 + tmp | 0;
|
||||
h3 = h2; h2 = h1; h1 = h0;
|
||||
|
||||
h0 = (tmp + ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0;
|
||||
}
|
||||
|
||||
h[0] = h[0]+h0 | 0;
|
||||
h[1] = h[1]+h1 | 0;
|
||||
h[2] = h[2]+h2 | 0;
|
||||
h[3] = h[3]+h3 | 0;
|
||||
h[4] = h[4]+h4 | 0;
|
||||
h[5] = h[5]+h5 | 0;
|
||||
h[6] = h[6]+h6 | 0;
|
||||
h[7] = h[7]+h7 | 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @fileOverview HMAC implementation.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** HMAC with the specified hash function.
|
||||
* @constructor
|
||||
* @param {bitArray} key the key for HMAC.
|
||||
* @param {Object} [hash=sjcl.hash.sha256] The hash function to use.
|
||||
*/
|
||||
sjcl.misc.hmac = function (key, Hash) {
|
||||
this._hash = Hash = Hash || sjcl.hash.sha256;
|
||||
var exKey = [[],[]], i,
|
||||
bs = Hash.prototype.blockSize / 32;
|
||||
this._baseHash = [new Hash(), new Hash()];
|
||||
|
||||
if (key.length > bs) {
|
||||
key = Hash.hash(key);
|
||||
}
|
||||
|
||||
for (i=0; i<bs; i++) {
|
||||
exKey[0][i] = key[i]^0x36363636;
|
||||
exKey[1][i] = key[i]^0x5C5C5C5C;
|
||||
}
|
||||
|
||||
this._baseHash[0].update(exKey[0]);
|
||||
this._baseHash[1].update(exKey[1]);
|
||||
this._resultHash = new Hash(this._baseHash[0]);
|
||||
};
|
||||
|
||||
/** HMAC with the specified hash function. Also called encrypt since it's a prf.
|
||||
* @param {bitArray|String} data The data to mac.
|
||||
*/
|
||||
sjcl.misc.hmac.prototype.encrypt = sjcl.misc.hmac.prototype.mac = function (data) {
|
||||
if (!this._updated) {
|
||||
this.update(data);
|
||||
return this.digest(data);
|
||||
} else {
|
||||
throw new sjcl.exception.invalid("encrypt on already updated hmac called!");
|
||||
}
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.reset = function () {
|
||||
this._resultHash = new this._hash(this._baseHash[0]);
|
||||
this._updated = false;
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.update = function (data) {
|
||||
this._updated = true;
|
||||
this._resultHash.update(data);
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.digest = function () {
|
||||
var w = this._resultHash.finalize(), result = new (this._hash)(this._baseHash[1]).update(w).finalize();
|
||||
|
||||
this.reset();
|
||||
|
||||
return result;
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
/* 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';
|
||||
|
||||
var PREFIX_NAME = 'identity.mozilla.com/picl/v1/';
|
||||
var bitSlice = sjcl.bitArray.bitSlice;
|
||||
var salt = sjcl.codec.hex.toBits('');
|
||||
|
||||
/**
|
||||
* hkdf - The HMAC-based Key Derivation Function
|
||||
* based on https://github.com/mozilla/node-hkdf
|
||||
*
|
||||
* @class hkdf
|
||||
* @param {bitArray} ikm Initial keying material
|
||||
* @param {bitArray} info Key derivation data
|
||||
* @param {bitArray} salt Salt
|
||||
* @param {integer} length Length of the derived key in bytes
|
||||
* @return promise object- It will resolve with `output` data
|
||||
*/
|
||||
function hkdf(ikm, info, salt, length, callback) {
|
||||
var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256);
|
||||
mac.update(ikm);
|
||||
|
||||
// compute the PRK
|
||||
var prk = mac.digest();
|
||||
|
||||
// hash length is 32 because only sjcl.hash.sha256 is used at this moment
|
||||
var hashLength = 32;
|
||||
var num_blocks = Math.ceil(length / hashLength);
|
||||
var prev = sjcl.codec.hex.toBits('');
|
||||
var output = '';
|
||||
|
||||
for (var i = 0; i < num_blocks; i++) {
|
||||
var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256);
|
||||
|
||||
var input = sjcl.bitArray.concat(
|
||||
sjcl.bitArray.concat(prev, info),
|
||||
sjcl.codec.utf8String.toBits((String.fromCharCode(i + 1)))
|
||||
);
|
||||
|
||||
hmac.update(input);
|
||||
|
||||
prev = hmac.digest();
|
||||
output += sjcl.codec.hex.fromBits(prev);
|
||||
}
|
||||
|
||||
var truncated = sjcl.bitArray.clamp(sjcl.codec.hex.toBits(output), length * 8);
|
||||
|
||||
callback(truncated);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @class hawkCredentials
|
||||
* @method deriveHawkCredentials
|
||||
* @param {String} tokenHex
|
||||
* @param {String} context
|
||||
* @param {int} size
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function deriveHawkCredentials(tokenHex, context, size, callback) {
|
||||
var token = sjcl.codec.hex.toBits(tokenHex);
|
||||
var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context);
|
||||
|
||||
hkdf(token, info, salt, size || 3 * 32, function(out) {
|
||||
var authKey = bitSlice(out, 8 * 32, 8 * 64);
|
||||
var bundleKey = bitSlice(out, 8 * 64);
|
||||
callback({
|
||||
algorithm: 'sha256',
|
||||
id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)),
|
||||
key: sjcl.codec.hex.fromBits(authKey),
|
||||
bundleKey: bundleKey
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -11,20 +11,17 @@ browser.jar:
|
||||
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
|
||||
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
|
||||
content/browser/loop/shared/img/loading-icon.gif (content/shared/img/loading-icon.gif)
|
||||
content/browser/loop/shared/js/client.js (content/shared/js/client.js)
|
||||
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
|
||||
content/browser/loop/shared/js/router.js (content/shared/js/router.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/libs/lodash-2.4.1.js (content/shared/libs/lodash-2.4.1.js)
|
||||
content/browser/loop/shared/libs/jquery-2.1.0.js (content/shared/libs/jquery-2.1.0.js)
|
||||
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
|
||||
content/browser/loop/shared/libs/sjcl-dev20140604.js (content/shared/libs/sjcl-dev20140604.js)
|
||||
content/browser/loop/shared/libs/token.js (content/shared/libs/token.js)
|
||||
content/browser/loop/shared/libs/hawk-browser-2.2.1.js (content/shared/libs/hawk-browser-2.2.1.js)
|
||||
content/browser/loop/shared/sounds/Firefox-Long.ogg (content/shared/sounds/Firefox-Long.ogg)
|
||||
content/browser/loop/libs/l10n.js (content/libs/l10n.js)
|
||||
content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js)
|
||||
content/browser/loop/js/client.js (content/js/client.js)
|
||||
content/browser/loop/js/conversation.js (content/js/conversation.js)
|
||||
content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js)
|
||||
content/browser/loop/js/panel.js (content/js/panel.js)
|
||||
# Partner SDK assets
|
||||
content/browser/loop/libs/sdk.js (content/libs/sdk.js)
|
||||
|
@ -30,10 +30,10 @@
|
||||
|
||||
<!-- app scripts -->
|
||||
<script type="text/javascript" src="config.js"></script>
|
||||
<script type="text/javascript" src="shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
<script type="text/javascript" src="js/webapp.js"></script>
|
||||
|
||||
<script>
|
||||
|
@ -0,0 +1,120 @@
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.StandaloneClient = (function($) {
|
||||
"use strict";
|
||||
|
||||
// The expected properties to be returned from the POST /calls request.
|
||||
var expectedCallsProperties = [ "sessionId", "sessionToken", "apiKey" ];
|
||||
|
||||
/**
|
||||
* Loop server standalone client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function StandaloneClient(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.baseServerUrl) {
|
||||
throw new Error("missing required baseServerUrl");
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
StandaloneClient.prototype = {
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length == 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param jqXHR See jQuery docs
|
||||
* @param textStatus See jQuery docs
|
||||
* @param errorThrown See jQuery docs
|
||||
*/
|
||||
_failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
|
||||
var error = "Unknown error.",
|
||||
jsonRes = jqXHR && jqXHR.responseJSON || {};
|
||||
// Received error response format:
|
||||
// { "status": "errors",
|
||||
// "errors": [{
|
||||
// "location": "url",
|
||||
// "name": "token",
|
||||
// "description": "invalid token"
|
||||
// }]}
|
||||
if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
|
||||
error = "Details: " + jsonRes.errors.map(function(err) {
|
||||
return Object.keys(err).map(function(field) {
|
||||
return field + ": " + err[field];
|
||||
}).join(", ");
|
||||
}).join("; ");
|
||||
}
|
||||
var message = "HTTP " + jqXHR.status + " " + errorThrown +
|
||||
"; " + error;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts a call request to the server for a call represented by the
|
||||
* loopToken. Will return the session data for the call.
|
||||
*
|
||||
* @param {String} loopToken The loopToken representing the call
|
||||
* @param {Function} cb Callback(err, sessionData)
|
||||
*/
|
||||
requestCallInfo: function(loopToken, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("missing required parameter loopToken");
|
||||
}
|
||||
|
||||
var req = $.ajax({
|
||||
url: this.settings.baseServerUrl + "/calls/" + loopToken,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.done(function(sessionData) {
|
||||
try {
|
||||
cb(null, this._validate(sessionData, expectedCallsProperties));
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
};
|
||||
|
||||
return StandaloneClient;
|
||||
})(jQuery);
|
@ -92,7 +92,9 @@ loop.webapp = (function($, _, OT) {
|
||||
initiate: function(event) {
|
||||
event.preventDefault();
|
||||
this.model.initiate({
|
||||
baseServerUrl: baseServerUrl,
|
||||
client: new loop.StandaloneClient({
|
||||
baseServerUrl: baseServerUrl,
|
||||
}),
|
||||
outgoing: true
|
||||
});
|
||||
this.disableForm();
|
||||
|
184
browser/components/loop/test/desktop-local/client_test.js
Normal file
184
browser/components/loop/test/desktop-local/client_test.js
Normal file
@ -0,0 +1,184 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.Client", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
callback,
|
||||
client,
|
||||
mozLoop,
|
||||
fakeToken,
|
||||
hawkRequestStub;
|
||||
|
||||
var fakeErrorRes = {
|
||||
code: 400,
|
||||
errno: 400,
|
||||
error: "Request Failed",
|
||||
message: "invalid token"
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
mozLoop = {
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken),
|
||||
ensureRegistered: sinon.stub().callsArgWith(0, null),
|
||||
noteCallUrlExpiry: sinon.spy(),
|
||||
hawkRequest: sinon.stub()
|
||||
};
|
||||
// Alias for clearer tests.
|
||||
hawkRequestStub = mozLoop.hawkRequest;
|
||||
client = new loop.Client({
|
||||
mozLoop: mozLoop
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.Client", function() {
|
||||
describe("#requestCallUrl", function() {
|
||||
it("should ensure loop is registered", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(mozLoop.ensureRegistered);
|
||||
});
|
||||
|
||||
it("should send an error when registration fails", function() {
|
||||
mozLoop.ensureRegistered.callsArgWith(0, "offline");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, "offline");
|
||||
});
|
||||
|
||||
it("should post to /call-url/", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(hawkRequestStub);
|
||||
sinon.assert.calledWith(hawkRequestStub,
|
||||
"/call-url/", "POST", {callerId: "foo"});
|
||||
});
|
||||
|
||||
it("should call the callback with the url when the request succeeds", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
// Sets up the hawkRequest stub to trigger the callback with no error
|
||||
// and the url.
|
||||
hawkRequestStub.callsArgWith(3, null,
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledWithExactly(callback, null, callUrlData);
|
||||
});
|
||||
|
||||
it("should note the call url expiry when the request succeeds", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
// Sets up the hawkRequest stub to trigger the callback with no error
|
||||
// and the url.
|
||||
hawkRequestStub.callsArgWith(3, null,
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
// expiresAt is in hours, and noteCallUrlExpiry wants seconds.
|
||||
sinon.assert.calledOnce(mozLoop.noteCallUrlExpiry);
|
||||
sinon.assert.calledWithExactly(mozLoop.noteCallUrlExpiry,
|
||||
60 * 60 * 60);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
// Sets up the hawkRequest stub to trigger the callback with
|
||||
// an error
|
||||
hawkRequestStub.callsArgWith(3, fakeErrorRes);
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
// Sets up the hawkRequest stub to trigger the callback with
|
||||
// an error
|
||||
hawkRequestStub.callsArgWith(3, null, "{}");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallsInfo", function() {
|
||||
it("should prevent launching a conversation when version is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallsInfo();
|
||||
}).to.Throw(Error, /missing required parameter version/);
|
||||
});
|
||||
|
||||
it("should perform a get on /calls", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledOnce(hawkRequestStub);
|
||||
sinon.assert.calledWith(hawkRequestStub,
|
||||
"/calls?version=42", "GET", null);
|
||||
|
||||
});
|
||||
|
||||
it("should request data for all calls", function() {
|
||||
hawkRequestStub.callsArgWith(3, null,
|
||||
'{"calls": [{"apiKey": "fake"}]}');
|
||||
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
hawkRequestStub.callsArgWith(3, fakeErrorRes);
|
||||
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
hawkRequestStub.callsArgWith(3, null, "{}");
|
||||
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -31,15 +31,16 @@
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/client.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/js/desktopRouter.js"></script>
|
||||
<script src="../../content/js/client.js"></script>
|
||||
<script src="../../content/js/conversation.js"></script>
|
||||
<script src="../../content/js/desktopRouter.js"></script>
|
||||
<script src="../../content/js/panel.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="client_test.js"></script>
|
||||
<script src="conversation_test.js"></script>
|
||||
<script src="panel_test.js"></script>
|
||||
<script>
|
||||
|
@ -210,7 +210,7 @@ describe("loop.panel", function() {
|
||||
|
||||
describe("#getCallUrl", function() {
|
||||
it("should reset all pending notifications", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
||||
|
||||
@ -220,7 +220,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
it("should request a call url to the server", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
sandbox.stub(view, "getNickname").returns("foo");
|
||||
@ -232,7 +232,7 @@ describe("loop.panel", function() {
|
||||
});
|
||||
|
||||
it("should set the call url form in a pending state", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||
"requestCallUrl");
|
||||
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
||||
|
||||
@ -248,7 +248,7 @@ describe("loop.panel", function() {
|
||||
sandbox.stub(loop.panel.PanelView.prototype,
|
||||
"clearPending");
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
@ -260,7 +260,7 @@ describe("loop.panel", function() {
|
||||
|
||||
it("should notify the user when the operation failed", function() {
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
@ -1,291 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.shared.Client", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
mozLoop,
|
||||
fakeToken;
|
||||
|
||||
var fakeErrorRes = JSON.stringify({
|
||||
status: "errors",
|
||||
errors: [{
|
||||
location: "url",
|
||||
name: "token",
|
||||
description: "invalid token"
|
||||
}]
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function (xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.shared.Client", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.shared.Client();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallUrl", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
window.navigator.mozLoop = {
|
||||
ensureRegistered: sinon.stub().callsArgWith(0, null),
|
||||
noteCallUrlExpiry: sinon.spy(),
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken)
|
||||
};
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: window.navigator.mozLoop}
|
||||
);
|
||||
});
|
||||
|
||||
it("should ensure loop is registered", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.ensureRegistered);
|
||||
});
|
||||
|
||||
it("should send an error when registration fails", function() {
|
||||
navigator.mozLoop.ensureRegistered.callsArgWith(0, "offline");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, "offline");
|
||||
});
|
||||
|
||||
it("should post to /call-url/", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/call-url/");
|
||||
expect(requests[0].requestBody).to.be.equal('callerId=foo');
|
||||
});
|
||||
|
||||
it("should set the XHR Authorization header", function() {
|
||||
sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
|
||||
client._credentials = {
|
||||
// XXX we probably really want to stub out external module calls
|
||||
// eg deriveHawkCredentials, rather supplying them with valid arguments
|
||||
// like we're doing here:
|
||||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
|
||||
algorithm: 'sha256',
|
||||
user: 'Steve'
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
|
||||
});
|
||||
|
||||
it("should request a call url", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
sinon.assert.calledWithExactly(callback, null, callUrlData);
|
||||
});
|
||||
|
||||
it("should note the call url expiry", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
// expiresAt is in hours, and noteCallUrlExpiry wants seconds.
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
|
||||
60 * 60 * 60);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": {}}');
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallsInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
mozLoop = {
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken)
|
||||
};
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: mozLoop}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when version is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallsInfo();
|
||||
}).to.Throw(Error, /missing required parameter version/);
|
||||
});
|
||||
|
||||
it("should request data for all calls", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls?version=42");
|
||||
expect(requests[0].method).to.be.equal("GET");
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"calls": [{"apiKey": "fake"}]}');
|
||||
sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
|
||||
});
|
||||
|
||||
it("should set the XHR Authorization header", function() {
|
||||
sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
|
||||
// XXX we probably really want to stub out external module calls
|
||||
// eg deriveHawkCredentials, rather supplying them with valid arguments
|
||||
// like we're doing here:
|
||||
client._credentials = {
|
||||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
|
||||
algorithm: 'sha256',
|
||||
user: 'Steve'
|
||||
};
|
||||
|
||||
client.requestCallsInfo("foo", callback);
|
||||
|
||||
expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": {}}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: undefined}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when token is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallInfo();
|
||||
}).to.Throw(Error, /missing.*[Tt]oken/);
|
||||
});
|
||||
|
||||
it("should post data for the given call", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
var sessionData = {
|
||||
sessionId: "one",
|
||||
sessionToken: "two",
|
||||
apiKey: "three"
|
||||
};
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(sessionData));
|
||||
sinon.assert.calledWithExactly(callback, null, sessionData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": "one"}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -19,10 +19,7 @@
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script src="../../content/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script src="../../content/shared/libs/token.js"></script>
|
||||
<script src="../../content/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
<script src="../../standalone/content/libs/webl10n-20130617.js"></script>
|
||||
<script src="../../standalone/content/libs/webl10n-20130617.js"></script>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<script src="vendor/mocha-1.17.1.js"></script>
|
||||
@ -35,13 +32,11 @@
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/client.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="client_test.js"></script>
|
||||
<script src="models_test.js"></script>
|
||||
<script src="views_test.js"></script>
|
||||
<script src="router_test.js"></script>
|
||||
|
@ -52,68 +52,68 @@ describe("loop.shared.models", function() {
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var conversation, reqCallInfoStub, reqCallsInfoStub, fakeBaseServerUrl;
|
||||
var conversation, fakeClient, fakeBaseServerUrl;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
fakeBaseServerUrl = "http://fakeBaseServerUrl";
|
||||
reqCallInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallInfo");
|
||||
reqCallsInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallsInfo");
|
||||
fakeClient = {
|
||||
requestCallInfo: sandbox.stub(),
|
||||
requestCallsInfo: sandbox.stub()
|
||||
};
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
it("call requestCallInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(reqCallInfoStub);
|
||||
sinon.assert.calledWith(reqCallInfoStub, "fakeToken");
|
||||
sinon.assert.calledOnce(fakeClient.requestCallInfo);
|
||||
sinon.assert.calledWith(fakeClient.requestCallInfo, "fakeToken");
|
||||
});
|
||||
|
||||
it("should not call requestCallsInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(reqCallsInfoStub);
|
||||
sinon.assert.notCalled(fakeClient.requestCallsInfo);
|
||||
});
|
||||
|
||||
it("call requestCallsInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(reqCallsInfoStub);
|
||||
sinon.assert.calledWith(reqCallsInfoStub);
|
||||
sinon.assert.calledOnce(fakeClient.requestCallsInfo);
|
||||
sinon.assert.calledWith(fakeClient.requestCallsInfo);
|
||||
});
|
||||
|
||||
it("should not call requestCallInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(reqCallInfoStub);
|
||||
sinon.assert.notCalled(fakeClient.requestCallInfo);
|
||||
});
|
||||
|
||||
it("should update conversation session information from server data",
|
||||
function() {
|
||||
sandbox.stub(conversation, "setReady");
|
||||
reqCallInfoStub.callsArgWith(1, null, fakeSessionData);
|
||||
fakeClient.requestCallInfo.callsArgWith(1, null, fakeSessionData);
|
||||
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
@ -122,14 +122,14 @@ describe("loop.shared.models", function() {
|
||||
});
|
||||
|
||||
it("should trigger a `session:error` on failure", function(done) {
|
||||
reqCallInfoStub.callsArgWith(1,
|
||||
fakeClient.requestCallInfo.callsArgWith(1,
|
||||
new Error("failed: HTTP 400 Bad Request; fake"));
|
||||
|
||||
conversation.on("session:error", function(err) {
|
||||
expect(err.message).to.match(/failed: HTTP 400 Bad Request; fake/);
|
||||
done();
|
||||
}).initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
client: fakeClient,
|
||||
outgoing: true
|
||||
});
|
||||
});
|
||||
|
@ -33,8 +33,10 @@
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneClient.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
<!-- Test scripts -->
|
||||
<script src="standalone_client_test.js"></script>
|
||||
<script src="webapp_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
|
@ -0,0 +1,111 @@
|
||||
/* 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/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
fakeToken;
|
||||
|
||||
var fakeErrorRes = JSON.stringify({
|
||||
status: "errors",
|
||||
errors: [{
|
||||
location: "url",
|
||||
name: "token",
|
||||
description: "invalid token"
|
||||
}]
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function (xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.StandaloneClient();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient(
|
||||
{baseServerUrl: "http://fake.api"}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when token is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallInfo();
|
||||
}).to.Throw(Error, /missing.*[Tt]oken/);
|
||||
});
|
||||
|
||||
it("should post data for the given call", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
var sessionData = {
|
||||
sessionId: "one",
|
||||
sessionToken: "two",
|
||||
apiKey: "three"
|
||||
};
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(sessionData));
|
||||
sinon.assert.calledWithExactly(callback, null, sessionData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": "one"}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -281,10 +281,11 @@ describe("loop.webapp", function() {
|
||||
|
||||
sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
|
||||
sinon.assert.calledOnce(initiate);
|
||||
sinon.assert.calledWith(initiate, {
|
||||
baseServerUrl: loop.webapp.baseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
sinon.assert.calledWith(initiate, sinon.match(function (value) {
|
||||
return !!value.outgoing &&
|
||||
(value.client instanceof loop.StandaloneClient) &&
|
||||
value.client.settings.baseServerUrl === loop.webapp.baseServerUrl;
|
||||
}, "{client: <properly constructed client>, outgoing: true}"));
|
||||
});
|
||||
|
||||
it("should disable current form once session is initiated", function() {
|
||||
|
Loading…
Reference in New Issue
Block a user