tor-browser

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

browsertime_pageload.js (13771B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* eslint-env node */
      6 
      7 const fs = require("fs");
      8 const http = require("http");
      9 const { logTest, logTask } = require("./utils/profiling");
     10 
     11 const URL = "/secrets/v1/secret/project/perftest/gecko/level-";
     12 const SECRET = "/perftest-login";
     13 const DEFAULT_SERVER = "https://firefox-ci-tc.services.mozilla.com";
     14 
     15 const SCM_LOGIN_SITES = ["facebook", "netflix"];
     16 
     17 /**
     18 * This function obtains the perftest secret from Taskcluster.
     19 *
     20 * It will NOT work locally. Please see the get_logins function, you
     21 * will need to define a JSON file and set the RAPTOR_LOGINS
     22 * env variable to its path.
     23 */
     24 async function get_tc_secrets(context) {
     25  const MOZ_AUTOMATION = process.env.MOZ_AUTOMATION;
     26  if (!MOZ_AUTOMATION) {
     27    throw Error(
     28      "Not running in CI. Set RAPTOR_LOGINS to a JSON file containing the logins."
     29    );
     30  }
     31 
     32  let TASKCLUSTER_PROXY_URL = process.env.TASKCLUSTER_PROXY_URL
     33    ? process.env.TASKCLUSTER_PROXY_URL
     34    : DEFAULT_SERVER;
     35 
     36  let MOZ_SCM_LEVEL = process.env.MOZ_SCM_LEVEL ? process.env.MOZ_SCM_LEVEL : 1;
     37 
     38  const url = TASKCLUSTER_PROXY_URL + URL + MOZ_SCM_LEVEL + SECRET;
     39 
     40  const data = await new Promise((resolve, reject) => {
     41    context.log.info("Obtaining secrets for login...");
     42 
     43    http.get(
     44      url,
     45      {
     46        headers: {
     47          "Content-Type": "application/json",
     48          Accept: "application/json",
     49        },
     50      },
     51      res => {
     52        let data = "";
     53        context.log.info(`Secret status code: ${res.statusCode}`);
     54 
     55        res.on("data", d => {
     56          data += d.toString();
     57        });
     58 
     59        res.on("end", () => {
     60          resolve(data);
     61        });
     62 
     63        res.on("error", error => {
     64          context.log.error(error);
     65          reject(error);
     66        });
     67      }
     68    );
     69  });
     70 
     71  return JSON.parse(data);
     72 }
     73 
     74 /**
     75 * This function gets the login information required.
     76 *
     77 * It starts by looking for a local file whose path is defined
     78 * within RAPTOR_LOGINS. If we don't find this file, then we'll
     79 * attempt to get the login information from our Taskcluster secret.
     80 * If MOZ_AUTOMATION is undefined, then the test will fail, Taskcluster
     81 * secrets can only be obtained in CI.
     82 */
     83 async function get_logins(context) {
     84  let logins;
     85 
     86  let RAPTOR_LOGINS = process.env.RAPTOR_LOGINS;
     87  if (RAPTOR_LOGINS) {
     88    // Get logins from a local file
     89    if (!RAPTOR_LOGINS.endsWith(".json")) {
     90      throw Error(
     91        `File given for logins does not end in '.json': ${RAPTOR_LOGINS}`
     92      );
     93    }
     94 
     95    let logins_file = null;
     96    try {
     97      logins_file = await fs.readFileSync(RAPTOR_LOGINS, "utf8");
     98    } catch (err) {
     99      throw Error(`Failed to read the file ${RAPTOR_LOGINS}: ${err}`);
    100    }
    101 
    102    logins = await JSON.parse(logins_file);
    103  } else {
    104    // Get logins from a perftest Taskcluster secret
    105    logins = await get_tc_secrets(context);
    106  }
    107 
    108  return logins;
    109 }
    110 
    111 /**
    112 * This function returns the type of login to do.
    113 *
    114 * This function returns "single-form" when we find a single form. If we only
    115 * find a single input field, we assume that there is one page per input
    116 * and return "multi-page". Otherwise, we return null.
    117 */
    118 async function get_login_type(context, commands) {
    119  /*
    120    Determine if there's a password field visible with this
    121    query selector. Some sites use `tabIndex` to hide the password
    122    field behind other elements. In this case, we are searching
    123    for any password-type field that has a tabIndex of 0 or undefined and
    124    is not hidden.
    125  */
    126  let input_length = await commands.js.run(`
    127    return document.querySelectorAll(
    128      "input[type=password][tabIndex='0']:not([type=hidden])," +
    129      "input[type=password]:not([tabIndex]):not([type=hidden])"
    130    ).length;
    131  `);
    132 
    133  if (input_length == 0) {
    134    context.log.info("Found a multi-page login");
    135    return multi_page_login;
    136  } else if (input_length == 1) {
    137    context.log.info("Found a single-page login");
    138    return single_page_login;
    139  }
    140 
    141  if (
    142    (await commands.js.run(
    143      `return document.querySelectorAll("form").length;`
    144    )) >= 1
    145  ) {
    146    context.log.info("Found a single-form login");
    147    return single_form_login;
    148  }
    149 
    150  return null;
    151 }
    152 
    153 /**
    154 * This function sets up the login for a single form.
    155 *
    156 * The username field is defined as the field which immediately precedes
    157 * the password field. We have to do this in two steps because we need
    158 * to make sure that the event we emit from the change has the `isTrusted`
    159 * field set to `true`. Otherwise, some websites will ignore the input and
    160 * the form submission.
    161 */
    162 async function single_page_login(login_info, context, commands, prefix = "") {
    163  // Get the first input field in the form that is not hidden and add the
    164  // username. Assumes that email/username is always the first input field.
    165  await commands.addText.bySelector(
    166    login_info.username,
    167    `${prefix}input:not([type=hidden]):not([type=password])`
    168  );
    169 
    170  // Get the password field and ensure it's not hidden.
    171  await commands.addText.bySelector(
    172    login_info.password,
    173    `${prefix}input[type=password]:not([type=hidden])`
    174  );
    175 
    176  return undefined;
    177 }
    178 
    179 /**
    180 * See single_page_login.
    181 */
    182 async function single_form_login(login_info, context, commands) {
    183  return single_page_login(login_info, context, commands, "form ");
    184 }
    185 
    186 /**
    187 * Login to a website that uses multiple pages for the login.
    188 *
    189 * WARNING: Assumes that the first page is for the username.
    190 */
    191 // TODO cleanup comments
    192 async function multi_page_login(login_info, context, commands) {
    193  const driver = context.selenium.driver;
    194  const webdriver = context.selenium.webdriver;
    195  //TODO fails here in netflix for Try...
    196  const username_field = await driver.findElement(
    197    webdriver.By.css(`input:not([type=hidden]):not([type=password])`)
    198  );
    199  await username_field.sendKeys(login_info.username);
    200  await username_field.sendKeys(webdriver.Key.ENTER);
    201  await commands.wait.byTime(5000);
    202  let password_field;
    203  try {
    204    password_field = await driver.findElement(
    205      webdriver.By.css(`input[type=password]:not([type=hidden])`)
    206    );
    207  } catch (err) {
    208    if (err.toString().includes("NoSuchElementError")) {
    209      // Sometimes we're suspicious (i.e. they think we're a bot/bad-actor)
    210      let name_field = await driver.findElement(
    211        webdriver.By.css(`input:not([type=hidden]):not([type=password])`)
    212      );
    213      await name_field.sendKeys(login_info.suspicious_answer);
    214      await name_field.sendKeys(webdriver.Key.ENTER);
    215      await commands.wait.byTime(5000);
    216 
    217      // Try getting the password field again
    218      password_field = await driver.findElement(
    219        webdriver.By.css(`input[type=password]:not([type=hidden])`)
    220      );
    221    } else {
    222      throw err;
    223    }
    224  }
    225 
    226  await password_field.sendKeys(login_info.password);
    227 
    228  return async function () {
    229    password_field.sendKeys(webdriver.Key.ENTER);
    230    await commands.wait.byTime(5000);
    231  };
    232 }
    233 
    234 /**
    235 * This function sets up the login.
    236 *
    237 * This is done by first the login type, and then performing the
    238 * actual login setup. The return is a possible button to click
    239 * to perform the login.
    240 */
    241 async function setup_login(login_info, context, commands) {
    242  let login_func = await get_login_type(context, commands);
    243  if (!login_func) {
    244    throw Error("Could not determine the type of login page.");
    245  }
    246 
    247  try {
    248    return await login_func(login_info, context, commands);
    249  } catch (err) {
    250    throw Error(`Could not setup login information: ${err}`);
    251  }
    252 }
    253 
    254 /**
    255 * This function performs the login.
    256 *
    257 * It does this by either clicking on a button with a type
    258 * of "sumbit", or running a final_button function that was
    259 * obtained from the setup_login function. Some pages also ask
    260 * questions about setting up 2FA or other information. Generally,
    261 * these contain the "skip" text.
    262 */
    263 async function login(context, commands, final_button) {
    264  try {
    265    if (!final_button) {
    266      // The mouse double click emits an event with `evt.isTrusted=true`
    267      await commands.mouse.doubleClick.bySelector("button[type=submit]");
    268      await commands.wait.byTime(10000);
    269    } else {
    270      // In some cases, it's preferable to be given a function for the final button
    271      await final_button();
    272    }
    273 
    274    // Some pages ask to setup 2FA, skip this based on the text
    275    const XPATHS = [
    276      "//a[contains(text(), 'skip')]",
    277      "//button[contains(text(), 'skip')]",
    278      "//input[contains(text(), 'skip')]",
    279      "//div[contains(text(), 'skip')]",
    280    ];
    281 
    282    for (let xpath of XPATHS) {
    283      try {
    284        await commands.mouse.doubleClick.byXpath(xpath);
    285      } catch (err) {
    286        if (err.toString().includes("not double click")) {
    287          context.log.info(`Can't find a button with the text: ${xpath}`);
    288        } else {
    289          throw err;
    290        }
    291      }
    292    }
    293  } catch (err) {
    294    throw Error(
    295      `Could not login to website as we could not find the submit button/input: ${err}`
    296    );
    297  }
    298 }
    299 
    300 /**
    301 * Grab the base URL from the browsertime url.
    302 *
    303 * This is a necessary step for getting the login values from the Taskcluster
    304 * secrets, which are hashed by the base URL.
    305 *
    306 * The first entry is the protocal, third is the top-level domain (or host)
    307 */
    308 function get_base_URL(fullUrl) {
    309  let pathAsArray = fullUrl.split("/");
    310  return pathAsArray[0] + "//" + pathAsArray[2];
    311 }
    312 
    313 /**
    314 * This function attempts the login-login sequence for a live pageload test
    315 */
    316 async function perform_live_login(context, commands) {
    317  let testUrl = context.options.browsertime.url;
    318 
    319  let logins = await get_logins(context);
    320  const baseUrl = get_base_URL(testUrl);
    321 
    322  await commands.navigate("about:blank");
    323 
    324  let login_info = logins.secret[baseUrl];
    325  try {
    326    await commands.navigate(login_info.login_url);
    327  } catch (err) {
    328    context.log.info("Unable to acquire login information");
    329    throw err;
    330  }
    331  await commands.wait.byTime(10000);
    332 
    333  let final_button = await setup_login(login_info, context, commands);
    334  await login(context, commands, final_button);
    335 }
    336 
    337 module.exports = logTest(
    338  "browsertime pageload",
    339  async function (context, commands) {
    340    context.log.info("Starting a browsertime pageload");
    341    let test_url = context.options.browsertime.url;
    342    let secondary_url = context.options.browsertime.secondary_url;
    343    let page_cycles = context.options.browsertime.page_cycles;
    344    let page_cycle_delay = context.options.browsertime.page_cycle_delay;
    345    let post_startup_delay = context.options.browsertime.post_startup_delay;
    346    let chimera_mode = context.options.browsertime.chimera;
    347    let test_bytecode_cache = context.options.browsertime.test_bytecode_cache;
    348    let login_required = context.options.browsertime.loginRequired;
    349    let live_site = context.options.browsertime.liveSite;
    350    let test_name = context.options.browsertime.testName;
    351 
    352    context.log.info(
    353      "Waiting for %d ms (post_startup_delay)",
    354      post_startup_delay
    355    );
    356    await commands.wait.byTime(post_startup_delay);
    357    let cached = false;
    358 
    359    // Login once before testing the test_url/secondary_url cycles
    360    // If the user has RAPTOR_LOGINS configured correctly, a local login pageload
    361    // test can be attempted. Otherwise if attempting it in CI, only sites with the
    362    // associated MOZ_SCM_LEVEL will be attempted (e.g. Try = 1, autoland = 3).
    363    // In addition, ensure login sequence is only attempted on live sites and for sites
    364    // that we have Taskcluster secrets for.
    365    if (
    366      login_required == "True" &&
    367      live_site == "True" &&
    368      SCM_LOGIN_SITES.includes(test_name)
    369    ) {
    370      await perform_live_login(context, commands);
    371    }
    372 
    373    for (let count = 0; count < page_cycles; count++) {
    374      await logTask(context, "cycle " + count, async function () {
    375        if (count !== 0 && secondary_url !== undefined) {
    376          context.log.info("Navigating to secondary url:" + secondary_url);
    377          await commands.navigate(secondary_url);
    378          await commands.wait.byTime(1000);
    379          await commands.js.runAndWait(`
    380        (function() {
    381          const white = document.createElement('div');
    382          white.id = 'raptor-white';
    383          white.style.position = 'absolute';
    384          white.style.top = '0';
    385          white.style.left = '0';
    386          white.style.width = Math.max(document.documentElement.clientWidth, document.body.clientWidth) + 'px';
    387          white.style.height = Math.max(document.documentElement.clientHeight,document.body.clientHeight) + 'px';
    388          white.style.backgroundColor = 'white';
    389          white.style.zIndex = '2147483647';
    390          document.body.appendChild(white);
    391          document.body.style.display = '';
    392        })();`);
    393          await commands.wait.byTime(1000);
    394        } else {
    395          context.log.info("Navigating to about:blank, count: " + count);
    396          await commands.navigate("about:blank");
    397        }
    398 
    399        context.log.info("Navigating to primary url:" + test_url);
    400        context.log.info(
    401          "Cycle %d, waiting for %d ms",
    402          count,
    403          page_cycle_delay
    404        );
    405        await commands.wait.byTime(page_cycle_delay);
    406 
    407        context.log.info("Cycle %d, starting the measure", count);
    408        await commands.measure.start(test_url);
    409 
    410        // Wait 20 seconds to populate bytecode cache
    411        if (
    412          test_bytecode_cache == "true" &&
    413          chimera_mode == "true" &&
    414          !cached
    415        ) {
    416          context.log.info("Waiting 20s to populate bytecode cache...");
    417          await commands.wait.byTime(20000);
    418          cached = true;
    419        }
    420      });
    421    }
    422 
    423    context.log.info("Browsertime pageload ended.");
    424    return true;
    425  }
    426 );