tor-browser

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

Capabilities.sys.mjs (30320B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
     11  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     12  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     13  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     14  truncate: "chrome://remote/content/shared/Format.sys.mjs",
     15  UserPromptHandler:
     16    "chrome://remote/content/shared/webdriver/UserPromptHandler.sys.mjs",
     17 });
     18 
     19 ChromeUtils.defineLazyGetter(lazy, "isHeadless", () => {
     20  return Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
     21 });
     22 
     23 ChromeUtils.defineLazyGetter(lazy, "userAgent", () => {
     24  return Cc["@mozilla.org/network/protocol;1?name=http"].getService(
     25    Ci.nsIHttpProtocolHandler
     26  ).userAgent;
     27 });
     28 
     29 XPCOMUtils.defineLazyPreferenceGetter(
     30  lazy,
     31  "shutdownTimeout",
     32  "toolkit.asyncshutdown.crash_timeout"
     33 );
     34 
     35 // List of capabilities which are only relevant for Webdriver Classic.
     36 export const WEBDRIVER_CLASSIC_CAPABILITIES = [
     37  "pageLoadStrategy",
     38  "strictFileInteractability",
     39  "timeouts",
     40  "webSocketUrl",
     41 
     42  // Gecko specific capabilities
     43  "moz:accessibilityChecks",
     44  "moz:firefoxOptions",
     45  "moz:webdriverClick",
     46 
     47  // Extension capabilities
     48  "webauthn:extension:credBlob",
     49  "webauthn:extension:largeBlob",
     50  "webauthn:extension:prf",
     51  "webauthn:extension:uvm",
     52  "webauthn:virtualAuthenticators",
     53 ];
     54 
     55 /** Representation of WebDriver session timeouts. */
     56 export class Timeouts {
     57  constructor() {
     58    // disabled
     59    this.implicit = 0;
     60    // five minutes
     61    this.pageLoad = 300000;
     62    // 30 seconds
     63    this.script = 30000;
     64  }
     65 
     66  toString() {
     67    return "[object Timeouts]";
     68  }
     69 
     70  /** Marshals timeout durations to a JSON Object. */
     71  toJSON() {
     72    return {
     73      implicit: this.implicit,
     74      pageLoad: this.pageLoad,
     75      script: this.script,
     76    };
     77  }
     78 
     79  static fromJSON(json) {
     80    lazy.assert.object(
     81      json,
     82      lazy.pprint`Expected "timeouts" to be an object, got ${json}`
     83    );
     84    let t = new Timeouts();
     85 
     86    for (let [type, ms] of Object.entries(json)) {
     87      switch (type) {
     88        case "implicit":
     89          t.implicit = lazy.assert.positiveInteger(
     90            ms,
     91            `Expected "${type}" to be a positive integer, ` +
     92              lazy.pprint`got ${ms}`
     93          );
     94          break;
     95 
     96        case "script":
     97          if (ms !== null) {
     98            lazy.assert.positiveInteger(
     99              ms,
    100              `Expected "${type}" to be a positive integer, ` +
    101                lazy.pprint`got ${ms}`
    102            );
    103          }
    104          t.script = ms;
    105          break;
    106 
    107        case "pageLoad":
    108          t.pageLoad = lazy.assert.positiveInteger(
    109            ms,
    110            `Expected "${type}" to be a positive integer, ` +
    111              lazy.pprint`got ${ms}`
    112          );
    113          break;
    114 
    115        default:
    116          throw new lazy.error.InvalidArgumentError(
    117            `Unrecognized timeout: ${type}`
    118          );
    119      }
    120    }
    121 
    122    return t;
    123  }
    124 }
    125 
    126 /**
    127 * Enum of page loading strategies.
    128 *
    129 * @enum
    130 */
    131 export const PageLoadStrategy = {
    132  /** No page load strategy.  Navigation will return immediately. */
    133  None: "none",
    134  /**
    135   * Eager, causing navigation to complete when the document reaches
    136   * the <code>interactive</code> ready state.
    137   */
    138  Eager: "eager",
    139  /**
    140   * Normal, causing navigation to return when the document reaches the
    141   * <code>complete</code> ready state.
    142   */
    143  Normal: "normal",
    144 };
    145 
    146 /**
    147 * Enum of proxy types.
    148 *
    149 * @enum
    150 */
    151 export const ProxyTypes = {
    152  Autodetect: "autodetect",
    153  Direct: "direct",
    154  Manual: "manual",
    155  Pac: "pac",
    156  System: "system",
    157 };
    158 
    159 /** Proxy configuration object representation. */
    160 export class ProxyConfiguration {
    161  #previousValuesForPreferences;
    162 
    163  /** @class */
    164  constructor() {
    165    this.proxyType = null;
    166    this.httpProxy = null;
    167    this.httpProxyPort = null;
    168    this.noProxy = null;
    169    this.sslProxy = null;
    170    this.sslProxyPort = null;
    171    this.socksProxy = null;
    172    this.socksProxyPort = null;
    173    this.socksVersion = null;
    174    this.proxyAutoconfigUrl = null;
    175 
    176    // List of applied preferences to clean up on destroy.
    177    this.#previousValuesForPreferences = new Set();
    178  }
    179 
    180  destroy() {
    181    for (const { type, name, value } of this.#previousValuesForPreferences) {
    182      if (type === "int") {
    183        Services.prefs.setIntPref(name, value);
    184      } else if (type === "string") {
    185        Services.prefs.setStringPref(name, value);
    186      }
    187    }
    188 
    189    this.#previousValuesForPreferences = new Set();
    190  }
    191 
    192  /**
    193   * Sets Firefox proxy settings.
    194   *
    195   * @returns {boolean}
    196   *     True if proxy settings were updated as a result of calling this
    197   *     function, or false indicating that this function acted as
    198   *     a no-op.
    199   */
    200  init() {
    201    switch (this.proxyType) {
    202      case ProxyTypes.Autodetect:
    203        this.#setPreference("network.proxy.type", 4);
    204        return true;
    205 
    206      case ProxyTypes.Direct:
    207        this.#setPreference("network.proxy.type", 0);
    208        return true;
    209 
    210      case ProxyTypes.Manual:
    211        this.#setPreference("network.proxy.type", 1);
    212 
    213        if (this.httpProxy) {
    214          this.#setPreference("network.proxy.http", this.httpProxy, "string");
    215          if (Number.isInteger(this.httpProxyPort)) {
    216            this.#setPreference("network.proxy.http_port", this.httpProxyPort);
    217          }
    218        }
    219 
    220        if (this.sslProxy) {
    221          this.#setPreference("network.proxy.ssl", this.sslProxy, "string");
    222          if (Number.isInteger(this.sslProxyPort)) {
    223            this.#setPreference("network.proxy.ssl_port", this.sslProxyPort);
    224          }
    225        }
    226 
    227        if (this.socksProxy) {
    228          this.#setPreference("network.proxy.socks", this.socksProxy, "string");
    229          if (Number.isInteger(this.socksProxyPort)) {
    230            this.#setPreference(
    231              "network.proxy.socks_port",
    232              this.socksProxyPort
    233            );
    234          }
    235          if (this.socksVersion) {
    236            this.#setPreference(
    237              "network.proxy.socks_version",
    238              this.socksVersion
    239            );
    240          }
    241        }
    242 
    243        if (this.noProxy) {
    244          this.#setPreference(
    245            "network.proxy.no_proxies_on",
    246            this.noProxy.join(", "),
    247            "string"
    248          );
    249        }
    250        return true;
    251 
    252      case ProxyTypes.Pac:
    253        this.#setPreference("network.proxy.type", 2);
    254        this.#setPreference(
    255          "network.proxy.autoconfig_url",
    256          this.proxyAutoconfigUrl,
    257          "string"
    258        );
    259        return true;
    260 
    261      case ProxyTypes.System:
    262        this.#setPreference("network.proxy.type", 5);
    263        return true;
    264 
    265      default:
    266        return false;
    267    }
    268  }
    269 
    270  /**
    271   * @param {Record<string, ?>} json
    272   *     JSON Object to unmarshal.
    273   *
    274   * @throws {InvalidArgumentError}
    275   *     When proxy configuration is invalid.
    276   */
    277  static fromJSON(json) {
    278    function stripBracketsFromIpv6Hostname(hostname) {
    279      return hostname.includes(":")
    280        ? hostname.replace(/[\[\]]/g, "")
    281        : hostname;
    282    }
    283 
    284    // Parse hostname and optional port from host
    285    function fromHost(scheme, host) {
    286      lazy.assert.string(
    287        host,
    288        lazy.pprint`Expected proxy "host" to be a string, got ${host}`
    289      );
    290 
    291      if (host.includes("://")) {
    292        throw new lazy.error.InvalidArgumentError(`${host} contains a scheme`);
    293      }
    294 
    295      // To parse the host a scheme has to be added temporarily.
    296      // If the returned value for the port is an empty string it
    297      // could mean no port or the default port for this scheme was
    298      // specified. In such a case parse again with a different
    299      // scheme to ensure we filter out the default port.
    300      let url;
    301      for (let _url of [`http://${host}`, `https://${host}`]) {
    302        url = URL.parse(_url);
    303        if (!url) {
    304          throw new lazy.error.InvalidArgumentError(
    305            lazy.truncate`Expected "url" to be a valid URL, got ${_url}`
    306          );
    307        }
    308        if (url.port != "") {
    309          break;
    310        }
    311      }
    312 
    313      if (
    314        url.username != "" ||
    315        url.password != "" ||
    316        url.pathname != "/" ||
    317        url.search != "" ||
    318        url.hash != ""
    319      ) {
    320        throw new lazy.error.InvalidArgumentError(
    321          `${host} was not of the form host[:port]`
    322        );
    323      }
    324 
    325      let hostname = stripBracketsFromIpv6Hostname(url.hostname);
    326 
    327      // If the port hasn't been set, use the default port of
    328      // the selected scheme (except for socks which doesn't have one).
    329      let port = parseInt(url.port);
    330      if (!Number.isInteger(port)) {
    331        if (scheme === "socks") {
    332          port = null;
    333        } else {
    334          port = Services.io.getDefaultPort(scheme);
    335        }
    336      }
    337 
    338      return [hostname, port];
    339    }
    340 
    341    let p = new ProxyConfiguration();
    342    if (typeof json == "undefined" || json === null) {
    343      return p;
    344    }
    345 
    346    lazy.assert.object(
    347      json,
    348      lazy.pprint`Expected "proxy" to be an object, got ${json}`
    349    );
    350 
    351    lazy.assert.in(
    352      "proxyType",
    353      json,
    354      lazy.pprint`Expected "proxyType" in "proxy" object, got ${json}`
    355    );
    356    p.proxyType = lazy.assert.string(
    357      json.proxyType,
    358      lazy.pprint`Expected "proxyType" to be a string, got ${json.proxyType}`
    359    );
    360 
    361    switch (p.proxyType) {
    362      case "autodetect":
    363      case "direct":
    364      case "system":
    365        break;
    366 
    367      case "pac":
    368        p.proxyAutoconfigUrl = lazy.assert.string(
    369          json.proxyAutoconfigUrl,
    370          `Expected "proxyAutoconfigUrl" to be a string, ` +
    371            lazy.pprint`got ${json.proxyAutoconfigUrl}`
    372        );
    373        break;
    374 
    375      case "manual":
    376        if (typeof json.httpProxy != "undefined") {
    377          [p.httpProxy, p.httpProxyPort] = fromHost("http", json.httpProxy);
    378        }
    379        if (typeof json.sslProxy != "undefined") {
    380          [p.sslProxy, p.sslProxyPort] = fromHost("https", json.sslProxy);
    381        }
    382        if (typeof json.socksProxy != "undefined") {
    383          [p.socksProxy, p.socksProxyPort] = fromHost("socks", json.socksProxy);
    384          p.socksVersion = lazy.assert.positiveInteger(
    385            json.socksVersion,
    386            lazy.pprint`Expected "socksVersion" to be a positive integer, got ${json.socksVersion}`
    387          );
    388        }
    389        if (
    390          typeof json.socksVersion != "undefined" &&
    391          typeof json.socksProxy == "undefined"
    392        ) {
    393          throw new lazy.error.InvalidArgumentError(
    394            `Expected "socksProxy" to be provided if "socksVersion" is provided, got ${json.socksProxy}`
    395          );
    396        }
    397        if (typeof json.noProxy != "undefined") {
    398          let entries = lazy.assert.array(
    399            json.noProxy,
    400            lazy.pprint`Expected "noProxy" to be an array, got ${json.noProxy}`
    401          );
    402          p.noProxy = entries.map(entry => {
    403            lazy.assert.string(
    404              entry,
    405              lazy.pprint`Expected "noProxy" entry to be a string, got ${entry}`
    406            );
    407            return stripBracketsFromIpv6Hostname(entry);
    408          });
    409        }
    410        break;
    411 
    412      default:
    413        throw new lazy.error.InvalidArgumentError(
    414          `Invalid type of proxy: ${p.proxyType}`
    415        );
    416    }
    417 
    418    return p;
    419  }
    420 
    421  /**
    422   * @returns {Record<string, (number | string)>}
    423   *     JSON serialisation of proxy object.
    424   */
    425  toJSON() {
    426    function addBracketsToIpv6Hostname(hostname) {
    427      return hostname.includes(":") ? `[${hostname}]` : hostname;
    428    }
    429 
    430    function toHost(hostname, port) {
    431      if (!hostname) {
    432        return null;
    433      }
    434 
    435      // Add brackets around IPv6 addresses
    436      hostname = addBracketsToIpv6Hostname(hostname);
    437 
    438      if (port != null) {
    439        return `${hostname}:${port}`;
    440      }
    441 
    442      return hostname;
    443    }
    444 
    445    let excludes = this.noProxy;
    446    if (excludes) {
    447      excludes = excludes.map(addBracketsToIpv6Hostname);
    448    }
    449 
    450    return marshal({
    451      proxyType: this.proxyType,
    452      httpProxy: toHost(this.httpProxy, this.httpProxyPort),
    453      noProxy: excludes,
    454      sslProxy: toHost(this.sslProxy, this.sslProxyPort),
    455      socksProxy: toHost(this.socksProxy, this.socksProxyPort),
    456      socksVersion: this.socksVersion,
    457      proxyAutoconfigUrl: this.proxyAutoconfigUrl,
    458    });
    459  }
    460 
    461  toString() {
    462    return "[object Proxy]";
    463  }
    464 
    465  #setPreference(name, value, type = "int") {
    466    let prevValue;
    467 
    468    if (type === "int") {
    469      if (Services.prefs.getPrefType(name) != Services.prefs.PREF_INVALID) {
    470        prevValue = Services.prefs.getIntPref(name);
    471      }
    472 
    473      Services.prefs.setIntPref(name, value);
    474    } else if (type === "string") {
    475      if (Services.prefs.getPrefType(name) != Services.prefs.PREF_INVALID) {
    476        prevValue = Services.prefs.getStringPref(name);
    477      }
    478 
    479      Services.prefs.setStringPref(name, value);
    480    }
    481 
    482    if (prevValue !== undefined) {
    483      this.#previousValuesForPreferences.add({ name, type, value: prevValue });
    484    }
    485  }
    486 }
    487 
    488 export class Capabilities extends Map {
    489  /**
    490   * WebDriver session capabilities representation.
    491   *
    492   * @param {boolean} isBidi
    493   *     Flag indicating that it is a WebDriver BiDi session. Defaults to false.
    494   */
    495  constructor(isBidi = false) {
    496    // Default values for capabilities supported by both WebDriver protocols
    497    const defaults = [
    498      ["acceptInsecureCerts", false],
    499      ["browserName", getWebDriverBrowserName()],
    500      ["browserVersion", lazy.AppInfo.version],
    501      ["platformName", getWebDriverPlatformName()],
    502      ["proxy", new ProxyConfiguration()],
    503      ["setWindowRect", !lazy.AppInfo.isAndroid],
    504      ["unhandledPromptBehavior", new lazy.UserPromptHandler()],
    505      ["userAgent", lazy.userAgent],
    506 
    507      // Gecko specific capabilities
    508      ["moz:buildID", lazy.AppInfo.appBuildID],
    509      ["moz:headless", lazy.isHeadless],
    510      ["moz:platformVersion", Services.sysinfo.getProperty("version")],
    511      ["moz:processID", lazy.AppInfo.processID],
    512      ["moz:profile", maybeProfile()],
    513      ["moz:shutdownTimeout", lazy.shutdownTimeout],
    514    ];
    515 
    516    if (!isBidi) {
    517      // HTTP-only capabilities
    518      defaults.push(
    519        ["pageLoadStrategy", PageLoadStrategy.Normal],
    520        ["timeouts", new Timeouts()],
    521        ["strictFileInteractability", false],
    522 
    523        // Gecko specific capabilities
    524        ["moz:accessibilityChecks", false],
    525        ["moz:webdriverClick", true],
    526        ["moz:windowless", false]
    527      );
    528    }
    529 
    530    super(defaults);
    531  }
    532 
    533  /**
    534   * @param {string} key
    535   *     Capability key.
    536   * @param {(string|number|boolean)} value
    537   *     JSON-safe capability value.
    538   */
    539  set(key, value) {
    540    if (key === "timeouts" && !(value instanceof Timeouts)) {
    541      throw new TypeError();
    542    } else if (key === "proxy" && !(value instanceof ProxyConfiguration)) {
    543      throw new TypeError();
    544    }
    545 
    546    return super.set(key, value);
    547  }
    548 
    549  toString() {
    550    return "[object Capabilities]";
    551  }
    552 
    553  /**
    554   * JSON serialization of capabilities object.
    555   *
    556   * @returns {Record<string, ?>}
    557   */
    558  toJSON() {
    559    let marshalled = marshal(this);
    560 
    561    // Always return the proxy capability even if it's empty
    562    if (!("proxy" in marshalled)) {
    563      marshalled.proxy = {};
    564    }
    565 
    566    marshalled.timeouts = super.get("timeouts");
    567    marshalled.unhandledPromptBehavior = super.get("unhandledPromptBehavior");
    568 
    569    return marshalled;
    570  }
    571 
    572  /**
    573   * Unmarshal a JSON object representation of WebDriver capabilities.
    574   *
    575   * @param {Record<string, *>=} json
    576   *     WebDriver capabilities.
    577   * @param {boolean=} isBidi
    578   *     Flag indicating that it is a WebDriver BiDi session. Defaults to false.
    579   *
    580   * @returns {Capabilities}
    581   *     Internal representation of WebDriver capabilities.
    582   */
    583  static fromJSON(json, isBidi = false) {
    584    if (typeof json == "undefined" || json === null) {
    585      json = {};
    586    }
    587    lazy.assert.object(
    588      json,
    589      lazy.pprint`Expected "capabilities" to be an object, got ${json}"`
    590    );
    591 
    592    const capabilities = new Capabilities(isBidi);
    593 
    594    // TODO: Bug 1823907. We can start using here spec compliant method `validate`,
    595    // as soon as `desiredCapabilities` and `requiredCapabilities` are not supported.
    596    for (let [k, v] of Object.entries(json)) {
    597      if (isBidi && WEBDRIVER_CLASSIC_CAPABILITIES.includes(k)) {
    598        // Ignore any WebDriver classic capability for a WebDriver BiDi session.
    599        continue;
    600      }
    601 
    602      switch (k) {
    603        case "acceptInsecureCerts":
    604          lazy.assert.boolean(
    605            v,
    606            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    607          );
    608          break;
    609 
    610        case "pageLoadStrategy":
    611          lazy.assert.string(
    612            v,
    613            `Expected "${k}" to be a string, ` + lazy.pprint`got ${v}`
    614          );
    615          if (!Object.values(PageLoadStrategy).includes(v)) {
    616            throw new lazy.error.InvalidArgumentError(
    617              "Unknown page load strategy: " + v
    618            );
    619          }
    620          break;
    621 
    622        case "proxy":
    623          v = ProxyConfiguration.fromJSON(v);
    624          break;
    625 
    626        case "setWindowRect":
    627          lazy.assert.boolean(
    628            v,
    629            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    630          );
    631          if (!lazy.AppInfo.isAndroid && !v) {
    632            throw new lazy.error.InvalidArgumentError(
    633              "setWindowRect cannot be disabled"
    634            );
    635          } else if (lazy.AppInfo.isAndroid && v) {
    636            throw new lazy.error.InvalidArgumentError(
    637              "setWindowRect is only supported on desktop"
    638            );
    639          }
    640          break;
    641 
    642        case "timeouts":
    643          v = Timeouts.fromJSON(v);
    644          break;
    645 
    646        case "strictFileInteractability":
    647          v = lazy.assert.boolean(
    648            v,
    649            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    650          );
    651          break;
    652 
    653        case "unhandledPromptBehavior":
    654          v = lazy.UserPromptHandler.fromJSON(v);
    655          break;
    656 
    657        case "webSocketUrl":
    658          lazy.assert.boolean(
    659            v,
    660            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    661          );
    662 
    663          if (!v) {
    664            throw new lazy.error.InvalidArgumentError(
    665              `Expected "${k}" to be true, ` + lazy.pprint`got ${v}`
    666            );
    667          }
    668          break;
    669 
    670        case "webauthn:virtualAuthenticators":
    671          lazy.assert.boolean(
    672            v,
    673            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    674          );
    675          break;
    676 
    677        case "webauthn:extension:uvm":
    678          lazy.assert.boolean(
    679            v,
    680            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    681          );
    682          break;
    683 
    684        case "webauthn:extension:prf":
    685          lazy.assert.boolean(
    686            v,
    687            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    688          );
    689          break;
    690 
    691        case "webauthn:extension:largeBlob":
    692          lazy.assert.boolean(
    693            v,
    694            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    695          );
    696          break;
    697 
    698        case "webauthn:extension:credBlob":
    699          lazy.assert.boolean(
    700            v,
    701            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    702          );
    703          break;
    704 
    705        case "moz:accessibilityChecks":
    706          lazy.assert.boolean(
    707            v,
    708            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    709          );
    710          break;
    711 
    712        case "moz:webdriverClick":
    713          lazy.assert.boolean(
    714            v,
    715            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    716          );
    717          break;
    718 
    719        case "moz:windowless":
    720          lazy.assert.boolean(
    721            v,
    722            `Expected "${k}" to be a boolean, ` + lazy.pprint`got ${v}`
    723          );
    724 
    725          // Only supported on MacOS
    726          if (v && !lazy.AppInfo.isMac) {
    727            throw new lazy.error.InvalidArgumentError(
    728              "moz:windowless only supported on MacOS"
    729            );
    730          }
    731          break;
    732      }
    733      capabilities.set(k, v);
    734    }
    735 
    736    return capabilities;
    737  }
    738 
    739  /**
    740   * Validate WebDriver capability.
    741   *
    742   * @param {string} name
    743   *    The name of capability.
    744   * @param {string} value
    745   *    The value of capability.
    746   *
    747   * @throws {InvalidArgumentError}
    748   *   If <var>value</var> doesn't pass validation,
    749   *   which depends on <var>name</var>.
    750   *
    751   * @returns {string}
    752   *     The validated capability value.
    753   */
    754  static validate(name, value) {
    755    if (value === null) {
    756      return value;
    757    }
    758    switch (name) {
    759      case "acceptInsecureCerts":
    760        lazy.assert.boolean(
    761          value,
    762          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    763        );
    764        return value;
    765 
    766      case "browserName":
    767      case "browserVersion":
    768      case "platformName":
    769        return lazy.assert.string(
    770          value,
    771          `Expected "${name}" to be a string, ` + lazy.pprint`got ${value}`
    772        );
    773 
    774      case "pageLoadStrategy":
    775        lazy.assert.string(
    776          value,
    777          `Expected "${name}" to be a string, ` + lazy.pprint`got ${value}`
    778        );
    779        if (!Object.values(PageLoadStrategy).includes(value)) {
    780          throw new lazy.error.InvalidArgumentError(
    781            "Unknown page load strategy: " + value
    782          );
    783        }
    784        return value;
    785 
    786      case "proxy":
    787        return ProxyConfiguration.fromJSON(value);
    788 
    789      case "strictFileInteractability":
    790        return lazy.assert.boolean(
    791          value,
    792          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    793        );
    794 
    795      case "timeouts":
    796        return Timeouts.fromJSON(value);
    797 
    798      case "unhandledPromptBehavior":
    799        return lazy.UserPromptHandler.fromJSON(value);
    800 
    801      case "webSocketUrl":
    802        lazy.assert.boolean(
    803          value,
    804          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    805        );
    806 
    807        if (!value) {
    808          throw new lazy.error.InvalidArgumentError(
    809            `Expected "${name}" to be true, ` + lazy.pprint`got ${value}`
    810          );
    811        }
    812        return value;
    813 
    814      case "webauthn:virtualAuthenticators":
    815        lazy.assert.boolean(
    816          value,
    817          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    818        );
    819        return value;
    820 
    821      case "webauthn:extension:uvm":
    822        lazy.assert.boolean(
    823          value,
    824          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    825        );
    826        return value;
    827 
    828      case "webauthn:extension:largeBlob":
    829        lazy.assert.boolean(
    830          value,
    831          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    832        );
    833        return value;
    834 
    835      case "moz:firefoxOptions":
    836        return lazy.assert.object(
    837          value,
    838          `Expected "${name}" to be an object, ` + lazy.pprint`got ${value}`
    839        );
    840 
    841      case "moz:accessibilityChecks":
    842        return lazy.assert.boolean(
    843          value,
    844          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    845        );
    846 
    847      case "moz:webdriverClick":
    848        return lazy.assert.boolean(
    849          value,
    850          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    851        );
    852 
    853      case "moz:windowless":
    854        lazy.assert.boolean(
    855          value,
    856          `Expected "${name}" to be a boolean, ` + lazy.pprint`got ${value}`
    857        );
    858 
    859        // Only supported on MacOS
    860        if (value && !lazy.AppInfo.isMac) {
    861          throw new lazy.error.InvalidArgumentError(
    862            "moz:windowless only supported on MacOS"
    863          );
    864        }
    865        return value;
    866 
    867      default:
    868        lazy.assert.string(
    869          name,
    870          `Expected capability "name" to be a string, ` +
    871            lazy.pprint`got ${name}`
    872        );
    873        if (name.includes(":")) {
    874          const [prefix] = name.split(":");
    875          if (prefix !== "moz") {
    876            return value;
    877          }
    878        }
    879        throw new lazy.error.InvalidArgumentError(
    880          `${name} is not the name of a known capability or extension capability`
    881        );
    882    }
    883  }
    884 }
    885 
    886 function getWebDriverBrowserName() {
    887  // Similar to chromedriver which reports "chrome" as browser name for all
    888  // WebView apps, we will report "firefox" for all GeckoView apps.
    889  if (lazy.AppInfo.isAndroid) {
    890    return "firefox";
    891  }
    892 
    893  return lazy.AppInfo.name?.toLowerCase();
    894 }
    895 
    896 function getWebDriverPlatformName() {
    897  let name = Services.sysinfo.getProperty("name");
    898 
    899  if (lazy.AppInfo.isAndroid) {
    900    return "android";
    901  }
    902 
    903  switch (name) {
    904    case "Windows_NT":
    905      return "windows";
    906 
    907    case "Darwin":
    908      return "mac";
    909 
    910    default:
    911      return name.toLowerCase();
    912  }
    913 }
    914 
    915 // Specialisation of |JSON.stringify| that produces JSON-safe object
    916 // literals, dropping empty objects and entries which values are undefined
    917 // or null. Objects are allowed to produce their own JSON representations
    918 // by implementing a |toJSON| function.
    919 function marshal(obj) {
    920  let rv = Object.create(null);
    921 
    922  function* iter(mapOrObject) {
    923    if (mapOrObject instanceof Map) {
    924      for (const [k, v] of mapOrObject) {
    925        yield [k, v];
    926      }
    927    } else {
    928      for (const k of Object.keys(mapOrObject)) {
    929        yield [k, mapOrObject[k]];
    930      }
    931    }
    932  }
    933 
    934  for (let [k, v] of iter(obj)) {
    935    // Skip empty values when serialising to JSON.
    936    if (typeof v == "undefined" || v === null) {
    937      continue;
    938    }
    939 
    940    // Recursively marshal objects that are able to produce their own
    941    // JSON representation.
    942    if (typeof v.toJSON == "function") {
    943      v = marshal(v.toJSON());
    944 
    945      // Or do the same for object literals.
    946    } else if (isObject(v)) {
    947      v = marshal(v);
    948    }
    949 
    950    // And finally drop (possibly marshaled) objects which have no
    951    // entries.
    952    if (!isObjectEmpty(v)) {
    953      rv[k] = v;
    954    }
    955  }
    956 
    957  return rv;
    958 }
    959 
    960 function isObject(obj) {
    961  return Object.prototype.toString.call(obj) == "[object Object]";
    962 }
    963 
    964 function isObjectEmpty(obj) {
    965  return isObject(obj) && Object.keys(obj).length === 0;
    966 }
    967 
    968 // Services.dirsvc is not accessible from JSWindowActor child,
    969 // but we should not panic about that.
    970 function maybeProfile() {
    971  try {
    972    return Services.dirsvc.get("ProfD", Ci.nsIFile).path;
    973  } catch (e) {
    974    return "<protected>";
    975  }
    976 }
    977 
    978 /**
    979 * Merge WebDriver capabilities.
    980 *
    981 * @see https://w3c.github.io/webdriver/#dfn-merging-capabilities
    982 *
    983 * @param {object} primary
    984 *     Required capabilities which need to be merged with <var>secondary</var>.
    985 * @param {object=} secondary
    986 *     Secondary capabilities.
    987 *
    988 * @returns {object} Merged capabilities.
    989 *
    990 * @throws {InvalidArgumentError}
    991 *     If <var>primary</var> and <var>secondary</var> have the same keys.
    992 */
    993 export function mergeCapabilities(primary, secondary) {
    994  const result = { ...primary };
    995 
    996  if (secondary === undefined) {
    997    return result;
    998  }
    999 
   1000  Object.entries(secondary).forEach(([name, value]) => {
   1001    if (primary[name] !== undefined) {
   1002      // Since at the moment we always pass as `primary` `alwaysMatch` object
   1003      // and as `secondary` an item from `firstMatch` array from `capabilities`,
   1004      // we can make this error message more specific.
   1005      throw new lazy.error.InvalidArgumentError(
   1006        `firstMatch key ${name} shadowed a value in alwaysMatch`
   1007      );
   1008    }
   1009    result[name] = value;
   1010  });
   1011 
   1012  return result;
   1013 }
   1014 
   1015 /**
   1016 * Validate WebDriver capabilities.
   1017 *
   1018 * @see https://w3c.github.io/webdriver/#dfn-validate-capabilities
   1019 *
   1020 * @param {object} capabilities
   1021 *     Capabilities which need to be validated.
   1022 *
   1023 * @returns {object} Validated capabilities.
   1024 *
   1025 * @throws {InvalidArgumentError}
   1026 *     If <var>capabilities</var> is not an object.
   1027 */
   1028 export function validateCapabilities(capabilities) {
   1029  lazy.assert.object(
   1030    capabilities,
   1031    lazy.pprint`Expected "capabilities" to be an object, got ${capabilities}`
   1032  );
   1033 
   1034  const result = {};
   1035 
   1036  Object.entries(capabilities).forEach(([name, value]) => {
   1037    const deserialized = Capabilities.validate(name, value);
   1038    if (deserialized !== null) {
   1039      if (["proxy", "timeouts", "unhandledPromptBehavior"].includes(name)) {
   1040        // Return pure values for objects that will be setup during session creation.
   1041        result[name] = value;
   1042      } else {
   1043        result[name] = deserialized;
   1044      }
   1045    }
   1046  });
   1047 
   1048  return result;
   1049 }
   1050 
   1051 /**
   1052 * Process WebDriver capabilities.
   1053 *
   1054 * @see https://w3c.github.io/webdriver/#processing-capabilities
   1055 *
   1056 * @param {object} params
   1057 * @param {object} params.capabilities
   1058 *     Capabilities which need to be processed.
   1059 *
   1060 * @returns {object} Processed capabilities.
   1061 *
   1062 * @throws {InvalidArgumentError}
   1063 *     If <var>capabilities</var> do not satisfy the criteria.
   1064 */
   1065 export function processCapabilities(params) {
   1066  const { capabilities } = params;
   1067  lazy.assert.object(
   1068    capabilities,
   1069    lazy.pprint`Expected "capabilities" to be an object, got ${capabilities}`
   1070  );
   1071 
   1072  let {
   1073    alwaysMatch: requiredCapabilities = {},
   1074    firstMatch: allFirstMatchCapabilities = [{}],
   1075  } = capabilities;
   1076 
   1077  requiredCapabilities = validateCapabilities(requiredCapabilities);
   1078 
   1079  lazy.assert.isNonEmptyArray(
   1080    allFirstMatchCapabilities,
   1081    lazy.pprint`Expected "firstMatch" to be a non-empty array, got ${allFirstMatchCapabilities}`
   1082  );
   1083 
   1084  const validatedFirstMatchCapabilities =
   1085    allFirstMatchCapabilities.map(validateCapabilities);
   1086 
   1087  const mergedCapabilities = [];
   1088  validatedFirstMatchCapabilities.forEach(firstMatchCapabilities => {
   1089    const merged = mergeCapabilities(
   1090      requiredCapabilities,
   1091      firstMatchCapabilities
   1092    );
   1093    mergedCapabilities.push(merged);
   1094  });
   1095 
   1096  // TODO: Bug 1836288. Implement the capability matching logic
   1097  // for "browserName", "browserVersion", "platformName", and
   1098  // "unhandledPromptBehavior" features,
   1099  // for now we can just pick the first merged capability.
   1100  const matchedCapabilities = mergedCapabilities[0];
   1101 
   1102  return matchedCapabilities;
   1103 }