tor-browser

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

request-cache.js (8812B)


      1 /**
      2 * Each test is run twice: once using etag/If-None-Match and once with
      3 * date/If-Modified-Since.  Each test run gets its own URL and randomized
      4 * content and operates independently.
      5 *
      6 * The test steps are run with request_cache.length fetch requests issued
      7 * and their immediate results sanity-checked.  The cache.py server script
      8 * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma,
      9 * and Cache-Control observed headers for each request it receives.  When
     10 * the test fetches have run, this state is retrieved from cache.py and the
     11 * expected_* lists are checked, including their length.
     12 *
     13 * This means that if a request_* fetch is expected to hit the cache and not
     14 * touch the network, then there will be no entry for it in the expect_*
     15 * lists.  AKA (request_cache.length - expected_validation_headers.length)
     16 * should equal the number of cache hits that didn't touch the network.
     17 *
     18 * Test dictionary keys:
     19 * - state: required string that determines whether the Expires response for
     20 *   the fetched document should be set in the future ("fresh") or past
     21 *   ("stale").
     22 * - vary: optional string to be passed to the server for it to quote back
     23 *   in a Vary header on the response to us.
     24 * - cache_control: optional string to be passed to the server for it to
     25 *   quote back in a Cache-Control header on the response to us.
     26 * - redirect: optional string "same-origin" or "cross-origin".  If
     27 *   provided, the server will issue an absolute redirect to the script on
     28 *   the same or a different origin, as appropriate.  The redirected
     29 *   location is the script with the redirect parameter removed, so the
     30 *   content/state/etc. will be as if you hadn't specified a redirect.
     31 * - request_cache: required array of cache modes to use (via `cache`).
     32 * - request_headers: optional array of explicit fetch `headers` arguments.
     33 *   If provided, the server will log an empty dictionary for each request
     34 *   instead of the request headers it would normally log.
     35 * - response: optional array of specialized response handling.  Right now,
     36 *   "error" array entries indicate a network error response is expected
     37 *   which will reject with a TypeError.
     38 * - expected_validation_headers: required boolean array indicating whether
     39 *   the server should have seen an If-None-Match/If-Modified-Since header
     40 *   in the request.
     41 * - expected_no_cache_headers: required boolean array indicating whether
     42 *   the server should have seen Pragma/Cache-control:no-cache headers in
     43 *   the request.
     44 * - expected_max_age_headers: optional boolean array indicating whether
     45 *   the server should have seen a Cache-Control:max-age=0 header in the
     46 *   request.
     47 */
     48 
     49 var now = new Date();
     50 
     51 function base_path() {
     52  return location.pathname.replace(/\/[^\/]*$/, '/');
     53 }
     54 function make_url(uuid, id, value, content, info) {
     55  var dates = {
     56    fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(),
     57    stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(),
     58  };
     59  var vary = "";
     60  if ("vary" in info) {
     61    vary = "&vary=" + info.vary;
     62  }
     63  var cache_control = "";
     64  if ("cache_control" in info) {
     65    cache_control = "&cache_control=" + info.cache_control;
     66  }
     67  var redirect = "";
     68 
     69  var ignore_request_headers = "";
     70  if ("request_headers" in info) {
     71    // Ignore the request headers that we send since they may be synthesized by the test.
     72    ignore_request_headers = "&ignore";
     73  }
     74  var url_sans_redirect = "resources/cache.py?token=" + uuid +
     75    "&content=" + content +
     76    "&" + id + "=" + value +
     77    "&expires=" + dates[info.state] +
     78    vary + cache_control + ignore_request_headers;
     79  // If there's a redirect, the target is the script without any redirect at
     80  // either the same domain or a different domain.
     81  if ("redirect" in info) {
     82    var host_info = get_host_info();
     83    var origin;
     84    switch (info.redirect) {
     85      case "same-origin":
     86        origin = host_info['HTTP_ORIGIN'];
     87        break;
     88      case "cross-origin":
     89        origin = host_info['HTTP_REMOTE_ORIGIN'];
     90        break;
     91    }
     92    var redirected_url = origin + base_path() + url_sans_redirect;
     93    return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url);
     94  } else {
     95    return url_sans_redirect;
     96  }
     97 }
     98 function expected_status(type, identifier, init) {
     99  if (type == "date" &&
    100      init.headers &&
    101      init.headers["If-Modified-Since"] == identifier) {
    102    // The server will respond with a 304 in this case.
    103    return [304, "Not Modified"];
    104  }
    105  return [200, "OK"];
    106 }
    107 function expected_response_text(type, identifier, init, content) {
    108  if (type == "date" &&
    109      init.headers &&
    110      init.headers["If-Modified-Since"] == identifier) {
    111    // The server will respond with a 304 in this case.
    112    return "";
    113  }
    114  return content;
    115 }
    116 function server_state(uuid) {
    117  return fetch("resources/cache.py?querystate&token=" + uuid)
    118    .then(function(response) {
    119      return response.text();
    120    }).then(function(text) {
    121      // null will be returned if the server never received any requests
    122      // for the given uuid.  Normalize that to an empty list consistent
    123      // with our representation.
    124      return JSON.parse(text) || [];
    125    });
    126 }
    127 function make_test(type, info) {
    128  return function(test) {
    129    var uuid = token();
    130    var identifier = (type == "tag" ? Math.random() : now.toGMTString());
    131    var content = Math.random().toString();
    132    var url = make_url(uuid, type, identifier, content, info);
    133    var fetch_functions = [];
    134    for (var i = 0; i < info.request_cache.length; ++i) {
    135      fetch_functions.push(function(idx) {
    136        var init = {cache: info.request_cache[idx]};
    137        if ("request_headers" in info) {
    138          init.headers = info.request_headers[idx];
    139        }
    140        if (init.cache === "only-if-cached") {
    141          // only-if-cached requires we use same-origin mode.
    142          init.mode = "same-origin";
    143        }
    144        return fetch(url, init)
    145          .then(function(response) {
    146            if ("response" in info && info.response[idx] === "error") {
    147              assert_true(false, "fetch should have been an error");
    148              return;
    149            }
    150            assert_array_equals([response.status, response.statusText],
    151                                expected_status(type, identifier, init));
    152            return response.text();
    153          }).then(function(text) {
    154            assert_equals(text, expected_response_text(type, identifier, init, content));
    155          }, function(reason) {
    156            if ("response" in info && info.response[idx] === "error") {
    157              assert_throws_js(TypeError, function() { throw reason; });
    158            } else {
    159              throw reason;
    160            }
    161          });
    162      });
    163    }
    164    var i = 0;
    165    function run_next_step() {
    166      if (fetch_functions.length) {
    167        return fetch_functions.shift()(i++)
    168          .then(run_next_step);
    169      } else {
    170        return Promise.resolve();
    171      }
    172    }
    173    return run_next_step()
    174      .then(function() {
    175        // Now, query the server state
    176        return server_state(uuid);
    177      }).then(function(state) {
    178        var expectedState = [];
    179        info.expected_validation_headers.forEach(function (validate) {
    180          if (validate) {
    181            if (type == "tag") {
    182              expectedState.push({"If-None-Match": '"' + identifier + '"'});
    183            } else {
    184              expectedState.push({"If-Modified-Since": identifier});
    185            }
    186          } else {
    187            expectedState.push({});
    188          }
    189        });
    190        for (var i = 0; i < info.expected_no_cache_headers.length; ++i) {
    191          if (info.expected_no_cache_headers[i]) {
    192            expectedState[i]["Pragma"] = "no-cache";
    193            expectedState[i]["Cache-Control"] = "no-cache";
    194          }
    195        }
    196        if ("expected_max_age_headers" in info) {
    197          for (var i = 0; i < info.expected_max_age_headers.length; ++i) {
    198            if (info.expected_max_age_headers[i]) {
    199              expectedState[i]["Cache-Control"] = "max-age=0";
    200            }
    201          }
    202        }
    203        assert_equals(state.length, expectedState.length);
    204        for (var i = 0; i < state.length; ++i) {
    205          for (var header in state[i]) {
    206            assert_equals(state[i][header], expectedState[i][header]);
    207            delete expectedState[i][header];
    208          }
    209          for (var header in expectedState[i]) {
    210            assert_false(header in state[i]);
    211          }
    212        }
    213      });
    214  };
    215 }
    216 
    217 function run_tests(tests)
    218 {
    219  tests.forEach(function(info) {
    220    promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response");
    221    promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response");
    222  });
    223 }