tor-browser

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

http-cache.js (9369B)


      1 /* global btoa fetch token promise_test step_timeout */
      2 /* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */
      3 
      4 const templates = {
      5  'fresh': {
      6    'response_headers': [
      7      ['Expires', 100000],
      8      ['Last-Modified', 0]
      9    ]
     10  },
     11  'stale': {
     12    'response_headers': [
     13      ['Expires', -5000],
     14      ['Last-Modified', -100000]
     15    ]
     16  },
     17  'lcl_response': {
     18    'response_headers': [
     19      ['Location', 'location_target'],
     20      ['Content-Location', 'content_location_target']
     21    ]
     22  },
     23  'location': {
     24    'query_arg': 'location_target',
     25    'response_headers': [
     26      ['Expires', 100000],
     27      ['Last-Modified', 0]
     28    ]
     29  },
     30  'content_location': {
     31    'query_arg': 'content_location_target',
     32    'response_headers': [
     33      ['Expires', 100000],
     34      ['Last-Modified', 0]
     35    ]
     36  }
     37 }
     38 
     39 const noBodyStatus = new Set([204, 304])
     40 
     41 function makeTest (test) {
     42  return function () {
     43    var uuid = token()
     44    var requests = expandTemplates(test)
     45    var fetchFunctions = makeFetchFunctions(requests, uuid)
     46    return runTest(fetchFunctions, test, requests, uuid)
     47  }
     48 }
     49 
     50 function makeFetchFunctions(requests, uuid) {
     51    var fetchFunctions = []
     52    for (let i = 0; i < requests.length; ++i) {
     53      var config = requests[i];
     54      if (config.skip) {
     55        // Skip request are ones that we expect the browser to make in
     56        // response to a redirect. We don't fetch them again, but
     57        // the server needs them in the config to be able to respond to
     58        // them.
     59        continue;
     60      }
     61      fetchFunctions.push({
     62        code: function (idx) {
     63          var config = requests[idx]
     64          var url = makeTestUrl(uuid, config);
     65          var init = fetchInit(requests, config)
     66          return fetch(url, init)
     67            .then(makeCheckResponse(idx, config))
     68            .then(makeCheckResponseBody(config, uuid), function (reason) {
     69              if ('expected_type' in config && config.expected_type === 'error') {
     70                assert_throws_js(TypeError, function () { throw reason })
     71              } else {
     72                throw reason
     73              }
     74            })
     75        },
     76        pauseAfter: 'pause_after' in requests[i]
     77      })
     78    }
     79    return fetchFunctions
     80 }
     81 
     82 function runTest(fetchFunctions, test, requests, uuid) {
     83    var idx = 0
     84    function runNextStep () {
     85      if (fetchFunctions.length) {
     86        var nextFetchFunction = fetchFunctions.shift()
     87        if (nextFetchFunction.pauseAfter === true) {
     88          return nextFetchFunction.code(idx++)
     89            .then(pause)
     90            .then(runNextStep)
     91        } else {
     92          return nextFetchFunction.code(idx++)
     93            .then(runNextStep)
     94        }
     95      } else {
     96        return Promise.resolve()
     97      }
     98    }
     99 
    100    return runNextStep()
    101      .then(function () {
    102        return getServerState(uuid)
    103      }).then(function (testState) {
    104        checkRequests(test, requests, testState)
    105        return Promise.resolve()
    106      })
    107 }
    108 
    109 function expandTemplates (test) {
    110  var rawRequests = test.requests
    111  var requests = []
    112  for (let i = 0; i < rawRequests.length; i++) {
    113    var request = rawRequests[i]
    114    request.name = test.name
    115    if ('template' in request) {
    116      var template = templates[request['template']]
    117      for (let member in template) {
    118        if (!request.hasOwnProperty(member)) {
    119          request[member] = template[member]
    120        }
    121      }
    122    }
    123    requests.push(request)
    124  }
    125  return requests
    126 }
    127 
    128 function fetchInit (requests, config) {
    129  var init = {
    130    'headers': []
    131  }
    132  if ('request_method' in config) init.method = config['request_method']
    133  // Note: init.headers must be a copy of config['request_headers'] array,
    134  // because new elements are added later.
    135  if ('request_headers' in config) init.headers = [...config['request_headers']];
    136  if ('name' in config) init.headers.push(['Test-Name', config.name])
    137  if ('request_body' in config) init.body = config['request_body']
    138  if ('mode' in config) init.mode = config['mode']
    139  if ('credentials' in config) init.credentials = config['credentials']
    140  if ('cache' in config) init.cache = config['cache']
    141  init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))])
    142  return init
    143 }
    144 
    145 function makeCheckResponse (idx, config) {
    146  return function checkResponse (response) {
    147    var reqNum = idx + 1
    148    var resNum = parseInt(response.headers.get('Server-Request-Count'))
    149    if ('expected_type' in config) {
    150      if (config.expected_type === 'error') {
    151        assert_true(false, `Request ${reqNum} doesn't throw an error`)
    152        return response.text()
    153      }
    154      if (config.expected_type === 'cached') {
    155        assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`)
    156      }
    157      if (config.expected_type === 'not_cached') {
    158        assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`)
    159      }
    160    }
    161    if ('expected_status' in config) {
    162      assert_equals(response.status, config.expected_status,
    163        `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`)
    164    } else if ('response_status' in config && config.response_status[0] != 301) {
    165      assert_equals(response.status, config.response_status[0],
    166        `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`)
    167    } else {
    168      assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`)
    169    }
    170    if ('response_headers' in config) {
    171      config.response_headers.forEach(function (header) {
    172        if (header.len < 3 || header[2] === true) {
    173          assert_equals(response.headers.get(header[0]), header[1],
    174            `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
    175        }
    176      })
    177    }
    178    if ('expected_response_headers' in config) {
    179      config.expected_response_headers.forEach(function (header) {
    180        assert_equals(response.headers.get(header[0]), header[1],
    181          `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`)
    182      })
    183    }
    184    return response.text()
    185  }
    186 }
    187 
    188 function makeCheckResponseBody (config, uuid) {
    189  return function checkResponseBody (resBody) {
    190    var statusCode = 200
    191    if ('response_status' in config) {
    192      statusCode = config.response_status[0]
    193    }
    194    if ('expected_response_text' in config) {
    195      if (config.expected_response_text !== null) {
    196        assert_equals(resBody, config.expected_response_text,
    197          `Response body is "${resBody}", not expected "${config.expected_response_text}"`)
    198      }
    199    } else if ('response_body' in config && config.response_body !== null) {
    200      assert_equals(resBody, config.response_body,
    201        `Response body is "${resBody}", not sent "${config.response_body}"`)
    202    } else if (!noBodyStatus.has(statusCode)) {
    203      assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`)
    204    }
    205  }
    206 }
    207 
    208 function checkRequests (test, requests, testState) {
    209  var testIdx = 0
    210  for (let i = 0; i < requests.length; ++i) {
    211    var expectedValidatingHeaders = []
    212    var config = requests[i]
    213    var serverRequest = testState[testIdx]
    214    var reqNum = i + 1
    215    if ('expected_type' in config) {
    216      if (config.expected_type === 'cached') continue // the server will not see the request
    217      if (config.expected_type === 'etag_validated') {
    218        expectedValidatingHeaders.push('if-none-match')
    219      }
    220      if (config.expected_type === 'lm_validated') {
    221        expectedValidatingHeaders.push('if-modified-since')
    222      }
    223    }
    224    testIdx++
    225    expectedValidatingHeaders.forEach(vhdr => {
    226      assert_own_property(serverRequest.request_headers, vhdr,
    227        `request ${reqNum} doesn't have ${vhdr} header`)
    228    })
    229    if ('expected_request_headers' in config) {
    230      config.expected_request_headers.forEach(expectedHdr => {
    231        assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1],
    232          `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`)
    233      })
    234    }
    235  }
    236  if (test?.check_count && testState) {
    237    assert_equals(requests.length, testState.length);
    238  }
    239 }
    240 
    241 function pause () {
    242  return new Promise(function (resolve, reject) {
    243    step_timeout(function () {
    244      return resolve()
    245    }, 3000)
    246  })
    247 }
    248 
    249 function makeTestUrl (uuid, config) {
    250  var arg = ''
    251  var base_url = ''
    252  if ('base_url' in config) {
    253    base_url = config.base_url
    254  }
    255  if ('query_arg' in config) {
    256    arg = `&target=${config.query_arg}`
    257  }
    258  if ('url_params' in config) {
    259    arg = `${arg}&${config.url_params}`
    260  }
    261  return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}`
    262 }
    263 
    264 function getServerState (uuid) {
    265  return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`)
    266    .then(function (response) {
    267      return response.text()
    268    }).then(function (text) {
    269      return JSON.parse(text) || []
    270    })
    271 }
    272 
    273 function run_tests (tests) {
    274  tests.forEach(function (test) {
    275    promise_test(makeTest(test), test.name)
    276  })
    277 }
    278 
    279 var contentStore = {}
    280 function http_content (csKey) {
    281  if (csKey in contentStore) {
    282    return contentStore[csKey]
    283  } else {
    284    var content = btoa(Math.random() * Date.now())
    285    contentStore[csKey] = content
    286    return content
    287  }
    288 }