tor-browser

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

helpers.js (10557B)


      1 /**
      2 * Helper functions for attribution reporting API tests.
      3 */
      4 
      5 const blankURL = (base = location.origin) => new URL('/attribution-reporting/resources/reporting_origin.py', base);
      6 
      7 const attribution_reporting_promise_test = (f, name) =>
      8    promise_test(async t => {
      9      await resetWptServer();
     10      return f(t);
     11    }, name);
     12 
     13 const resetWptServer = () =>
     14    Promise
     15        .all([
     16          resetAttributionReports(eventLevelReportsUrl),
     17          resetAttributionReports(aggregatableReportsUrl),
     18          resetAttributionReports(eventLevelDebugReportsUrl),
     19          resetAttributionReports(attributionSuccessDebugAggregatableReportsUrl),
     20          resetAttributionReports(verboseDebugReportsUrl),
     21          resetAttributionReports(aggregatableDebugReportsUrl),
     22          resetRegisteredSources(),
     23        ]);
     24 
     25 const eventLevelReportsUrl =
     26    '/.well-known/attribution-reporting/report-event-attribution';
     27 const eventLevelDebugReportsUrl =
     28    '/.well-known/attribution-reporting/debug/report-event-attribution';
     29 const aggregatableReportsUrl =
     30    '/.well-known/attribution-reporting/report-aggregate-attribution';
     31 const attributionSuccessDebugAggregatableReportsUrl =
     32    '/.well-known/attribution-reporting/debug/report-aggregate-attribution';
     33 const verboseDebugReportsUrl =
     34    '/.well-known/attribution-reporting/debug/verbose';
     35 const aggregatableDebugReportsUrl =
     36    '/.well-known/attribution-reporting/debug/report-aggregate-debug';
     37 
     38 const pipeHeaderPattern = /[,)]/g;
     39 
     40 // , and ) in pipe values must be escaped with \
     41 const encodeForPipe = urlString => urlString.replace(pipeHeaderPattern, '\\$&');
     42 
     43 const blankURLWithHeaders = (headers, origin, status) => {
     44  const url = blankURL(origin);
     45 
     46  const parts = headers.map(h => `header(${h.name},${encodeForPipe(h.value)})`);
     47 
     48  if (status !== undefined) {
     49    parts.push(`status(${encodeForPipe(status)})`);
     50  }
     51 
     52  if (parts.length > 0) {
     53    url.searchParams.set('pipe', parts.join('|'));
     54  }
     55 
     56  return url;
     57 };
     58 
     59 /**
     60 * Clears the source registration stash.
     61 */
     62 const resetRegisteredSources = () => {
     63  return fetch(`${blankURL()}?clear-stash=true`);
     64 }
     65 
     66 function prepareAnchorOrArea(tag, referrerPolicy, eligible, url) {
     67  const el = document.createElement(tag);
     68  el.referrerPolicy = referrerPolicy;
     69  el.target = '_blank';
     70  el.textContent = 'link';
     71  if (eligible === null) {
     72    el.attributionSrc = url;
     73    el.href = blankURL();
     74  } else {
     75    el.attributionSrc = '';
     76    el.href = url;
     77  }
     78  return el;
     79 }
     80 
     81 let nextMapId = 0;
     82 
     83 /**
     84 * Method to clear the stash. Takes the URL as parameter. This could be for
     85 * event-level or aggregatable reports.
     86 */
     87 const resetAttributionReports = url => {
     88  // The view of the stash is path-specific (https://web-platform-tests.org/tools/wptserve/docs/stash.html),
     89  // therefore the origin doesn't need to be specified.
     90  url = `${url}?clear_stash=true`;
     91  const options = {
     92    method: 'POST',
     93  };
     94  return fetch(url, options);
     95 };
     96 
     97 const redirectReportsTo = origin => {
     98  return Promise.all([
     99      fetch(`${eventLevelReportsUrl}?redirect_to=${origin}`, {method: 'POST'}),
    100      fetch(`${aggregatableReportsUrl}?redirect_to=${origin}`, {method: 'POST'})
    101    ]);
    102 };
    103 
    104 const getFetchParams = (origin) => {
    105  let credentials;
    106  const headers = [];
    107 
    108  if (!origin || origin === location.origin) {
    109    return {credentials, headers};
    110  }
    111 
    112  // https://fetch.spec.whatwg.org/#http-cors-protocol
    113  headers.push({
    114    name: 'Access-Control-Allow-Origin',
    115    value: '*',
    116  });
    117  return {credentials, headers};
    118 };
    119 
    120 const getDefaultReportingOrigin = () => {
    121  // cross-origin means that the reporting origin differs from the source/destination origin.
    122  const crossOrigin = new URLSearchParams(location.search).get('cross-origin');
    123  return crossOrigin === null ? location.origin : get_host_info().HTTPS_REMOTE_ORIGIN;
    124 };
    125 
    126 const createRedirectChain = (redirects) => {
    127  let redirectTo;
    128 
    129  for (let i = redirects.length - 1; i >= 0; i--) {
    130    const {source, trigger, reportingOrigin} = redirects[i];
    131    const headers = [];
    132 
    133    if (source) {
    134      headers.push({
    135        name: 'Attribution-Reporting-Register-Source',
    136        value: JSON.stringify(source),
    137      });
    138    }
    139 
    140    if (trigger) {
    141      headers.push({
    142        name: 'Attribution-Reporting-Register-Trigger',
    143        value: JSON.stringify(trigger),
    144      });
    145    }
    146 
    147    let status;
    148    if (redirectTo) {
    149      headers.push({name: 'Location', value: redirectTo.toString()});
    150      status = '302';
    151    }
    152 
    153    redirectTo = blankURLWithHeaders(
    154        headers, reportingOrigin || getDefaultReportingOrigin(), status);
    155  }
    156 
    157  return redirectTo;
    158 };
    159 
    160 const registerAttributionSrcByImg = (attributionSrc) => {
    161  const element = document.createElement('img');
    162  element.attributionSrc = attributionSrc;
    163 };
    164 
    165 const registerAttributionSrc = ({
    166  source,
    167  trigger,
    168  method = 'img',
    169  extraQueryParams = {},
    170  reportingOrigin,
    171  extraHeaders = [],
    172  referrerPolicy = '',
    173 }) => {
    174  const searchParams = new URLSearchParams(location.search);
    175 
    176  if (method === 'variant') {
    177    method = searchParams.get('method');
    178  }
    179 
    180  const eligible = searchParams.get('eligible');
    181 
    182  let headers = [];
    183 
    184  if (source) {
    185    headers.push({
    186      name: 'Attribution-Reporting-Register-Source',
    187      value: JSON.stringify(source),
    188    });
    189  }
    190 
    191  if (trigger) {
    192    headers.push({
    193      name: 'Attribution-Reporting-Register-Trigger',
    194      value: JSON.stringify(trigger),
    195    });
    196  }
    197 
    198  let credentials;
    199  if (method === 'fetch') {
    200    const params = getFetchParams(reportingOrigin);
    201    credentials = params.credentials;
    202    headers = headers.concat(params.headers);
    203  }
    204 
    205  headers = headers.concat(extraHeaders);
    206 
    207  const url = blankURLWithHeaders(headers, reportingOrigin);
    208 
    209  Object.entries(extraQueryParams)
    210      .forEach(([key, value]) => url.searchParams.set(key, value));
    211 
    212  switch (method) {
    213    case 'img': {
    214      const img = document.createElement('img');
    215      img.referrerPolicy = referrerPolicy;
    216      if (eligible === null) {
    217        img.attributionSrc = url;
    218      } else {
    219        img.attributionSrc = '';
    220        img.src = url;
    221      }
    222      return 'event';
    223    }
    224    case 'script':
    225      const script = document.createElement('script');
    226      script.referrerPolicy = referrerPolicy;
    227      if (eligible === null) {
    228        script.attributionSrc = url;
    229      } else {
    230        script.attributionSrc = '';
    231        script.src = url;
    232        document.body.appendChild(script);
    233      }
    234      return 'event';
    235    case 'a':
    236      const a = prepareAnchorOrArea('a', referrerPolicy, eligible, url);
    237      document.body.appendChild(a);
    238      test_driver.click(a);
    239      return 'navigation';
    240    case 'area': {
    241      const area = prepareAnchorOrArea('area', referrerPolicy, eligible, url);
    242      const size = 100;
    243      area.coords = `0,0,${size},${size}`;
    244      area.shape = 'rect';
    245      const map = document.createElement('map');
    246      map.name = `map-${nextMapId++}`;
    247      map.append(area);
    248      const img = document.createElement('img');
    249      img.width = size;
    250      img.height = size;
    251      img.useMap = `#${map.name}`;
    252      document.body.append(map, img);
    253      test_driver.click(area);
    254      return 'navigation';
    255    }
    256    case 'open':
    257      test_driver.bless('open window', () => {
    258        const feature = referrerPolicy === 'no-referrer' ? 'noreferrer' : '';
    259        if (eligible === null) {
    260          open(
    261              blankURL(), '_blank',
    262              `attributionsrc=${encodeURIComponent(url)} ${feature}`);
    263        } else {
    264          open(url, '_blank', `attributionsrc ${feature}`);
    265        }
    266      });
    267      return 'navigation';
    268    case 'fetch': {
    269      let attributionReporting;
    270      if (eligible !== null) {
    271        attributionReporting = JSON.parse(eligible);
    272      }
    273      fetch(url, {credentials, attributionReporting, referrerPolicy});
    274      return 'event';
    275    }
    276    case 'xhr':
    277      const req = new XMLHttpRequest();
    278      req.open('GET', url);
    279      if (eligible !== null) {
    280        req.setAttributionReporting(JSON.parse(eligible));
    281      }
    282      req.send();
    283      return 'event';
    284    default:
    285      throw `unknown method "${method}"`;
    286  }
    287 };
    288 
    289 
    290 /**
    291 * Generates a random pseudo-unique source event id.
    292 */
    293 const generateSourceEventId = () => {
    294  return `${Math.round(Math.random() * 10000000000000)}`;
    295 }
    296 
    297 /**
    298 * Delay method that waits for prescribed number of milliseconds.
    299 */
    300 const delay = ms => new Promise(resolve => step_timeout(resolve, ms));
    301 
    302 /**
    303 * Method that polls a particular URL for reports. Once reports
    304 * are received, returns the payload as promise. Returns null if the
    305 * timeout is reached before a report is available.
    306 */
    307 const pollAttributionReports = async (url, origin = location.origin, timeout = 60 * 1000 /*ms*/) => {
    308  let startTime = performance.now();
    309  while (performance.now() - startTime < timeout) {
    310    const resp = await fetch(new URL(url, origin));
    311    const payload = await resp.json();
    312    if (payload.reports.length > 0) {
    313      return payload;
    314    }
    315    await delay(/*ms=*/ 100);
    316  }
    317  return null;
    318 };
    319 
    320 // Verbose debug reporting must have been enabled on the source registration for this to work.
    321 const waitForSourceToBeRegistered = async (sourceId, reportingOrigin) => {
    322  const debugReportPayload = await pollVerboseDebugReports(reportingOrigin);
    323  assert_equals(debugReportPayload.reports.length, 1);
    324  const debugReport = JSON.parse(debugReportPayload.reports[0].body);
    325  assert_equals(debugReport.length, 1);
    326  assert_equals(debugReport[0].type, 'source-success');
    327  assert_equals(debugReport[0].body.source_event_id, sourceId);
    328 };
    329 
    330 const pollEventLevelReports = (origin) =>
    331    pollAttributionReports(eventLevelReportsUrl, origin);
    332 const pollEventLevelDebugReports = (origin) =>
    333    pollAttributionReports(eventLevelDebugReportsUrl, origin);
    334 const pollAggregatableReports = (origin) =>
    335    pollAttributionReports(aggregatableReportsUrl, origin);
    336 const pollAttributionSuccessDebugAggregatableReports = (origin) =>
    337    pollAttributionReports(attributionSuccessDebugAggregatableReportsUrl, origin);
    338 const pollVerboseDebugReports = (origin) =>
    339    pollAttributionReports(verboseDebugReportsUrl, origin);
    340 const pollAggregatableDebugReports = (origin) =>
    341  pollAttributionReports(aggregatableDebugReportsUrl, origin);
    342 
    343 const validateReportHeaders = headers => {
    344  assert_array_equals(headers['content-type'], ['application/json']);
    345  assert_array_equals(headers['cache-control'], ['no-cache']);
    346  assert_own_property(headers, 'user-agent');
    347  assert_not_own_property(headers, 'cookie');
    348  assert_not_own_property(headers, 'referer');
    349 };