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