michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-sync/identity.js"); michael@0: Cu.import("resource://services-sync/jpakeclient.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://testing-common/services/sync/utils.js"); michael@0: michael@0: const JPAKE_LENGTH_SECRET = 8; michael@0: const JPAKE_LENGTH_CLIENTID = 256; michael@0: const KEYEXCHANGE_VERSION = 3; michael@0: michael@0: /* michael@0: * Simple server. michael@0: */ michael@0: michael@0: const SERVER_MAX_GETS = 6; michael@0: michael@0: function check_headers(request) { michael@0: let stack = Components.stack.caller; michael@0: michael@0: // There shouldn't be any Basic auth michael@0: do_check_false(request.hasHeader("Authorization"), stack); michael@0: michael@0: // Ensure key exchange ID is set and the right length michael@0: do_check_true(request.hasHeader("X-KeyExchange-Id"), stack); michael@0: do_check_eq(request.getHeader("X-KeyExchange-Id").length, michael@0: JPAKE_LENGTH_CLIENTID, stack); michael@0: } michael@0: michael@0: function new_channel() { michael@0: // Create a new channel and register it with the server. michael@0: let cid = Math.floor(Math.random() * 10000); michael@0: while (channels[cid]) { michael@0: cid = Math.floor(Math.random() * 10000); michael@0: } michael@0: let channel = channels[cid] = new ServerChannel(); michael@0: server.registerPathHandler("/" + cid, channel.handler()); michael@0: return cid; michael@0: } michael@0: michael@0: let server; michael@0: let channels = {}; // Map channel -> ServerChannel object michael@0: function server_new_channel(request, response) { michael@0: check_headers(request); michael@0: let cid = new_channel(); michael@0: let body = JSON.stringify("" + cid); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(body, body.length); michael@0: } michael@0: michael@0: let error_report; michael@0: function server_report(request, response) { michael@0: check_headers(request); michael@0: michael@0: if (request.hasHeader("X-KeyExchange-Log")) { michael@0: error_report = request.getHeader("X-KeyExchange-Log"); michael@0: } michael@0: michael@0: if (request.hasHeader("X-KeyExchange-Cid")) { michael@0: let cid = request.getHeader("X-KeyExchange-Cid"); michael@0: let channel = channels[cid]; michael@0: if (channel) { michael@0: channel.clear(); michael@0: } michael@0: } michael@0: michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: } michael@0: michael@0: // Hook for test code. michael@0: let hooks = {}; michael@0: function initHooks() { michael@0: hooks.onGET = function onGET(request) {}; michael@0: } michael@0: initHooks(); michael@0: michael@0: function ServerChannel() { michael@0: this.data = ""; michael@0: this.etag = ""; michael@0: this.getCount = 0; michael@0: } michael@0: ServerChannel.prototype = { michael@0: michael@0: GET: function GET(request, response) { michael@0: if (!this.data) { michael@0: response.setStatusLine(request.httpVersion, 404, "Not Found"); michael@0: return; michael@0: } michael@0: michael@0: if (request.hasHeader("If-None-Match")) { michael@0: let etag = request.getHeader("If-None-Match"); michael@0: if (etag == this.etag) { michael@0: response.setStatusLine(request.httpVersion, 304, "Not Modified"); michael@0: hooks.onGET(request); michael@0: return; michael@0: } michael@0: } michael@0: response.setHeader("ETag", this.etag); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(this.data, this.data.length); michael@0: michael@0: // Automatically clear the channel after 6 successful GETs. michael@0: this.getCount += 1; michael@0: if (this.getCount == SERVER_MAX_GETS) { michael@0: this.clear(); michael@0: } michael@0: hooks.onGET(request); michael@0: }, michael@0: michael@0: PUT: function PUT(request, response) { michael@0: if (this.data) { michael@0: do_check_true(request.hasHeader("If-Match")); michael@0: let etag = request.getHeader("If-Match"); michael@0: if (etag != this.etag) { michael@0: response.setHeader("ETag", this.etag); michael@0: response.setStatusLine(request.httpVersion, 412, "Precondition Failed"); michael@0: return; michael@0: } michael@0: } else { michael@0: do_check_true(request.hasHeader("If-None-Match")); michael@0: do_check_eq(request.getHeader("If-None-Match"), "*"); michael@0: } michael@0: michael@0: this.data = readBytesFromInputStream(request.bodyInputStream); michael@0: this.etag = '"' + Utils.sha1(this.data) + '"'; michael@0: response.setHeader("ETag", this.etag); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: }, michael@0: michael@0: clear: function clear() { michael@0: delete this.data; michael@0: }, michael@0: michael@0: handler: function handler() { michael@0: let self = this; michael@0: return function(request, response) { michael@0: check_headers(request); michael@0: let method = self[request.method]; michael@0: return method.apply(self, arguments); michael@0: }; michael@0: } michael@0: michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Controller that throws for everything. michael@0: */ michael@0: let BaseController = { michael@0: displayPIN: function displayPIN() { michael@0: do_throw("displayPIN() shouldn't have been called!"); michael@0: }, michael@0: onPairingStart: function onPairingStart() { michael@0: do_throw("onPairingStart shouldn't have been called!"); michael@0: }, michael@0: onAbort: function onAbort(error) { michael@0: do_throw("Shouldn't have aborted with " + error + "!"); michael@0: }, michael@0: onPaired: function onPaired() { michael@0: do_throw("onPaired() shouldn't have been called!"); michael@0: }, michael@0: onComplete: function onComplete(data) { michael@0: do_throw("Shouldn't have completed with " + data + "!"); michael@0: } michael@0: }; michael@0: michael@0: michael@0: const DATA = {"msg": "eggstreamly sekrit"}; michael@0: const POLLINTERVAL = 50; michael@0: michael@0: function run_test() { michael@0: server = httpd_setup({"/new_channel": server_new_channel, michael@0: "/report": server_report}); michael@0: Svc.Prefs.set("jpake.serverURL", server.baseURI + "/"); michael@0: Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL); michael@0: Svc.Prefs.set("jpake.maxTries", 2); michael@0: Svc.Prefs.set("jpake.firstMsgMaxTries", 5); michael@0: Svc.Prefs.set("jpake.lastMsgMaxTries", 5); michael@0: // Ensure clean up michael@0: Svc.Obs.add("profile-before-change", function() { michael@0: Svc.Prefs.resetBranch(""); michael@0: }); michael@0: michael@0: // Ensure PSM is initialized. michael@0: Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); michael@0: michael@0: // Simulate Sync setup with credentials in place. We want to make michael@0: // sure the J-PAKE requests don't include those data. michael@0: ensureLegacyIdentityManager(); michael@0: setBasicCredentials("johndoe", "ilovejane"); michael@0: michael@0: initTestLogging("Trace"); michael@0: Log.repository.getLogger("Sync.JPAKEClient").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Common.RESTRequest").level = michael@0: Log.Level.Trace; michael@0: run_next_test(); michael@0: } michael@0: michael@0: michael@0: add_test(function test_success_receiveNoPIN() { michael@0: _("Test a successful exchange started by receiveNoPIN()."); michael@0: michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onPaired: function onPaired() { michael@0: _("Pairing successful, sending final payload."); michael@0: do_check_true(pairingStartCalledOnReceiver); michael@0: Utils.nextTick(function() { snd.sendAndComplete(DATA); }); michael@0: }, michael@0: onComplete: function onComplete() {} michael@0: }); michael@0: michael@0: let pairingStartCalledOnReceiver = false; michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: displayPIN: function displayPIN(pin) { michael@0: _("Received PIN " + pin + ". Entering it in the other computer..."); michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); michael@0: }, michael@0: onPairingStart: function onPairingStart() { michael@0: pairingStartCalledOnReceiver = true; michael@0: }, michael@0: onComplete: function onComplete(data) { michael@0: do_check_true(Utils.deepEquals(DATA, data)); michael@0: // Ensure channel was cleared, no error report. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: do_check_eq(error_report, undefined); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_firstMsgMaxTries_timeout() { michael@0: _("Test abort when sender doesn't upload anything."); michael@0: michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: displayPIN: function displayPIN(pin) { michael@0: _("Received PIN " + pin + ". Doing nothing..."); michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: }, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_TIMEOUT); michael@0: // Ensure channel was cleared, error report was sent. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: do_check_eq(error_report, JPAKE_ERROR_TIMEOUT); michael@0: error_report = undefined; michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_firstMsgMaxTries() { michael@0: _("Test that receiver can wait longer for the first message."); michael@0: michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onPaired: function onPaired() { michael@0: _("Pairing successful, sending final payload."); michael@0: Utils.nextTick(function() { snd.sendAndComplete(DATA); }); michael@0: }, michael@0: onComplete: function onComplete() {} michael@0: }); michael@0: michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: displayPIN: function displayPIN(pin) { michael@0: // For the purpose of the tests, the poll interval is 50ms and michael@0: // we're polling up to 5 times for the first exchange (as michael@0: // opposed to 2 times for most of the other exchanges). So let's michael@0: // pretend it took 150ms to enter the PIN on the sender, which should michael@0: // require 3 polls. michael@0: // Rather than using an imprecise timer, we hook into the channel's michael@0: // GET handler to know how long to wait. michael@0: _("Received PIN " + pin + ". Waiting for three polls before entering it into sender..."); michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: let count = 0; michael@0: hooks.onGET = function onGET(request) { michael@0: if (++count == 3) { michael@0: _("Third GET. Triggering pair."); michael@0: Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); michael@0: } michael@0: }; michael@0: }, michael@0: onPairingStart: function onPairingStart(pin) {}, michael@0: onComplete: function onComplete(data) { michael@0: do_check_true(Utils.deepEquals(DATA, data)); michael@0: // Ensure channel was cleared, no error report. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: do_check_eq(error_report, undefined); michael@0: michael@0: // Clean up. michael@0: initHooks(); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_lastMsgMaxTries() { michael@0: _("Test that receiver can wait longer for the last message."); michael@0: michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onPaired: function onPaired() { michael@0: // For the purpose of the tests, the poll interval is 50ms and michael@0: // we're polling up to 5 times for the last exchange (as opposed michael@0: // to 2 times for other exchanges). So let's pretend it took michael@0: // 150ms to come up with the final payload, which should require michael@0: // 3 polls. michael@0: // Rather than using an imprecise timer, we hook into the channel's michael@0: // GET handler to know how long to wait. michael@0: let count = 0; michael@0: hooks.onGET = function onGET(request) { michael@0: if (++count == 3) { michael@0: _("Third GET. Triggering send."); michael@0: Utils.nextTick(function() { snd.sendAndComplete(DATA); }); michael@0: } michael@0: }; michael@0: }, michael@0: onComplete: function onComplete() {} michael@0: }); michael@0: michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: displayPIN: function displayPIN(pin) { michael@0: _("Received PIN " + pin + ". Entering it in the other computer..."); michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); michael@0: }, michael@0: onPairingStart: function onPairingStart(pin) {}, michael@0: onComplete: function onComplete(data) { michael@0: do_check_true(Utils.deepEquals(DATA, data)); michael@0: // Ensure channel was cleared, no error report. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: do_check_eq(error_report, undefined); michael@0: michael@0: // Clean up. michael@0: initHooks(); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_wrongPIN() { michael@0: _("Test abort when PINs don't match."); michael@0: michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_KEYMISMATCH); michael@0: do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH); michael@0: error_report = undefined; michael@0: } michael@0: }); michael@0: michael@0: let pairingStartCalledOnReceiver = false; michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: displayPIN: function displayPIN(pin) { michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: let secret = pin.slice(0, JPAKE_LENGTH_SECRET); michael@0: secret = [char for each (char in secret)].reverse().join(""); michael@0: let new_pin = secret + this.cid; michael@0: _("Received PIN " + pin + ", but I'm entering " + new_pin); michael@0: michael@0: Utils.nextTick(function() { snd.pairWithPIN(new_pin, false); }); michael@0: }, michael@0: onPairingStart: function onPairingStart() { michael@0: pairingStartCalledOnReceiver = true; michael@0: }, michael@0: onAbort: function onAbort(error) { michael@0: do_check_true(pairingStartCalledOnReceiver); michael@0: do_check_eq(error, JPAKE_ERROR_NODATA); michael@0: // Ensure channel was cleared. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_abort_receiver() { michael@0: _("Test user abort on receiving side."); michael@0: michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: // Manual abort = userabort. michael@0: do_check_eq(error, JPAKE_ERROR_USERABORT); michael@0: // Ensure channel was cleared. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: do_check_eq(error_report, JPAKE_ERROR_USERABORT); michael@0: error_report = undefined; michael@0: run_next_test(); michael@0: }, michael@0: displayPIN: function displayPIN(pin) { michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: Utils.nextTick(function() { rec.abort(); }); michael@0: } michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_abort_sender() { michael@0: _("Test user abort on sending side."); michael@0: michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: // Manual abort == userabort. michael@0: do_check_eq(error, JPAKE_ERROR_USERABORT); michael@0: do_check_eq(error_report, JPAKE_ERROR_USERABORT); michael@0: error_report = undefined; michael@0: } michael@0: }); michael@0: michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_NODATA); michael@0: // Ensure channel was cleared, no error report. michael@0: do_check_eq(channels[this.cid].data, undefined); michael@0: do_check_eq(error_report, undefined); michael@0: initHooks(); michael@0: run_next_test(); michael@0: }, michael@0: displayPIN: function displayPIN(pin) { michael@0: _("Received PIN " + pin + ". Entering it in the other computer..."); michael@0: this.cid = pin.slice(JPAKE_LENGTH_SECRET); michael@0: Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); michael@0: michael@0: // Abort after the first poll. michael@0: let count = 0; michael@0: hooks.onGET = function onGET(request) { michael@0: if (++count >= 1) { michael@0: _("First GET. Aborting."); michael@0: Utils.nextTick(function() { snd.abort(); }); michael@0: } michael@0: }; michael@0: }, michael@0: onPairingStart: function onPairingStart(pin) {} michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_wrongmessage() { michael@0: let cid = new_channel(); michael@0: let channel = channels[cid]; michael@0: channel.data = JSON.stringify({type: "receiver2", michael@0: version: KEYEXCHANGE_VERSION, michael@0: payload: {}}); michael@0: channel.etag = '"fake-etag"'; michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onComplete: function onComplete(data) { michael@0: do_throw("onComplete shouldn't be called."); michael@0: }, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_WRONGMESSAGE); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: snd.pairWithPIN("01234567" + cid, false); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_error_channel() { michael@0: let serverURL = Svc.Prefs.get("jpake.serverURL"); michael@0: Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); michael@0: michael@0: let rec = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_CHANNEL); michael@0: Svc.Prefs.set("jpake.serverURL", serverURL); michael@0: run_next_test(); michael@0: }, michael@0: onPairingStart: function onPairingStart(pin) {}, michael@0: displayPIN: function displayPIN(pin) {} michael@0: }); michael@0: rec.receiveNoPIN(); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_error_network() { michael@0: let serverURL = Svc.Prefs.get("jpake.serverURL"); michael@0: Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); michael@0: michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_NETWORK); michael@0: Svc.Prefs.set("jpake.serverURL", serverURL); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: snd.pairWithPIN("0123456789ab", false); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_error_server_noETag() { michael@0: let cid = new_channel(); michael@0: let channel = channels[cid]; michael@0: channel.data = JSON.stringify({type: "receiver1", michael@0: version: KEYEXCHANGE_VERSION, michael@0: payload: {}}); michael@0: // This naughty server doesn't supply ETag (well, it supplies empty one). michael@0: channel.etag = ""; michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_SERVER); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: snd.pairWithPIN("01234567" + cid, false); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_error_delayNotSupported() { michael@0: let cid = new_channel(); michael@0: let channel = channels[cid]; michael@0: channel.data = JSON.stringify({type: "receiver1", michael@0: version: 2, michael@0: payload: {}}); michael@0: channel.etag = '"fake-etag"'; michael@0: let snd = new JPAKEClient({ michael@0: __proto__: BaseController, michael@0: onAbort: function onAbort(error) { michael@0: do_check_eq(error, JPAKE_ERROR_DELAYUNSUPPORTED); michael@0: run_next_test(); michael@0: } michael@0: }); michael@0: snd.pairWithPIN("01234567" + cid, true); michael@0: }); michael@0: michael@0: michael@0: add_test(function test_sendAndComplete_notPaired() { michael@0: let snd = new JPAKEClient({__proto__: BaseController}); michael@0: do_check_throws(function () { michael@0: snd.sendAndComplete(DATA); michael@0: }); michael@0: run_next_test(); michael@0: }); michael@0: michael@0: michael@0: add_test(function tearDown() { michael@0: server.stop(run_next_test); michael@0: });