tor-browser

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

range-sw.js (7585B)


      1 importScripts('/resources/testharness.js');
      2 
      3 setup({ explicit_done: true });
      4 
      5 function assert_range_request(request, expectedRangeHeader, name) {
      6  assert_equals(request.headers.get('Range'), expectedRangeHeader, name);
      7 }
      8 
      9 async function broadcast(msg) {
     10  for (const client of await clients.matchAll()) {
     11    client.postMessage(msg);
     12  }
     13 }
     14 
     15 addEventListener('fetch', async event => {
     16  /** @type Request */
     17  const request = event.request;
     18  const url = new URL(request.url);
     19  const action = url.searchParams.get('action');
     20 
     21  switch (action) {
     22    case 'range-header-filter-test':
     23      rangeHeaderFilterTest(request);
     24      return;
     25    case 'range-header-passthrough-test':
     26      rangeHeaderPassthroughTest(event);
     27      return;
     28    case 'store-ranged-response':
     29      storeRangedResponse(event);
     30      return;
     31    case 'use-stored-ranged-response':
     32      useStoredRangeResponse(event);
     33      return;
     34    case 'broadcast-accept-encoding':
     35      broadcastAcceptEncoding(event);
     36      return;
     37    case 'record-media-range-request':
     38      return recordMediaRangeRequest(event);
     39    case 'use-media-range-request':
     40      useMediaRangeRequest(event);
     41      return;
     42  }
     43 });
     44 
     45 /**
     46 * @param {Request} request
     47 */
     48 function rangeHeaderFilterTest(request) {
     49  const rangeValue = request.headers.get('Range');
     50 
     51  test(() => {
     52    assert_range_request(new Request(request), rangeValue, `Untampered`);
     53    assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`);
     54    assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`);
     55    assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`);
     56    assert_range_request(request.clone(), rangeValue, `Clone`);
     57  }, "Range headers correctly preserved");
     58 
     59  test(() => {
     60    assert_range_request(new Request(request, { headers: { Range: 'foo' } }), null, `Tampered - range header set`);
     61    assert_range_request(new Request(request, { headers: {} }), null, `Tampered - empty headers set`);
     62    assert_range_request(new Request(request, { mode: 'no-cors' }), null, `Tampered – mode set`);
     63    assert_range_request(new Request(request, { cache: 'no-cache' }), null, `Tampered – cache mode set`);
     64  }, "Range headers correctly removed");
     65 
     66  test(() => {
     67    let headers;
     68 
     69    headers = new Request(request).headers;
     70    headers.delete('does-not-exist');
     71    assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`);
     72 
     73    headers = new Request(request).headers;
     74    headers.append('foo', 'bar');
     75    assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`);
     76 
     77    headers = new Request(request).headers;
     78    headers.set('foo', 'bar');
     79    assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`);
     80 
     81    headers = new Request(request).headers;
     82    headers.append('Range', 'foo');
     83    assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`);
     84 
     85    headers = new Request(request).headers;
     86    headers.set('Range', 'foo');
     87    assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`);
     88 
     89    headers = new Request(request).headers;
     90    headers.append('Accept', 'whatever');
     91    assert_equals(headers.get('Range'), null, `Stripped if header successfully appended`);
     92 
     93    headers = new Request(request).headers;
     94    headers.set('Accept', 'whatever');
     95    assert_equals(headers.get('Range'), null, `Stripped if header successfully set`);
     96 
     97    headers = new Request(request).headers;
     98    headers.delete('Accept');
     99    assert_equals(headers.get('Range'), null, `Stripped if header successfully deleted`);
    100 
    101    headers = new Request(request).headers;
    102    headers.delete('Range');
    103    assert_equals(headers.get('Range'), null, `Stripped if range header successfully deleted`);
    104  }, "Headers correctly filtered");
    105 
    106  done();
    107 }
    108 
    109 function rangeHeaderPassthroughTest(event) {
    110  /** @type Request */
    111  const request = event.request;
    112  const url = new URL(request.url);
    113  const key = url.searchParams.get('range-received-key');
    114 
    115  event.waitUntil(new Promise(resolve => {
    116    promise_test(async () => {
    117      await fetch(event.request);
    118      const response = await fetch('stash-take.py?key=' + key);
    119      assert_equals(await response.json(), 'range-header-received');
    120      resolve();
    121    }, `Include range header in network request`);
    122 
    123    done();
    124  }));
    125 
    126  // Just send back any response, it isn't important for the test.
    127  event.respondWith(new Response(''));
    128 }
    129 
    130 let storedRangeResponseP;
    131 
    132 function storeRangedResponse(event) {
    133  /** @type Request */
    134  const request = event.request;
    135  const id = new URL(request.url).searchParams.get('id');
    136 
    137  storedRangeResponseP = fetch(event.request);
    138  broadcast({ id });
    139 
    140  // Just send back any response, it isn't important for the test.
    141  event.respondWith(new Response(''));
    142 }
    143 
    144 function useStoredRangeResponse(event) {
    145  event.respondWith(async function() {
    146    const response = await storedRangeResponseP;
    147    if (!response) throw Error("Expected stored range response");
    148    return response.clone();
    149  }());
    150 }
    151 
    152 function broadcastAcceptEncoding(event) {
    153  /** @type Request */
    154  const request = event.request;
    155  const id = new URL(request.url).searchParams.get('id');
    156 
    157  broadcast({
    158    id,
    159    acceptEncoding: request.headers.get('Accept-Encoding')
    160  });
    161 
    162  // Just send back any response, it isn't important for the test.
    163  event.respondWith(new Response(''));
    164 }
    165 
    166 let rangeResponse = {};
    167 
    168 async function recordMediaRangeRequest(event) {
    169  /** @type Request */
    170  const request = event.request;
    171  const url = new URL(request.url);
    172  const urlParams = new URLSearchParams(url.search);
    173  const size = urlParams.get("size");
    174  const id = urlParams.get('id');
    175  const key = 'size' + size;
    176 
    177  if (key in rangeResponse) {
    178    // Don't re-fetch ranges we already have.
    179    const clonedResponse = rangeResponse[key].clone();
    180    event.respondWith(clonedResponse);
    181  } else if (event.request.headers.get("range") === "bytes=0-") {
    182    // Generate a bogus 206 response to trigger subsequent range requests
    183    // of the desired size.
    184    const length = urlParams.get("length") + 100;
    185    const body = "A".repeat(Number(size));
    186    event.respondWith(new Response(body, {status: 206, headers: {
    187      "Content-Type": "audio/mp4",
    188      "Content-Range": `bytes 0-1/${length}`
    189    }}));
    190  } else if (event.request.headers.get("range") === `bytes=${Number(size)}-`) {
    191    // Pass through actual range requests which will attempt to fetch up to the
    192    // length in the original response which is bigger than the actual resource
    193    // to make sure 206 and 416 responses are treated the same.
    194    rangeResponse[key] = await fetch(event.request);
    195 
    196    // Let the client know we have the range response for the given ID
    197    broadcast({id});
    198  } else {
    199    event.respondWith(Promise.reject(Error("Invalid Request")));
    200  }
    201 }
    202 
    203 function useMediaRangeRequest(event) {
    204  /** @type Request */
    205  const request = event.request;
    206  const url = new URL(request.url);
    207  const urlParams = new URLSearchParams(url.search);
    208  const size = urlParams.get("size");
    209  const key = 'size' + size;
    210 
    211  // Send a clone of the range response to preload.
    212  if (key in rangeResponse) {
    213    const clonedResponse = rangeResponse[key].clone();
    214    event.respondWith(clonedResponse);
    215  } else {
    216    event.respondWith(Promise.reject(Error("Invalid Request")));
    217  }
    218 }