From 6934ba0b92d60600c2bf05a1a1d52c534557a73b Mon Sep 17 00:00:00 2001 From: lilia Date: Tue, 11 Mar 2014 01:21:28 -0700 Subject: [PATCH 1/3] Refactor Server API functions The details of the server API are now mostly relegated to api.js, and accessed through the API container object, improving modularity and readability, and setting us up to derive a FakeAPI for serverless development. --- background.html | 1 + js/api.js | 145 ++++++++++++++++++++++++++++++++++++++++++++++++ js/helpers.js | 78 +++----------------------- js/options.js | 30 +++++----- options.html | 1 + popup.html | 1 + 6 files changed, 170 insertions(+), 86 deletions(-) create mode 100644 js/api.js diff --git a/background.html b/background.html index 548a7b27..f03238f9 100644 --- a/background.html +++ b/background.html @@ -16,6 +16,7 @@ + diff --git a/js/api.js b/js/api.js new file mode 100644 index 00000000..dae8ecd4 --- /dev/null +++ b/js/api.js @@ -0,0 +1,145 @@ +/************************************************ + *** Utilities to communicate with the server *** + ************************************************/ +var URL_BASE = "http://textsecure-test.herokuapp.com"; +//var URL_BASE = "https://textsecure-service.whispersystems.org"; +var URL_CALLS = {}; +URL_CALLS['accounts'] = "/v1/accounts"; +URL_CALLS['devices'] = "/v1/devices"; +URL_CALLS['keys'] = "/v1/keys"; +URL_CALLS['push'] = "/v1/messagesocket"; +URL_CALLS['messages'] = "/v1/messages/"; + +var API = new function() { + + /** + * REQUIRED PARAMS: + * call: URL_CALLS entry + * httpType: POST/GET/PUT/etc + * OPTIONAL PARAMS: + * success_callback: function(response object) called on success + * error_callback: function(http status code = -1 or != 200) called on failure + * urlParameters: crap appended to the url (probably including a leading /) + * user: user name to be sent in a basic auth header + * password: password to be sent in a basic auth headerA + * do_auth: alternative to user/password where user/password are figured out automagically + * jsonData: JSON data sent in the request body + */ + this.doAjax = function doAjax(param) { + if (param.urlParameters === undefined) + param.urlParameters = ""; + + if (param.do_auth) { + param.user = storage.getUnencrypted("number_id"); + param.password = storage.getEncrypted("password"); + } + + $.ajax(URL_BASE + URL_CALLS[param.call] + param.urlParameters, { + type : param.httpType, + data : param.jsonData && jsonThing(param.jsonData), + contentType : 'application/json; charset=utf-8', + dataType : 'json', + + beforeSend : function(xhr) { + if (param.user !== undefined && + param.password !== undefined) { + xhr.setRequestHeader("Authorization", "Basic " + btoa(getString(param.user) + ":" + getString(param.password))); + } + }, + + success : function(response, textStatus, jqXHR) { + if (param.success_callback !== undefined) + param.success_callback(response); + }, + + error : function(jqXHR, textStatus, errorThrown) { + var code = jqXHR.status; + if (code == 200) { + // happens sometimes when we get no response + // (TODO: Fix server to return 204? instead) + if (param.success_callback !== undefined) + param.success_callback(null); + return; + } + if (code > 999 || code < 100) + code = -1; + if (param.error_callback !== undefined) + param.error_callback(code); + } + }); + }; + + + this.requestVerificationCode = function(number, success_callback, error_callback) { + this.doAjax({ + call : 'accounts', + httpType : 'GET', + urlParameters : '/sms/code/' + number, + success_callback : success_callback, + error_callback : error_callback + }); + }; + + this.confirmCode = function(code, number, password, + signaling_key, single_device, + success_callback, error_callback) { + var call = single_device ? 'accounts' : 'devices'; + var urlPrefix = single_device ? '/code/' : '/'; + + API.doAjax({ + call : call, + httpType : 'PUT', + urlParameters : urlPrefix + code, + user : number, + password : password, + jsonData : { signalingKey : btoa(getString(signaling_key)), + supportsSms : false, + fetchesMessages : true }, + success_callback : success_callback, + error_callback : error_callback + }); + }; + + this.registerKeys = function(keys, success_callback, error_callback) { + this.doAjax({ + call : 'keys', + httpType : 'PUT', + do_auth : true, + jsonData : keys, + success_callback : success_callback, + error_callback : error_callback + }); + }; + + this.getKeysForNumber = function(number, success_callback, error_callback) { + this.doAjax({ + call : 'keys', + httpType : 'GET', + do_auth : true, + urlParameters : "/" + getNumberFromString(number) + "?multikeys", + success_callback : success_callback, + error_callback : error_callback + }); + }; + + this.sendMessages = function(jsonData, success_callback, error_callback) { + this.doAjax({ + call : 'messages', + httpType : 'POST', + do_auth : true, + jsonData : jsonData, + success_callback : success_callback, + error_callback : error_callback + }); + }; + + this.pushMessage = function(messageId) { + this.doAjax({ + call : 'push', + httpType : 'PUT', + urlParameters : '/' + message.id, + do_auth : true + }); + }; + +}(); // API diff --git a/js/helpers.js b/js/helpers.js index 432b4612..d763995b 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -860,65 +860,6 @@ var crypto_tests = {}; }( window.crypto = window.crypto || {}, jQuery )); -/************************************************ - *** Utilities to communicate with the server *** - ************************************************/ -var URL_BASE = "http://textsecure-test.herokuapp.com"; -var URL_CALLS = {}; -URL_CALLS['accounts'] = "/v1/accounts"; -URL_CALLS['devices'] = "/v1/devices"; -URL_CALLS['keys'] = "/v1/keys"; -URL_CALLS['push'] = "/v1/messagesocket"; -URL_CALLS['messages'] = "/v1/messages/"; - -/** - * REQUIRED PARAMS: - * call: URL_CALLS entry - * httpType: POST/GET/PUT/etc - * OPTIONAL PARAMS: - * success_callback: function(response object) called on success - * error_callback: function(http status code = -1 or != 200) called on failure - * urlParameters: crap appended to the url (probably including a leading /) - * user: user name to be sent in a basic auth header - * password: password to be sent in a basic auth headerA - * do_auth: alternative to user/password where user/password are figured out automagically - * jsonData: JSON data sent in the request body - */ -function doAjax(param) { - if (param.urlParameters === undefined) - param.urlParameters = ""; - if (param.do_auth) { - param.user = storage.getUnencrypted("number_id"); - param.password = storage.getEncrypted("password"); - } - $.ajax(URL_BASE + URL_CALLS[param.call] + param.urlParameters, { - type: param.httpType, - data: param.jsonData && jsonThing(param.jsonData), - contentType: 'application/json; charset=utf-8', - dataType: 'json', - beforeSend: function(xhr) { - if (param.user !== undefined && param.password !== undefined) - xhr.setRequestHeader("Authorization", "Basic " + btoa(getString(param.user) + ":" + getString(param.password))); - }, - success: function(response, textStatus, jqXHR) { - if (param.success_callback !== undefined) - param.success_callback(response); - }, - error: function(jqXHR, textStatus, errorThrown) { - var code = jqXHR.status; - if (code == 200) {// happens sometimes when we get no response (TODO: Fix server to return 204? instead) - if (param.success_callback !== undefined) - param.success_callback(null); - return; - } - if (code > 999 || code < 100) - code = -1; - if (param.error_callback !== undefined) - param.error_callback(code); - }, - cache: false - }); -} // message_callback(decoded_protobuf) (use decodeMessage(proto)) function subscribeToPush(message_callback) { @@ -951,7 +892,7 @@ function subscribeToPush(message_callback) { // After this point, a) decoding errors are not the server's fault, and // b) we should handle them gracefully and tell the user they received an invalid message - doAjax({call: 'push', httpType: 'PUT', urlParameters: '/' + message.id, do_auth: true}); + API.pushMessage(message.id); } catch (e) { console.log("Error decoding message: " + e); return; @@ -974,8 +915,8 @@ function subscribeToPush(message_callback) { // success_callback(identity_key), error_callback(error_msg) function getKeysForNumber(number, success_callback, error_callback) { - doAjax({call: 'keys', httpType: 'GET', do_auth: true, urlParameters: "/" + getNumberFromString(number) + "?multikeys", - success_callback: function(response) { + API.getKeysForNumber(number, + function(response) { for (var i = 0; i < response.length; i++) { try { saveDeviceObject({ @@ -990,10 +931,9 @@ function getKeysForNumber(number, success_callback, error_callback) { } } success_callback(response[0].identityKey); - }, error_callback: function(code) { + }, function(code) { error_callback("Error making HTTP request: " + code); - } - }); + }); } // success_callback(server success/failure map), error_callback(error_msg) @@ -1011,8 +951,8 @@ function sendMessageToDevices(deviceObjectList, message, success_callback, error }; //TODO: need to encrypt with session key? } - doAjax({call: 'messages', httpType: 'POST', do_auth: true, jsonData: jsonData, - success_callback: function(result) { + API.sendMessages(jsonData, + function(result) { if (result.missingDeviceIds.length > 0) { var responsesLeft = result.missingDeviceIds.length; var errorThrown = 0; @@ -1030,10 +970,10 @@ function sendMessageToDevices(deviceObjectList, message, success_callback, error } else { success_callback(result); } - }, error_callback: function(code) { + }, function(code) { error_callback("Failed to conect to data channel: " + code); } - }); + ); } // success_callback(success/failure map, see second-to-last line), error_callback(error_msg) diff --git a/js/options.js b/js/options.js index 5f4bb587..8efb76cb 100644 --- a/js/options.js +++ b/js/options.js @@ -22,7 +22,7 @@ $('#number').on('change', function() {//TODO $('#number').attr('style', ''); }); -var single_device = false; +var single_device = true; var signaling_key = getRandomBytes(32 + 20); var password = btoa(getRandomBytes(16)); password = password.substring(0, password.length - 2); @@ -38,12 +38,12 @@ $('#init-go-single-client').click(function() { single_device = true; - doAjax({call: 'accounts', httpType: 'GET', urlParameters: '/sms/code/' + number, - success_callback: function(response) { }, - error_callback: function(code) { + API.requestVerificationCode(number, + function(response) { }, + function(code) { alert("Failed to send key?" + code); //TODO } - }); + ); } }); @@ -58,12 +58,8 @@ $('#init-go').click(function() { $('#verify4done').html(''); $('#verify').show(); - var call = single_device ? 'accounts' : 'devices'; - var urlPrefix = single_device ? '/code/' : '/'; - - doAjax({call: call, httpType: 'PUT', urlParameters: urlPrefix + $('#code').val(), user: number, password: password, - jsonData: {signalingKey: btoa(getString(signaling_key)), supportsSms: false, fetchesMessages: true}, - success_callback: function(response) { + API.confirmCode(code, number, password, signaling_key, single_device, + function(response) { if (single_device) response = 1; var number_id = number + "." + response; @@ -76,16 +72,16 @@ $('#init-go').click(function() { $('#verify2done').html('done'); crypto.generateKeys(function(keys) { $('#verify3done').html('done'); - doAjax({call: 'keys', httpType: 'PUT', do_auth: true, jsonData: keys, - success_callback: function(response) { + API.registerKeys(keys, + function(response) { $('#complete-number').html(number); $('#verify').hide(); $('#setup-complete').show(); registrationDone(); - }, error_callback: function(code) { + }, function(code) { alert(code); //TODO } - }); + ); }); } @@ -102,7 +98,7 @@ $('#init-go').click(function() { } else { register_keys_func(); } - }, error_callback: function(code) { + }, function(code) { var error; switch(code) { case 403: @@ -117,7 +113,7 @@ $('#init-go').click(function() { } alert(error); //TODO } - }); + ); } }); diff --git a/options.html b/options.html index 4aa16b6e..fad6e53c 100644 --- a/options.html +++ b/options.html @@ -40,6 +40,7 @@ + diff --git a/popup.html b/popup.html index 49408cef..6c2d39d8 100644 --- a/popup.html +++ b/popup.html @@ -29,6 +29,7 @@ + From fe1b5435aacf4b3c757fa7908abaa260006c6745 Mon Sep 17 00:00:00 2001 From: lilia Date: Fri, 14 Mar 2014 00:47:13 -0700 Subject: [PATCH 2/3] Add FakeWhisperAPI for serverless development When included after api.js, fake_api.js inits a FakeWhisperAPI. FakeWhisperAPI inherits the methods of API, overrides a few, and then usurps its place as the one true API. Single device mode successfully "registers" against FakeAPI. Sadly, multidevice mode has a recursive loop somewhere that makes the callstack asplode. --- background.html | 1 + js/fake_api.js | 27 +++++++++++++++++++++++++++ options.html | 1 + popup.html | 1 + 4 files changed, 30 insertions(+) create mode 100644 js/fake_api.js diff --git a/background.html b/background.html index f03238f9..d4a7773a 100644 --- a/background.html +++ b/background.html @@ -17,6 +17,7 @@ + diff --git a/js/fake_api.js b/js/fake_api.js new file mode 100644 index 00000000..8d1e099a --- /dev/null +++ b/js/fake_api.js @@ -0,0 +1,27 @@ +var FakeWhisperAPI = function() { + + this.doAjax = function(param) { + if (param.success_callback) { + setTimeout(param.success_callback, 100, param.response); + } + } + + this.getKeysForNumber = function(number, success_callback, error_callback) { + this.doAjax({ success_callback: success_callback, + response : [{ identityKey: 1, + deviceId : 1, + publicKey : 1, + keyId : 1 }] + }); + } + + this.sendMessages = function(jsonData, success_callback, error_callback) { + this.doAjax({ success_callback: success_callback, + response : { missingDeviceIds: [] } + }); + } +}; + +FakeWhisperAPI.prototype = API; +API = new FakeWhisperAPI(); + diff --git a/options.html b/options.html index fad6e53c..b5d34b2d 100644 --- a/options.html +++ b/options.html @@ -41,6 +41,7 @@ + diff --git a/popup.html b/popup.html index 6c2d39d8..da4b9b72 100644 --- a/popup.html +++ b/popup.html @@ -30,6 +30,7 @@ + From be52e4c3a4a2f37a3649b234a980fc5385fc1dec Mon Sep 17 00:00:00 2001 From: lilia Date: Thu, 20 Mar 2014 00:20:54 -0700 Subject: [PATCH 3/3] Fix infinite recursion in ensureStringed(Array) To ensureStringed(thing), you must first ensureStringed(thing)... This was causing an infintel loop in multidevice mode. --- js/helpers.js | 2 +- js/options.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/helpers.js b/js/helpers.js index d763995b..85125d66 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -139,7 +139,7 @@ function ensureStringed(thing) { else if (thing instanceof Array) { var res = []; for (var i = 0; i < thing.length; i++) - res[i] = ensureStringed(thing); + res[i] = ensureStringed(thing[i]); return res; } else if (thing === Object(thing)) { var res = {}; diff --git a/js/options.js b/js/options.js index 8efb76cb..615f6e66 100644 --- a/js/options.js +++ b/js/options.js @@ -22,7 +22,7 @@ $('#number').on('change', function() {//TODO $('#number').attr('style', ''); }); -var single_device = true; +var single_device = false; var signaling_key = getRandomBytes(32 + 20); var password = btoa(getRandomBytes(16)); password = password.substring(0, password.length - 2);