tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }