Bug 1020876 Route desktop client XHRs though the mozLoop API to share hawk implementation with MozLoopService. r=ttaubert
This commit is contained in:
@@ -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.
|
* Returns the current locale of the browser.
|
||||||
*
|
*
|
||||||
@@ -214,7 +201,40 @@ function injectLoopAPI(targetWindow) {
|
|||||||
ringer = null;
|
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);
|
let contentObj = Cu.createObjectIn(targetWindow);
|
||||||
|
|||||||
@@ -147,6 +147,11 @@ let MozLoopServiceInternal = {
|
|||||||
* @param {String} method The request method, e.g. 'POST', 'GET'.
|
* @param {String} method The request method, e.g. 'POST', 'GET'.
|
||||||
* @param {Object} payloadObj An object which is converted to JSON and
|
* @param {Object} payloadObj An object which is converted to JSON and
|
||||||
* transmitted with the request.
|
* 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) {
|
hawkRequest: function(path, method, payloadObj) {
|
||||||
if (!this._hawkClient) {
|
if (!this._hawkClient) {
|
||||||
@@ -481,5 +486,22 @@ this.MozLoopService = {
|
|||||||
"; exception: " + ex);
|
"; exception: " + ex);
|
||||||
return null;
|
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/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/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/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/models.js"></script>
|
||||||
<script type="text/javascript" src="loop/shared/js/router.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/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/desktopRouter.js"></script>
|
||||||
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
||||||
</body>
|
</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() {
|
accept: function() {
|
||||||
window.navigator.mozLoop.stopAlerting();
|
window.navigator.mozLoop.stopAlerting();
|
||||||
this._conversation.initiate({
|
this._conversation.initiate({
|
||||||
baseServerUrl: window.navigator.mozLoop.serverUrl,
|
client: new loop.Client(),
|
||||||
outgoing: false
|
outgoing: false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ loop.panel = (function(_, mozL10n) {
|
|||||||
throw new Error("missing required notifier");
|
throw new Error("missing required notifier");
|
||||||
}
|
}
|
||||||
this.notifier = options.notifier;
|
this.notifier = options.notifier;
|
||||||
this.client = new loop.shared.Client({
|
this.client = new loop.Client();
|
||||||
baseServerUrl: navigator.mozLoop.serverUrl
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getNickname: function() {
|
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/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/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/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/models.js"></script>
|
||||||
<script type="text/javascript" src="loop/shared/js/router.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/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/desktopRouter.js"></script>
|
||||||
<script type="text/javascript" src="loop/js/panel.js"></script>
|
<script type="text/javascript" src="loop/js/panel.js"></script>
|
||||||
</body>
|
</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:
|
* Available options:
|
||||||
*
|
*
|
||||||
* - {String} baseServerUrl The server URL
|
|
||||||
* - {Boolean} outgoing Set to true if this model represents the
|
* - {Boolean} outgoing Set to true if this model represents the
|
||||||
* outgoing call.
|
* 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:
|
* Triggered events:
|
||||||
*
|
*
|
||||||
@@ -75,10 +77,6 @@ loop.shared.models = (function() {
|
|||||||
* @param {Object} options Options object
|
* @param {Object} options Options object
|
||||||
*/
|
*/
|
||||||
initiate: function(options) {
|
initiate: function(options) {
|
||||||
var client = new loop.shared.Client({
|
|
||||||
baseServerUrl: options.baseServerUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleResult(err, sessionData) {
|
function handleResult(err, sessionData) {
|
||||||
/*jshint validthis:true */
|
/*jshint validthis:true */
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -99,10 +97,10 @@ loop.shared.models = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.outgoing) {
|
if (options.outgoing) {
|
||||||
client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
|
options.client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
client.requestCallsInfo(this.get("loopVersion"),
|
options.client.requestCallsInfo(this.get("loopVersion"),
|
||||||
handleResult.bind(this));
|
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_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/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/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/models.js (content/shared/js/models.js)
|
||||||
content/browser/loop/shared/js/router.js (content/shared/js/router.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/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/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/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/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/shared/sounds/Firefox-Long.ogg (content/shared/sounds/Firefox-Long.ogg)
|
||||||
content/browser/loop/libs/l10n.js (content/libs/l10n.js)
|
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/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)
|
content/browser/loop/js/panel.js (content/js/panel.js)
|
||||||
# Partner SDK assets
|
# Partner SDK assets
|
||||||
content/browser/loop/libs/sdk.js (content/libs/sdk.js)
|
content/browser/loop/libs/sdk.js (content/libs/sdk.js)
|
||||||
|
|||||||
@@ -30,10 +30,10 @@
|
|||||||
|
|
||||||
<!-- app scripts -->
|
<!-- app scripts -->
|
||||||
<script type="text/javascript" src="config.js"></script>
|
<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/models.js"></script>
|
||||||
<script type="text/javascript" src="shared/js/views.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="shared/js/router.js"></script>
|
||||||
|
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||||
<script type="text/javascript" src="js/webapp.js"></script>
|
<script type="text/javascript" src="js/webapp.js"></script>
|
||||||
|
|
||||||
<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) {
|
initiate: function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.model.initiate({
|
this.model.initiate({
|
||||||
|
client: new loop.StandaloneClient({
|
||||||
baseServerUrl: baseServerUrl,
|
baseServerUrl: baseServerUrl,
|
||||||
|
}),
|
||||||
outgoing: true
|
outgoing: true
|
||||||
});
|
});
|
||||||
this.disableForm();
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- App scripts -->
|
<!-- App scripts -->
|
||||||
<script src="../../content/shared/js/client.js"></script>
|
|
||||||
<script src="../../content/shared/js/models.js"></script>
|
<script src="../../content/shared/js/models.js"></script>
|
||||||
<script src="../../content/shared/js/router.js"></script>
|
<script src="../../content/shared/js/router.js"></script>
|
||||||
<script src="../../content/shared/js/views.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/conversation.js"></script>
|
||||||
|
<script src="../../content/js/desktopRouter.js"></script>
|
||||||
<script src="../../content/js/panel.js"></script>
|
<script src="../../content/js/panel.js"></script>
|
||||||
|
|
||||||
<!-- Test scripts -->
|
<!-- Test scripts -->
|
||||||
|
<script src="client_test.js"></script>
|
||||||
<script src="conversation_test.js"></script>
|
<script src="conversation_test.js"></script>
|
||||||
<script src="panel_test.js"></script>
|
<script src="panel_test.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ describe("loop.panel", function() {
|
|||||||
|
|
||||||
describe("#getCallUrl", function() {
|
describe("#getCallUrl", function() {
|
||||||
it("should reset all pending notifications", function() {
|
it("should reset all pending notifications", function() {
|
||||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
var requestCallUrl = sandbox.stub(loop.Client.prototype,
|
||||||
"requestCallUrl");
|
"requestCallUrl");
|
||||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
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() {
|
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");
|
"requestCallUrl");
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
var view = new loop.panel.PanelView({notifier: notifier});
|
||||||
sandbox.stub(view, "getNickname").returns("foo");
|
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() {
|
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");
|
"requestCallUrl");
|
||||||
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ describe("loop.panel", function() {
|
|||||||
sandbox.stub(loop.panel.PanelView.prototype,
|
sandbox.stub(loop.panel.PanelView.prototype,
|
||||||
"clearPending");
|
"clearPending");
|
||||||
var requestCallUrl = sandbox.stub(
|
var requestCallUrl = sandbox.stub(
|
||||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||||
cb("fake error");
|
cb("fake error");
|
||||||
});
|
});
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
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() {
|
it("should notify the user when the operation failed", function() {
|
||||||
var requestCallUrl = sandbox.stub(
|
var requestCallUrl = sandbox.stub(
|
||||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
loop.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||||
cb("fake error");
|
cb("fake error");
|
||||||
});
|
});
|
||||||
var view = new loop.panel.PanelView({notifier: notifier});
|
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,9 +19,6 @@
|
|||||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
<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/lodash-2.4.1.js"></script>
|
||||||
<script src="../../content/shared/libs/backbone-1.1.2.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 -->
|
<!-- test dependencies -->
|
||||||
@@ -35,13 +32,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- App scripts -->
|
<!-- App scripts -->
|
||||||
<script src="../../content/shared/js/client.js"></script>
|
|
||||||
<script src="../../content/shared/js/models.js"></script>
|
<script src="../../content/shared/js/models.js"></script>
|
||||||
<script src="../../content/shared/js/views.js"></script>
|
<script src="../../content/shared/js/views.js"></script>
|
||||||
<script src="../../content/shared/js/router.js"></script>
|
<script src="../../content/shared/js/router.js"></script>
|
||||||
|
|
||||||
<!-- Test scripts -->
|
<!-- Test scripts -->
|
||||||
<script src="client_test.js"></script>
|
|
||||||
<script src="models_test.js"></script>
|
<script src="models_test.js"></script>
|
||||||
<script src="views_test.js"></script>
|
<script src="views_test.js"></script>
|
||||||
<script src="router_test.js"></script>
|
<script src="router_test.js"></script>
|
||||||
|
|||||||
@@ -52,68 +52,68 @@ describe("loop.shared.models", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("constructed", function() {
|
describe("constructed", function() {
|
||||||
var conversation, reqCallInfoStub, reqCallsInfoStub, fakeBaseServerUrl;
|
var conversation, fakeClient, fakeBaseServerUrl;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
|
conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
|
||||||
conversation.set("loopToken", "fakeToken");
|
conversation.set("loopToken", "fakeToken");
|
||||||
fakeBaseServerUrl = "http://fakeBaseServerUrl";
|
fakeBaseServerUrl = "http://fakeBaseServerUrl";
|
||||||
reqCallInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
fakeClient = {
|
||||||
"requestCallInfo");
|
requestCallInfo: sandbox.stub(),
|
||||||
reqCallsInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
requestCallsInfo: sandbox.stub()
|
||||||
"requestCallsInfo");
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#initiate", function() {
|
describe("#initiate", function() {
|
||||||
it("call requestCallInfo on the client for outgoing calls",
|
it("call requestCallInfo on the client for outgoing calls",
|
||||||
function() {
|
function() {
|
||||||
conversation.initiate({
|
conversation.initiate({
|
||||||
baseServerUrl: fakeBaseServerUrl,
|
client: fakeClient,
|
||||||
outgoing: true
|
outgoing: true
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon.assert.calledOnce(reqCallInfoStub);
|
sinon.assert.calledOnce(fakeClient.requestCallInfo);
|
||||||
sinon.assert.calledWith(reqCallInfoStub, "fakeToken");
|
sinon.assert.calledWith(fakeClient.requestCallInfo, "fakeToken");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not call requestCallsInfo on the client for outgoing calls",
|
it("should not call requestCallsInfo on the client for outgoing calls",
|
||||||
function() {
|
function() {
|
||||||
conversation.initiate({
|
conversation.initiate({
|
||||||
baseServerUrl: fakeBaseServerUrl,
|
client: fakeClient,
|
||||||
outgoing: true
|
outgoing: true
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon.assert.notCalled(reqCallsInfoStub);
|
sinon.assert.notCalled(fakeClient.requestCallsInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("call requestCallsInfo on the client for incoming calls",
|
it("call requestCallsInfo on the client for incoming calls",
|
||||||
function() {
|
function() {
|
||||||
conversation.initiate({
|
conversation.initiate({
|
||||||
baseServerUrl: fakeBaseServerUrl,
|
client: fakeClient,
|
||||||
outgoing: false
|
outgoing: false
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon.assert.calledOnce(reqCallsInfoStub);
|
sinon.assert.calledOnce(fakeClient.requestCallsInfo);
|
||||||
sinon.assert.calledWith(reqCallsInfoStub);
|
sinon.assert.calledWith(fakeClient.requestCallsInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not call requestCallInfo on the client for incoming calls",
|
it("should not call requestCallInfo on the client for incoming calls",
|
||||||
function() {
|
function() {
|
||||||
conversation.initiate({
|
conversation.initiate({
|
||||||
baseServerUrl: fakeBaseServerUrl,
|
client: fakeClient,
|
||||||
outgoing: false
|
outgoing: false
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon.assert.notCalled(reqCallInfoStub);
|
sinon.assert.notCalled(fakeClient.requestCallInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update conversation session information from server data",
|
it("should update conversation session information from server data",
|
||||||
function() {
|
function() {
|
||||||
sandbox.stub(conversation, "setReady");
|
sandbox.stub(conversation, "setReady");
|
||||||
reqCallInfoStub.callsArgWith(1, null, fakeSessionData);
|
fakeClient.requestCallInfo.callsArgWith(1, null, fakeSessionData);
|
||||||
|
|
||||||
conversation.initiate({
|
conversation.initiate({
|
||||||
baseServerUrl: fakeBaseServerUrl,
|
client: fakeClient,
|
||||||
outgoing: true
|
outgoing: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,14 +122,14 @@ describe("loop.shared.models", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger a `session:error` on failure", function(done) {
|
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"));
|
new Error("failed: HTTP 400 Bad Request; fake"));
|
||||||
|
|
||||||
conversation.on("session:error", function(err) {
|
conversation.on("session:error", function(err) {
|
||||||
expect(err.message).to.match(/failed: HTTP 400 Bad Request; fake/);
|
expect(err.message).to.match(/failed: HTTP 400 Bad Request; fake/);
|
||||||
done();
|
done();
|
||||||
}).initiate({
|
}).initiate({
|
||||||
baseServerUrl: fakeBaseServerUrl,
|
client: fakeClient,
|
||||||
outgoing: true
|
outgoing: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,10 @@
|
|||||||
<script src="../../content/shared/js/models.js"></script>
|
<script src="../../content/shared/js/models.js"></script>
|
||||||
<script src="../../content/shared/js/views.js"></script>
|
<script src="../../content/shared/js/views.js"></script>
|
||||||
<script src="../../content/shared/js/router.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>
|
<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 src="webapp_test.js"></script>
|
||||||
<script>
|
<script>
|
||||||
mocha.run(function () {
|
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(fakeSubmitEvent.preventDefault);
|
||||||
sinon.assert.calledOnce(initiate);
|
sinon.assert.calledOnce(initiate);
|
||||||
sinon.assert.calledWith(initiate, {
|
sinon.assert.calledWith(initiate, sinon.match(function (value) {
|
||||||
baseServerUrl: loop.webapp.baseServerUrl,
|
return !!value.outgoing &&
|
||||||
outgoing: true
|
(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() {
|
it("should disable current form once session is initiated", function() {
|
||||||
|
|||||||
Reference in New Issue
Block a user