|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 "use strict"; |
|
5 |
|
6 Cu.import("resource://gre/modules/Promise.jsm"); |
|
7 Cu.import("resource://services-common/hawkclient.js"); |
|
8 |
|
9 const SECOND_MS = 1000; |
|
10 const MINUTE_MS = SECOND_MS * 60; |
|
11 const HOUR_MS = MINUTE_MS * 60; |
|
12 |
|
13 const TEST_CREDS = { |
|
14 id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", |
|
15 key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", |
|
16 algorithm: "sha256" |
|
17 }; |
|
18 |
|
19 initTestLogging("Trace"); |
|
20 |
|
21 add_task(function test_now() { |
|
22 let client = new HawkClient("https://example.com"); |
|
23 |
|
24 do_check_true(client.now() - Date.now() < SECOND_MS); |
|
25 }); |
|
26 |
|
27 add_task(function test_updateClockOffset() { |
|
28 let client = new HawkClient("https://example.com"); |
|
29 |
|
30 let now = new Date(); |
|
31 let serverDate = now.toUTCString(); |
|
32 |
|
33 // Client's clock is off |
|
34 client.now = () => { return now.valueOf() + HOUR_MS; } |
|
35 |
|
36 client._updateClockOffset(serverDate); |
|
37 |
|
38 // Check that they're close; there will likely be a one-second rounding |
|
39 // error, so checking strict equality will likely fail. |
|
40 // |
|
41 // localtimeOffsetMsec is how many milliseconds to add to the local clock so |
|
42 // that it agrees with the server. We are one hour ahead of the server, so |
|
43 // our offset should be -1 hour. |
|
44 do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) <= SECOND_MS); |
|
45 }); |
|
46 |
|
47 add_task(function test_authenticated_get_request() { |
|
48 let message = "{\"msg\": \"Great Success!\"}"; |
|
49 let method = "GET"; |
|
50 |
|
51 let server = httpd_setup({"/foo": (request, response) => { |
|
52 do_check_true(request.hasHeader("Authorization")); |
|
53 |
|
54 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
55 response.bodyOutputStream.write(message, message.length); |
|
56 } |
|
57 }); |
|
58 |
|
59 let client = new HawkClient(server.baseURI); |
|
60 |
|
61 let response = yield client.request("/foo", method, TEST_CREDS); |
|
62 let result = JSON.parse(response); |
|
63 |
|
64 do_check_eq("Great Success!", result.msg); |
|
65 |
|
66 yield deferredStop(server); |
|
67 }); |
|
68 |
|
69 add_task(function test_authenticated_post_request() { |
|
70 let method = "POST"; |
|
71 |
|
72 let server = httpd_setup({"/foo": (request, response) => { |
|
73 do_check_true(request.hasHeader("Authorization")); |
|
74 |
|
75 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
76 response.setHeader("Content-Type", "application/json"); |
|
77 response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available()); |
|
78 } |
|
79 }); |
|
80 |
|
81 let client = new HawkClient(server.baseURI); |
|
82 |
|
83 let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"}); |
|
84 let result = JSON.parse(response); |
|
85 |
|
86 do_check_eq("bar", result.foo); |
|
87 |
|
88 yield deferredStop(server); |
|
89 }); |
|
90 |
|
91 add_task(function test_credentials_optional() { |
|
92 let method = "GET"; |
|
93 let server = httpd_setup({ |
|
94 "/foo": (request, response) => { |
|
95 do_check_false(request.hasHeader("Authorization")); |
|
96 |
|
97 let message = JSON.stringify({msg: "you're in the friend zone"}); |
|
98 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
99 response.setHeader("Content-Type", "application/json"); |
|
100 response.bodyOutputStream.write(message, message.length); |
|
101 } |
|
102 }); |
|
103 |
|
104 let client = new HawkClient(server.baseURI); |
|
105 let result = yield client.request("/foo", method); // credentials undefined |
|
106 do_check_eq(JSON.parse(result).msg, "you're in the friend zone"); |
|
107 |
|
108 yield deferredStop(server); |
|
109 }); |
|
110 |
|
111 add_task(function test_server_error() { |
|
112 let message = "Ohai!"; |
|
113 let method = "GET"; |
|
114 |
|
115 let server = httpd_setup({"/foo": (request, response) => { |
|
116 response.setStatusLine(request.httpVersion, 418, "I am a Teapot"); |
|
117 response.bodyOutputStream.write(message, message.length); |
|
118 } |
|
119 }); |
|
120 |
|
121 let client = new HawkClient(server.baseURI); |
|
122 |
|
123 try { |
|
124 yield client.request("/foo", method, TEST_CREDS); |
|
125 do_throw("Expected an error"); |
|
126 } catch(err) { |
|
127 do_check_eq(418, err.code); |
|
128 do_check_eq("I am a Teapot", err.message); |
|
129 } |
|
130 |
|
131 yield deferredStop(server); |
|
132 }); |
|
133 |
|
134 add_task(function test_server_error_json() { |
|
135 let message = JSON.stringify({error: "Cannot get ye flask."}); |
|
136 let method = "GET"; |
|
137 |
|
138 let server = httpd_setup({"/foo": (request, response) => { |
|
139 response.setStatusLine(request.httpVersion, 400, "What wouldst thou deau?"); |
|
140 response.bodyOutputStream.write(message, message.length); |
|
141 } |
|
142 }); |
|
143 |
|
144 let client = new HawkClient(server.baseURI); |
|
145 |
|
146 try { |
|
147 yield client.request("/foo", method, TEST_CREDS); |
|
148 do_throw("Expected an error"); |
|
149 } catch(err) { |
|
150 do_check_eq("Cannot get ye flask.", err.error); |
|
151 } |
|
152 |
|
153 yield deferredStop(server); |
|
154 }); |
|
155 |
|
156 add_task(function test_offset_after_request() { |
|
157 let message = "Ohai!"; |
|
158 let method = "GET"; |
|
159 |
|
160 let server = httpd_setup({"/foo": (request, response) => { |
|
161 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
162 response.bodyOutputStream.write(message, message.length); |
|
163 } |
|
164 }); |
|
165 |
|
166 let client = new HawkClient(server.baseURI); |
|
167 let now = Date.now(); |
|
168 client.now = () => { return now + HOUR_MS; }; |
|
169 |
|
170 do_check_eq(client.localtimeOffsetMsec, 0); |
|
171 |
|
172 let response = yield client.request("/foo", method, TEST_CREDS); |
|
173 // Should be about an hour off |
|
174 do_check_true(Math.abs(client.localtimeOffsetMsec + HOUR_MS) < SECOND_MS); |
|
175 |
|
176 yield deferredStop(server); |
|
177 }); |
|
178 |
|
179 add_task(function test_offset_in_hawk_header() { |
|
180 let message = "Ohai!"; |
|
181 let method = "GET"; |
|
182 |
|
183 let server = httpd_setup({ |
|
184 "/first": function(request, response) { |
|
185 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
186 response.bodyOutputStream.write(message, message.length); |
|
187 }, |
|
188 |
|
189 "/second": function(request, response) { |
|
190 // We see a better date now in the ts component of the header |
|
191 let delta = getTimestampDelta(request.getHeader("Authorization")); |
|
192 let message = "Delta: " + delta; |
|
193 |
|
194 // We're now within HAWK's one-minute window. |
|
195 // I hope this isn't a recipe for intermittent oranges ... |
|
196 if (delta < MINUTE_MS) { |
|
197 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
198 } else { |
|
199 response.setStatusLine(request.httpVersion, 400, "Delta: " + delta); |
|
200 } |
|
201 response.bodyOutputStream.write(message, message.length); |
|
202 } |
|
203 }); |
|
204 |
|
205 let client = new HawkClient(server.baseURI); |
|
206 function getOffset() { |
|
207 return client.localtimeOffsetMsec; |
|
208 } |
|
209 |
|
210 client.now = () => { |
|
211 return Date.now() + 12 * HOUR_MS; |
|
212 }; |
|
213 |
|
214 // We begin with no offset |
|
215 do_check_eq(client.localtimeOffsetMsec, 0); |
|
216 yield client.request("/first", method, TEST_CREDS); |
|
217 |
|
218 // After the first server response, our offset is updated to -12 hours. |
|
219 // We should be safely in the window, now. |
|
220 do_check_true(Math.abs(client.localtimeOffsetMsec + 12 * HOUR_MS) < MINUTE_MS); |
|
221 yield client.request("/second", method, TEST_CREDS); |
|
222 |
|
223 yield deferredStop(server); |
|
224 }); |
|
225 |
|
226 add_task(function test_2xx_success() { |
|
227 // Just to ensure that we're not biased toward 200 OK for success |
|
228 let credentials = { |
|
229 id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", |
|
230 key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", |
|
231 algorithm: "sha256" |
|
232 }; |
|
233 let method = "GET"; |
|
234 |
|
235 let server = httpd_setup({"/foo": (request, response) => { |
|
236 response.setStatusLine(request.httpVersion, 202, "Accepted"); |
|
237 } |
|
238 }); |
|
239 |
|
240 let client = new HawkClient(server.baseURI); |
|
241 |
|
242 let response = yield client.request("/foo", method, credentials); |
|
243 |
|
244 // Shouldn't be any content in a 202 |
|
245 do_check_eq(response, ""); |
|
246 |
|
247 yield deferredStop(server); |
|
248 }); |
|
249 |
|
250 add_task(function test_retry_request_on_fail() { |
|
251 let attempts = 0; |
|
252 let credentials = { |
|
253 id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", |
|
254 key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", |
|
255 algorithm: "sha256" |
|
256 }; |
|
257 let method = "GET"; |
|
258 |
|
259 let server = httpd_setup({ |
|
260 "/maybe": function(request, response) { |
|
261 // This path should be hit exactly twice; once with a bad timestamp, and |
|
262 // again when the client retries the request with a corrected timestamp. |
|
263 attempts += 1; |
|
264 do_check_true(attempts <= 2); |
|
265 |
|
266 let delta = getTimestampDelta(request.getHeader("Authorization")); |
|
267 |
|
268 // First time through, we should have a bad timestamp |
|
269 if (attempts === 1) { |
|
270 do_check_true(delta > MINUTE_MS); |
|
271 let message = "never!!!"; |
|
272 response.setStatusLine(request.httpVersion, 401, "Unauthorized"); |
|
273 response.bodyOutputStream.write(message, message.length); |
|
274 return; |
|
275 } |
|
276 |
|
277 // Second time through, timestamp should be corrected by client |
|
278 do_check_true(delta < MINUTE_MS); |
|
279 let message = "i love you!!!"; |
|
280 response.setStatusLine(request.httpVersion, 200, "OK"); |
|
281 response.bodyOutputStream.write(message, message.length); |
|
282 return; |
|
283 } |
|
284 }); |
|
285 |
|
286 let client = new HawkClient(server.baseURI); |
|
287 function getOffset() { |
|
288 return client.localtimeOffsetMsec; |
|
289 } |
|
290 |
|
291 client.now = () => { |
|
292 return Date.now() + 12 * HOUR_MS; |
|
293 }; |
|
294 |
|
295 // We begin with no offset |
|
296 do_check_eq(client.localtimeOffsetMsec, 0); |
|
297 |
|
298 // Request will have bad timestamp; client will retry once |
|
299 let response = yield client.request("/maybe", method, credentials); |
|
300 do_check_eq(response, "i love you!!!"); |
|
301 |
|
302 yield deferredStop(server); |
|
303 }); |
|
304 |
|
305 add_task(function test_multiple_401_retry_once() { |
|
306 // Like test_retry_request_on_fail, but always return a 401 |
|
307 // and ensure that the client only retries once. |
|
308 let attempts = 0; |
|
309 let credentials = { |
|
310 id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", |
|
311 key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", |
|
312 algorithm: "sha256" |
|
313 }; |
|
314 let method = "GET"; |
|
315 |
|
316 let server = httpd_setup({ |
|
317 "/maybe": function(request, response) { |
|
318 // This path should be hit exactly twice; once with a bad timestamp, and |
|
319 // again when the client retries the request with a corrected timestamp. |
|
320 attempts += 1; |
|
321 |
|
322 do_check_true(attempts <= 2); |
|
323 |
|
324 let message = "never!!!"; |
|
325 response.setStatusLine(request.httpVersion, 401, "Unauthorized"); |
|
326 response.bodyOutputStream.write(message, message.length); |
|
327 } |
|
328 }); |
|
329 |
|
330 let client = new HawkClient(server.baseURI); |
|
331 function getOffset() { |
|
332 return client.localtimeOffsetMsec; |
|
333 } |
|
334 |
|
335 client.now = () => { |
|
336 return Date.now() - 12 * HOUR_MS; |
|
337 }; |
|
338 |
|
339 // We begin with no offset |
|
340 do_check_eq(client.localtimeOffsetMsec, 0); |
|
341 |
|
342 // Request will have bad timestamp; client will retry once |
|
343 try { |
|
344 yield client.request("/maybe", method, credentials); |
|
345 do_throw("Expected an error"); |
|
346 } catch (err) { |
|
347 do_check_eq(err.code, 401); |
|
348 } |
|
349 do_check_eq(attempts, 2); |
|
350 |
|
351 yield deferredStop(server); |
|
352 }); |
|
353 |
|
354 add_task(function test_500_no_retry() { |
|
355 // If we get a 500 error, the client should not retry (as it would with a |
|
356 // 401) |
|
357 let credentials = { |
|
358 id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", |
|
359 key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", |
|
360 algorithm: "sha256" |
|
361 }; |
|
362 let method = "GET"; |
|
363 |
|
364 let server = httpd_setup({ |
|
365 "/no-shutup": function() { |
|
366 let message = "Cannot get ye flask."; |
|
367 response.setStatusLine(request.httpVersion, 500, "Internal server error"); |
|
368 response.bodyOutputStream.write(message, message.length); |
|
369 } |
|
370 }); |
|
371 |
|
372 let client = new HawkClient(server.baseURI); |
|
373 function getOffset() { |
|
374 return client.localtimeOffsetMsec; |
|
375 } |
|
376 |
|
377 // Throw off the clock so the HawkClient would want to retry the request if |
|
378 // it could |
|
379 client.now = () => { |
|
380 return Date.now() - 12 * HOUR_MS; |
|
381 }; |
|
382 |
|
383 // Request will 500; no retries |
|
384 try { |
|
385 yield client.request("/no-shutup", method, credentials); |
|
386 do_throw("Expected an error"); |
|
387 } catch(err) { |
|
388 do_check_eq(err.code, 500); |
|
389 } |
|
390 |
|
391 yield deferredStop(server); |
|
392 }); |
|
393 |
|
394 add_task(function test_401_then_500() { |
|
395 // Like test_multiple_401_retry_once, but return a 500 to the |
|
396 // second request, ensuring that the promise is properly rejected |
|
397 // in client.request. |
|
398 let attempts = 0; |
|
399 let credentials = { |
|
400 id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x", |
|
401 key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=", |
|
402 algorithm: "sha256" |
|
403 }; |
|
404 let method = "GET"; |
|
405 |
|
406 let server = httpd_setup({ |
|
407 "/maybe": function(request, response) { |
|
408 // This path should be hit exactly twice; once with a bad timestamp, and |
|
409 // again when the client retries the request with a corrected timestamp. |
|
410 attempts += 1; |
|
411 do_check_true(attempts <= 2); |
|
412 |
|
413 let delta = getTimestampDelta(request.getHeader("Authorization")); |
|
414 |
|
415 // First time through, we should have a bad timestamp |
|
416 // Client will retry |
|
417 if (attempts === 1) { |
|
418 do_check_true(delta > MINUTE_MS); |
|
419 let message = "never!!!"; |
|
420 response.setStatusLine(request.httpVersion, 401, "Unauthorized"); |
|
421 response.bodyOutputStream.write(message, message.length); |
|
422 return; |
|
423 } |
|
424 |
|
425 // Second time through, timestamp should be corrected by client |
|
426 // And fail on the client |
|
427 do_check_true(delta < MINUTE_MS); |
|
428 let message = "Cannot get ye flask."; |
|
429 response.setStatusLine(request.httpVersion, 500, "Internal server error"); |
|
430 response.bodyOutputStream.write(message, message.length); |
|
431 return; |
|
432 } |
|
433 }); |
|
434 |
|
435 let client = new HawkClient(server.baseURI); |
|
436 function getOffset() { |
|
437 return client.localtimeOffsetMsec; |
|
438 } |
|
439 |
|
440 client.now = () => { |
|
441 return Date.now() - 12 * HOUR_MS; |
|
442 }; |
|
443 |
|
444 // We begin with no offset |
|
445 do_check_eq(client.localtimeOffsetMsec, 0); |
|
446 |
|
447 // Request will have bad timestamp; client will retry once |
|
448 try { |
|
449 yield client.request("/maybe", method, credentials); |
|
450 } catch(err) { |
|
451 do_check_eq(err.code, 500); |
|
452 } |
|
453 do_check_eq(attempts, 2); |
|
454 |
|
455 yield deferredStop(server); |
|
456 }); |
|
457 |
|
458 add_task(function throw_if_not_json_body() { |
|
459 let client = new HawkClient("https://example.com"); |
|
460 try { |
|
461 yield client.request("/bogus", "GET", {}, "I am not json"); |
|
462 do_throw("Expected an error"); |
|
463 } catch(err) { |
|
464 do_check_true(!!err.message); |
|
465 } |
|
466 }); |
|
467 |
|
468 // End of tests. |
|
469 // Utility functions follow |
|
470 |
|
471 function getTimestampDelta(authHeader, now=Date.now()) { |
|
472 let tsMS = new Date( |
|
473 parseInt(/ts="(\d+)"/.exec(authHeader)[1], 10) * SECOND_MS); |
|
474 return Math.abs(tsMS - now); |
|
475 } |
|
476 |
|
477 function deferredStop(server) { |
|
478 let deferred = Promise.defer(); |
|
479 server.stop(deferred.resolve); |
|
480 return deferred.promise; |
|
481 } |
|
482 |
|
483 function run_test() { |
|
484 initTestLogging("Trace"); |
|
485 run_next_test(); |
|
486 } |
|
487 |