tor-browser

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

bug418986-2.js (10348B)


      1 // # Bug 418986, part 2.
      2 
      3 /* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */
      4 
      5 const is_chrome_window = window.location.protocol === "chrome:";
      6 
      7 const HTML_NS = "http://www.w3.org/1999/xhtml";
      8 
      9 // Expected values. Format: [name, pref_off_value, pref_on_value]
     10 // If pref_*_value is an array with two values, then we will match
     11 // any value in between those two values. If a value is null, then
     12 // we skip the media query.
     13 var expected_values = [
     14  ["color", null, 8],
     15  ["color-index", null, 0],
     16  ["aspect-ratio", null, window.innerWidth + "/" + window.innerHeight],
     17  [
     18    "device-aspect-ratio",
     19    screen.width + "/" + screen.height,
     20    window.innerWidth + "/" + window.innerHeight,
     21  ],
     22  ["device-height", screen.height + "px", window.innerHeight + "px"],
     23  ["device-width", screen.width + "px", window.innerWidth + "px"],
     24  ["grid", null, 0],
     25  ["height", window.innerHeight + "px", window.innerHeight + "px"],
     26  ["monochrome", null, 0],
     27  // Square is defined as portrait:
     28  [
     29    "orientation",
     30    null,
     31    window.innerWidth > window.innerHeight ? "landscape" : "portrait",
     32  ],
     33  ["resolution", null, "192dpi"],
     34  [
     35    "resolution",
     36    [
     37      0.999 * window.devicePixelRatio + "dppx",
     38      1.001 * window.devicePixelRatio + "dppx",
     39    ],
     40    "2dppx",
     41  ],
     42  ["width", window.innerWidth + "px", window.innerWidth + "px"],
     43  ["-moz-device-pixel-ratio", window.devicePixelRatio, 2],
     44  [
     45    "-moz-device-orientation",
     46    screen.width > screen.height ? "landscape" : "portrait",
     47    window.innerWidth > window.innerHeight ? "landscape" : "portrait",
     48  ],
     49 ];
     50 
     51 // These media queries return value 0 or 1 when the pref is off.
     52 // When the pref is on, they should not match.
     53 var suppressed_toggles = [
     54  "-moz-gtk-csd-available",
     55  "-moz-gtk-csd-minimize-button",
     56  "-moz-gtk-csd-maximize-button",
     57  "-moz-gtk-csd-close-button",
     58  "-moz-gtk-csd-reversed-placement",
     59 ];
     60 
     61 var toggles_enabled_in_content = [];
     62 
     63 // Read the current OS.
     64 var OS = SpecialPowers.Services.appinfo.OS;
     65 
     66 // __keyValMatches(key, val)__.
     67 // Runs a media query and returns true if key matches to val.
     68 var keyValMatches = (key, val) =>
     69  matchMedia("(" + key + ":" + val + ")").matches;
     70 
     71 // __testMatch(key, val)__.
     72 // Attempts to run a media query match for the given key and value.
     73 // If value is an array of two elements [min max], then matches any
     74 // value in-between.
     75 var testMatch = function (key, val) {
     76  if (val === null) {
     77    return;
     78  } else if (Array.isArray(val)) {
     79    ok(
     80      keyValMatches("min-" + key, val[0]) &&
     81        keyValMatches("max-" + key, val[1]),
     82      "Expected " + key + " between " + val[0] + " and " + val[1]
     83    );
     84  } else {
     85    ok(keyValMatches(key, val), "Expected " + key + ":" + val);
     86  }
     87 };
     88 
     89 // __testToggles(resisting)__.
     90 // Test whether we are able to match the "toggle" media queries.
     91 var testToggles = function (resisting) {
     92  suppressed_toggles.forEach(function (key) {
     93    var exists = keyValMatches(key, 0) || keyValMatches(key, 1);
     94    if (!toggles_enabled_in_content.includes(key) && !is_chrome_window) {
     95      ok(!exists, key + " should not exist.");
     96    } else {
     97      ok(exists, key + " should exist.");
     98      if (resisting) {
     99        ok(
    100          keyValMatches(key, 0) && !keyValMatches(key, 1),
    101          "Should always match as false"
    102        );
    103      }
    104    }
    105  });
    106 };
    107 
    108 // __generateHtmlLines(resisting)__.
    109 // Create a series of div elements that look like:
    110 // `<div class='spoof' id='resolution'>resolution</div>`,
    111 // where each line corresponds to a different media query.
    112 var generateHtmlLines = function (resisting) {
    113  let fragment = document.createDocumentFragment();
    114  expected_values.forEach(function ([key, offVal, onVal]) {
    115    let val = resisting ? onVal : offVal;
    116    if (val) {
    117      let div = document.createElementNS(HTML_NS, "div");
    118      div.setAttribute("class", "spoof");
    119      div.setAttribute("id", key);
    120      div.textContent = key;
    121      fragment.appendChild(div);
    122    }
    123  });
    124  suppressed_toggles.forEach(function (key) {
    125    let div = document.createElementNS(HTML_NS, "div");
    126    div.setAttribute("class", "suppress");
    127    div.setAttribute("id", key);
    128    div.textContent = key;
    129    fragment.appendChild(div);
    130  });
    131  return fragment;
    132 };
    133 
    134 // __cssLine__.
    135 // Creates a line of css that looks something like
    136 // `@media (resolution: 1ppx) { .spoof#resolution { background-color: green; } }`.
    137 var cssLine = function (query, clazz, id, color) {
    138  return (
    139    "@media " +
    140    query +
    141    " { ." +
    142    clazz +
    143    "#" +
    144    id +
    145    " { background-color: " +
    146    color +
    147    "; } }\n"
    148  );
    149 };
    150 
    151 // __constructQuery(key, val)__.
    152 // Creates a CSS media query from key and val. If key is an array of
    153 // two elements, constructs a range query (using min- and max-).
    154 var constructQuery = function (key, val) {
    155  return Array.isArray(val)
    156    ? "(min-" + key + ": " + val[0] + ") and (max-" + key + ": " + val[1] + ")"
    157    : "(" + key + ": " + val + ")";
    158 };
    159 
    160 // __mediaQueryCSSLine(key, val, color)__.
    161 // Creates a line containing a CSS media query and a CSS expression.
    162 var mediaQueryCSSLine = function (key, val, color) {
    163  if (val === null) {
    164    return "";
    165  }
    166  return cssLine(constructQuery(key, val), "spoof", key, color);
    167 };
    168 
    169 // __suppressedMediaQueryCSSLine(key, color)__.
    170 // Creates a CSS line that matches the existence of a
    171 // media query that is supposed to be suppressed.
    172 var suppressedMediaQueryCSSLine = function (key, color, suppressed) {
    173  let query = "(" + key + ": 0), (" + key + ": 1)";
    174  return cssLine(query, "suppress", key, color);
    175 };
    176 
    177 // __generateCSSLines(resisting)__.
    178 // Creates a series of lines of CSS, each of which corresponds to
    179 // a different media query. If the query produces a match to the
    180 // expected value, then the element will be colored green.
    181 var generateCSSLines = function (resisting) {
    182  let lines = ".spoof { background-color: red;}\n";
    183  expected_values.forEach(function ([key, offVal, onVal]) {
    184    lines += mediaQueryCSSLine(key, resisting ? onVal : offVal, "green");
    185  });
    186  lines +=
    187    ".suppress { background-color: " + (resisting ? "green" : "red") + ";}\n";
    188  suppressed_toggles.forEach(function (key) {
    189    if (
    190      !toggles_enabled_in_content.includes(key) &&
    191      !resisting &&
    192      !is_chrome_window
    193    ) {
    194      lines += "#" + key + " { background-color: green; }\n";
    195    } else {
    196      lines += suppressedMediaQueryCSSLine(key, "green");
    197    }
    198  });
    199  return lines;
    200 };
    201 
    202 // __green__.
    203 // Returns the computed color style corresponding to green.
    204 var green = "rgb(0, 128, 0)";
    205 
    206 // __testCSS(resisting)__.
    207 // Creates a series of divs and CSS using media queries to set their
    208 // background color. If all media queries match as expected, then
    209 // all divs should have a green background color.
    210 var testCSS = function (resisting) {
    211  document.getElementById("display").appendChild(generateHtmlLines(resisting));
    212  document.getElementById("test-css").textContent = generateCSSLines(resisting);
    213  let cssTestDivs = document.querySelectorAll(".spoof,.suppress");
    214  for (let div of cssTestDivs) {
    215    let color = window.getComputedStyle(div).backgroundColor;
    216    ok(color === green, "CSS for '" + div.id + "'");
    217  }
    218 };
    219 
    220 // __testOSXFontSmoothing(resisting)__.
    221 // When fingerprinting resistance is enabled, the `getComputedStyle`
    222 // should always return `undefined` for `MozOSXFontSmoothing`.
    223 var testOSXFontSmoothing = function (resisting) {
    224  let div = document.createElementNS(HTML_NS, "div");
    225  div.style.MozOsxFontSmoothing = "unset";
    226  document.documentElement.appendChild(div);
    227  let readBack = window.getComputedStyle(div).MozOsxFontSmoothing;
    228  div.remove();
    229  let smoothingPref = SpecialPowers.getBoolPref(
    230    "layout.css.osx-font-smoothing.enabled",
    231    false
    232  );
    233  is(
    234    readBack,
    235    resisting ? "" : smoothingPref ? "auto" : "",
    236    "-moz-osx-font-smoothing"
    237  );
    238 };
    239 
    240 // __sleep(timeoutMs)__.
    241 // Returns a promise that resolves after the given timeout.
    242 var sleep = function (timeoutMs) {
    243  return new Promise(function (resolve, reject) {
    244    window.setTimeout(resolve);
    245  });
    246 };
    247 
    248 // __testMediaQueriesInPictureElements(resisting)__.
    249 // Test to see if media queries are properly spoofed in picture elements
    250 // when we are resisting fingerprinting.
    251 var testMediaQueriesInPictureElements = async function (resisting) {
    252  const MATCH = "/tests/layout/style/test/chrome/match.png";
    253  let container = document.getElementById("pictures");
    254  let testImages = [];
    255  for (let [key, offVal, onVal] of expected_values) {
    256    let expected = resisting ? onVal : offVal;
    257    if (expected) {
    258      let picture = document.createElementNS(HTML_NS, "picture");
    259      let query = constructQuery(key, expected);
    260      ok(matchMedia(query).matches, `${query} should match`);
    261 
    262      let source = document.createElementNS(HTML_NS, "source");
    263      source.setAttribute("srcset", MATCH);
    264      source.setAttribute("media", query);
    265 
    266      let image = document.createElementNS(HTML_NS, "img");
    267      image.setAttribute("title", key + ":" + expected);
    268      image.setAttribute("class", "testImage");
    269      image.setAttribute("src", "/tests/layout/style/test/chrome/mismatch.png");
    270      image.setAttribute("alt", key);
    271 
    272      testImages.push(image);
    273 
    274      picture.appendChild(source);
    275      picture.appendChild(image);
    276      container.appendChild(picture);
    277    }
    278  }
    279  const matchURI = new URL(MATCH, document.baseURI).href;
    280  await sleep(0);
    281  for (let testImage of testImages) {
    282    is(
    283      testImage.currentSrc,
    284      matchURI,
    285      "Media query '" + testImage.title + "' in picture should match."
    286    );
    287  }
    288 };
    289 
    290 // __pushPref(key, value)__.
    291 // Set a pref value asynchronously, returning a promise that resolves
    292 // when it succeeds.
    293 var pushPref = function (key, value) {
    294  return new Promise(function (resolve, reject) {
    295    SpecialPowers.pushPrefEnv({ set: [[key, value]] }, resolve);
    296  });
    297 };
    298 
    299 // __test(isContent)__.
    300 // Run all tests.
    301 var test = async function (isContent) {
    302  for (prefValue of [false, true]) {
    303    await pushPref("privacy.resistFingerprinting", prefValue);
    304    let resisting = prefValue && isContent;
    305    expected_values.forEach(function ([key, offVal, onVal]) {
    306      testMatch(key, resisting ? onVal : offVal);
    307    });
    308    testToggles(resisting);
    309    testCSS(resisting);
    310    if (OS === "Darwin") {
    311      testOSXFontSmoothing(resisting);
    312    }
    313    await testMediaQueriesInPictureElements(resisting);
    314  }
    315 };