test_hawkclient.js 15 KB

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