services/sync/tests/unit/test_jpakeclient.js

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:1e8cc024d02e
1 Cu.import("resource://gre/modules/Log.jsm");
2 Cu.import("resource://services-sync/identity.js");
3 Cu.import("resource://services-sync/jpakeclient.js");
4 Cu.import("resource://services-sync/constants.js");
5 Cu.import("resource://services-sync/util.js");
6 Cu.import("resource://testing-common/services/sync/utils.js");
7
8 const JPAKE_LENGTH_SECRET = 8;
9 const JPAKE_LENGTH_CLIENTID = 256;
10 const KEYEXCHANGE_VERSION = 3;
11
12 /*
13 * Simple server.
14 */
15
16 const SERVER_MAX_GETS = 6;
17
18 function check_headers(request) {
19 let stack = Components.stack.caller;
20
21 // There shouldn't be any Basic auth
22 do_check_false(request.hasHeader("Authorization"), stack);
23
24 // Ensure key exchange ID is set and the right length
25 do_check_true(request.hasHeader("X-KeyExchange-Id"), stack);
26 do_check_eq(request.getHeader("X-KeyExchange-Id").length,
27 JPAKE_LENGTH_CLIENTID, stack);
28 }
29
30 function new_channel() {
31 // Create a new channel and register it with the server.
32 let cid = Math.floor(Math.random() * 10000);
33 while (channels[cid]) {
34 cid = Math.floor(Math.random() * 10000);
35 }
36 let channel = channels[cid] = new ServerChannel();
37 server.registerPathHandler("/" + cid, channel.handler());
38 return cid;
39 }
40
41 let server;
42 let channels = {}; // Map channel -> ServerChannel object
43 function server_new_channel(request, response) {
44 check_headers(request);
45 let cid = new_channel();
46 let body = JSON.stringify("" + cid);
47 response.setStatusLine(request.httpVersion, 200, "OK");
48 response.bodyOutputStream.write(body, body.length);
49 }
50
51 let error_report;
52 function server_report(request, response) {
53 check_headers(request);
54
55 if (request.hasHeader("X-KeyExchange-Log")) {
56 error_report = request.getHeader("X-KeyExchange-Log");
57 }
58
59 if (request.hasHeader("X-KeyExchange-Cid")) {
60 let cid = request.getHeader("X-KeyExchange-Cid");
61 let channel = channels[cid];
62 if (channel) {
63 channel.clear();
64 }
65 }
66
67 response.setStatusLine(request.httpVersion, 200, "OK");
68 }
69
70 // Hook for test code.
71 let hooks = {};
72 function initHooks() {
73 hooks.onGET = function onGET(request) {};
74 }
75 initHooks();
76
77 function ServerChannel() {
78 this.data = "";
79 this.etag = "";
80 this.getCount = 0;
81 }
82 ServerChannel.prototype = {
83
84 GET: function GET(request, response) {
85 if (!this.data) {
86 response.setStatusLine(request.httpVersion, 404, "Not Found");
87 return;
88 }
89
90 if (request.hasHeader("If-None-Match")) {
91 let etag = request.getHeader("If-None-Match");
92 if (etag == this.etag) {
93 response.setStatusLine(request.httpVersion, 304, "Not Modified");
94 hooks.onGET(request);
95 return;
96 }
97 }
98 response.setHeader("ETag", this.etag);
99 response.setStatusLine(request.httpVersion, 200, "OK");
100 response.bodyOutputStream.write(this.data, this.data.length);
101
102 // Automatically clear the channel after 6 successful GETs.
103 this.getCount += 1;
104 if (this.getCount == SERVER_MAX_GETS) {
105 this.clear();
106 }
107 hooks.onGET(request);
108 },
109
110 PUT: function PUT(request, response) {
111 if (this.data) {
112 do_check_true(request.hasHeader("If-Match"));
113 let etag = request.getHeader("If-Match");
114 if (etag != this.etag) {
115 response.setHeader("ETag", this.etag);
116 response.setStatusLine(request.httpVersion, 412, "Precondition Failed");
117 return;
118 }
119 } else {
120 do_check_true(request.hasHeader("If-None-Match"));
121 do_check_eq(request.getHeader("If-None-Match"), "*");
122 }
123
124 this.data = readBytesFromInputStream(request.bodyInputStream);
125 this.etag = '"' + Utils.sha1(this.data) + '"';
126 response.setHeader("ETag", this.etag);
127 response.setStatusLine(request.httpVersion, 200, "OK");
128 },
129
130 clear: function clear() {
131 delete this.data;
132 },
133
134 handler: function handler() {
135 let self = this;
136 return function(request, response) {
137 check_headers(request);
138 let method = self[request.method];
139 return method.apply(self, arguments);
140 };
141 }
142
143 };
144
145
146 /**
147 * Controller that throws for everything.
148 */
149 let BaseController = {
150 displayPIN: function displayPIN() {
151 do_throw("displayPIN() shouldn't have been called!");
152 },
153 onPairingStart: function onPairingStart() {
154 do_throw("onPairingStart shouldn't have been called!");
155 },
156 onAbort: function onAbort(error) {
157 do_throw("Shouldn't have aborted with " + error + "!");
158 },
159 onPaired: function onPaired() {
160 do_throw("onPaired() shouldn't have been called!");
161 },
162 onComplete: function onComplete(data) {
163 do_throw("Shouldn't have completed with " + data + "!");
164 }
165 };
166
167
168 const DATA = {"msg": "eggstreamly sekrit"};
169 const POLLINTERVAL = 50;
170
171 function run_test() {
172 server = httpd_setup({"/new_channel": server_new_channel,
173 "/report": server_report});
174 Svc.Prefs.set("jpake.serverURL", server.baseURI + "/");
175 Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL);
176 Svc.Prefs.set("jpake.maxTries", 2);
177 Svc.Prefs.set("jpake.firstMsgMaxTries", 5);
178 Svc.Prefs.set("jpake.lastMsgMaxTries", 5);
179 // Ensure clean up
180 Svc.Obs.add("profile-before-change", function() {
181 Svc.Prefs.resetBranch("");
182 });
183
184 // Ensure PSM is initialized.
185 Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
186
187 // Simulate Sync setup with credentials in place. We want to make
188 // sure the J-PAKE requests don't include those data.
189 ensureLegacyIdentityManager();
190 setBasicCredentials("johndoe", "ilovejane");
191
192 initTestLogging("Trace");
193 Log.repository.getLogger("Sync.JPAKEClient").level = Log.Level.Trace;
194 Log.repository.getLogger("Common.RESTRequest").level =
195 Log.Level.Trace;
196 run_next_test();
197 }
198
199
200 add_test(function test_success_receiveNoPIN() {
201 _("Test a successful exchange started by receiveNoPIN().");
202
203 let snd = new JPAKEClient({
204 __proto__: BaseController,
205 onPaired: function onPaired() {
206 _("Pairing successful, sending final payload.");
207 do_check_true(pairingStartCalledOnReceiver);
208 Utils.nextTick(function() { snd.sendAndComplete(DATA); });
209 },
210 onComplete: function onComplete() {}
211 });
212
213 let pairingStartCalledOnReceiver = false;
214 let rec = new JPAKEClient({
215 __proto__: BaseController,
216 displayPIN: function displayPIN(pin) {
217 _("Received PIN " + pin + ". Entering it in the other computer...");
218 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
219 Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
220 },
221 onPairingStart: function onPairingStart() {
222 pairingStartCalledOnReceiver = true;
223 },
224 onComplete: function onComplete(data) {
225 do_check_true(Utils.deepEquals(DATA, data));
226 // Ensure channel was cleared, no error report.
227 do_check_eq(channels[this.cid].data, undefined);
228 do_check_eq(error_report, undefined);
229 run_next_test();
230 }
231 });
232 rec.receiveNoPIN();
233 });
234
235
236 add_test(function test_firstMsgMaxTries_timeout() {
237 _("Test abort when sender doesn't upload anything.");
238
239 let rec = new JPAKEClient({
240 __proto__: BaseController,
241 displayPIN: function displayPIN(pin) {
242 _("Received PIN " + pin + ". Doing nothing...");
243 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
244 },
245 onAbort: function onAbort(error) {
246 do_check_eq(error, JPAKE_ERROR_TIMEOUT);
247 // Ensure channel was cleared, error report was sent.
248 do_check_eq(channels[this.cid].data, undefined);
249 do_check_eq(error_report, JPAKE_ERROR_TIMEOUT);
250 error_report = undefined;
251 run_next_test();
252 }
253 });
254 rec.receiveNoPIN();
255 });
256
257
258 add_test(function test_firstMsgMaxTries() {
259 _("Test that receiver can wait longer for the first message.");
260
261 let snd = new JPAKEClient({
262 __proto__: BaseController,
263 onPaired: function onPaired() {
264 _("Pairing successful, sending final payload.");
265 Utils.nextTick(function() { snd.sendAndComplete(DATA); });
266 },
267 onComplete: function onComplete() {}
268 });
269
270 let rec = new JPAKEClient({
271 __proto__: BaseController,
272 displayPIN: function displayPIN(pin) {
273 // For the purpose of the tests, the poll interval is 50ms and
274 // we're polling up to 5 times for the first exchange (as
275 // opposed to 2 times for most of the other exchanges). So let's
276 // pretend it took 150ms to enter the PIN on the sender, which should
277 // require 3 polls.
278 // Rather than using an imprecise timer, we hook into the channel's
279 // GET handler to know how long to wait.
280 _("Received PIN " + pin + ". Waiting for three polls before entering it into sender...");
281 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
282 let count = 0;
283 hooks.onGET = function onGET(request) {
284 if (++count == 3) {
285 _("Third GET. Triggering pair.");
286 Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
287 }
288 };
289 },
290 onPairingStart: function onPairingStart(pin) {},
291 onComplete: function onComplete(data) {
292 do_check_true(Utils.deepEquals(DATA, data));
293 // Ensure channel was cleared, no error report.
294 do_check_eq(channels[this.cid].data, undefined);
295 do_check_eq(error_report, undefined);
296
297 // Clean up.
298 initHooks();
299 run_next_test();
300 }
301 });
302 rec.receiveNoPIN();
303 });
304
305
306 add_test(function test_lastMsgMaxTries() {
307 _("Test that receiver can wait longer for the last message.");
308
309 let snd = new JPAKEClient({
310 __proto__: BaseController,
311 onPaired: function onPaired() {
312 // For the purpose of the tests, the poll interval is 50ms and
313 // we're polling up to 5 times for the last exchange (as opposed
314 // to 2 times for other exchanges). So let's pretend it took
315 // 150ms to come up with the final payload, which should require
316 // 3 polls.
317 // Rather than using an imprecise timer, we hook into the channel's
318 // GET handler to know how long to wait.
319 let count = 0;
320 hooks.onGET = function onGET(request) {
321 if (++count == 3) {
322 _("Third GET. Triggering send.");
323 Utils.nextTick(function() { snd.sendAndComplete(DATA); });
324 }
325 };
326 },
327 onComplete: function onComplete() {}
328 });
329
330 let rec = new JPAKEClient({
331 __proto__: BaseController,
332 displayPIN: function displayPIN(pin) {
333 _("Received PIN " + pin + ". Entering it in the other computer...");
334 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
335 Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
336 },
337 onPairingStart: function onPairingStart(pin) {},
338 onComplete: function onComplete(data) {
339 do_check_true(Utils.deepEquals(DATA, data));
340 // Ensure channel was cleared, no error report.
341 do_check_eq(channels[this.cid].data, undefined);
342 do_check_eq(error_report, undefined);
343
344 // Clean up.
345 initHooks();
346 run_next_test();
347 }
348 });
349
350 rec.receiveNoPIN();
351 });
352
353
354 add_test(function test_wrongPIN() {
355 _("Test abort when PINs don't match.");
356
357 let snd = new JPAKEClient({
358 __proto__: BaseController,
359 onAbort: function onAbort(error) {
360 do_check_eq(error, JPAKE_ERROR_KEYMISMATCH);
361 do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH);
362 error_report = undefined;
363 }
364 });
365
366 let pairingStartCalledOnReceiver = false;
367 let rec = new JPAKEClient({
368 __proto__: BaseController,
369 displayPIN: function displayPIN(pin) {
370 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
371 let secret = pin.slice(0, JPAKE_LENGTH_SECRET);
372 secret = [char for each (char in secret)].reverse().join("");
373 let new_pin = secret + this.cid;
374 _("Received PIN " + pin + ", but I'm entering " + new_pin);
375
376 Utils.nextTick(function() { snd.pairWithPIN(new_pin, false); });
377 },
378 onPairingStart: function onPairingStart() {
379 pairingStartCalledOnReceiver = true;
380 },
381 onAbort: function onAbort(error) {
382 do_check_true(pairingStartCalledOnReceiver);
383 do_check_eq(error, JPAKE_ERROR_NODATA);
384 // Ensure channel was cleared.
385 do_check_eq(channels[this.cid].data, undefined);
386 run_next_test();
387 }
388 });
389 rec.receiveNoPIN();
390 });
391
392
393 add_test(function test_abort_receiver() {
394 _("Test user abort on receiving side.");
395
396 let rec = new JPAKEClient({
397 __proto__: BaseController,
398 onAbort: function onAbort(error) {
399 // Manual abort = userabort.
400 do_check_eq(error, JPAKE_ERROR_USERABORT);
401 // Ensure channel was cleared.
402 do_check_eq(channels[this.cid].data, undefined);
403 do_check_eq(error_report, JPAKE_ERROR_USERABORT);
404 error_report = undefined;
405 run_next_test();
406 },
407 displayPIN: function displayPIN(pin) {
408 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
409 Utils.nextTick(function() { rec.abort(); });
410 }
411 });
412 rec.receiveNoPIN();
413 });
414
415
416 add_test(function test_abort_sender() {
417 _("Test user abort on sending side.");
418
419 let snd = new JPAKEClient({
420 __proto__: BaseController,
421 onAbort: function onAbort(error) {
422 // Manual abort == userabort.
423 do_check_eq(error, JPAKE_ERROR_USERABORT);
424 do_check_eq(error_report, JPAKE_ERROR_USERABORT);
425 error_report = undefined;
426 }
427 });
428
429 let rec = new JPAKEClient({
430 __proto__: BaseController,
431 onAbort: function onAbort(error) {
432 do_check_eq(error, JPAKE_ERROR_NODATA);
433 // Ensure channel was cleared, no error report.
434 do_check_eq(channels[this.cid].data, undefined);
435 do_check_eq(error_report, undefined);
436 initHooks();
437 run_next_test();
438 },
439 displayPIN: function displayPIN(pin) {
440 _("Received PIN " + pin + ". Entering it in the other computer...");
441 this.cid = pin.slice(JPAKE_LENGTH_SECRET);
442 Utils.nextTick(function() { snd.pairWithPIN(pin, false); });
443
444 // Abort after the first poll.
445 let count = 0;
446 hooks.onGET = function onGET(request) {
447 if (++count >= 1) {
448 _("First GET. Aborting.");
449 Utils.nextTick(function() { snd.abort(); });
450 }
451 };
452 },
453 onPairingStart: function onPairingStart(pin) {}
454 });
455 rec.receiveNoPIN();
456 });
457
458
459 add_test(function test_wrongmessage() {
460 let cid = new_channel();
461 let channel = channels[cid];
462 channel.data = JSON.stringify({type: "receiver2",
463 version: KEYEXCHANGE_VERSION,
464 payload: {}});
465 channel.etag = '"fake-etag"';
466 let snd = new JPAKEClient({
467 __proto__: BaseController,
468 onComplete: function onComplete(data) {
469 do_throw("onComplete shouldn't be called.");
470 },
471 onAbort: function onAbort(error) {
472 do_check_eq(error, JPAKE_ERROR_WRONGMESSAGE);
473 run_next_test();
474 }
475 });
476 snd.pairWithPIN("01234567" + cid, false);
477 });
478
479
480 add_test(function test_error_channel() {
481 let serverURL = Svc.Prefs.get("jpake.serverURL");
482 Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
483
484 let rec = new JPAKEClient({
485 __proto__: BaseController,
486 onAbort: function onAbort(error) {
487 do_check_eq(error, JPAKE_ERROR_CHANNEL);
488 Svc.Prefs.set("jpake.serverURL", serverURL);
489 run_next_test();
490 },
491 onPairingStart: function onPairingStart(pin) {},
492 displayPIN: function displayPIN(pin) {}
493 });
494 rec.receiveNoPIN();
495 });
496
497
498 add_test(function test_error_network() {
499 let serverURL = Svc.Prefs.get("jpake.serverURL");
500 Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/");
501
502 let snd = new JPAKEClient({
503 __proto__: BaseController,
504 onAbort: function onAbort(error) {
505 do_check_eq(error, JPAKE_ERROR_NETWORK);
506 Svc.Prefs.set("jpake.serverURL", serverURL);
507 run_next_test();
508 }
509 });
510 snd.pairWithPIN("0123456789ab", false);
511 });
512
513
514 add_test(function test_error_server_noETag() {
515 let cid = new_channel();
516 let channel = channels[cid];
517 channel.data = JSON.stringify({type: "receiver1",
518 version: KEYEXCHANGE_VERSION,
519 payload: {}});
520 // This naughty server doesn't supply ETag (well, it supplies empty one).
521 channel.etag = "";
522 let snd = new JPAKEClient({
523 __proto__: BaseController,
524 onAbort: function onAbort(error) {
525 do_check_eq(error, JPAKE_ERROR_SERVER);
526 run_next_test();
527 }
528 });
529 snd.pairWithPIN("01234567" + cid, false);
530 });
531
532
533 add_test(function test_error_delayNotSupported() {
534 let cid = new_channel();
535 let channel = channels[cid];
536 channel.data = JSON.stringify({type: "receiver1",
537 version: 2,
538 payload: {}});
539 channel.etag = '"fake-etag"';
540 let snd = new JPAKEClient({
541 __proto__: BaseController,
542 onAbort: function onAbort(error) {
543 do_check_eq(error, JPAKE_ERROR_DELAYUNSUPPORTED);
544 run_next_test();
545 }
546 });
547 snd.pairWithPIN("01234567" + cid, true);
548 });
549
550
551 add_test(function test_sendAndComplete_notPaired() {
552 let snd = new JPAKEClient({__proto__: BaseController});
553 do_check_throws(function () {
554 snd.sendAndComplete(DATA);
555 });
556 run_next_test();
557 });
558
559
560 add_test(function tearDown() {
561 server.stop(run_next_test);
562 });

mercurial