|
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 }); |