tor-browser

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

RemoteAgent.sys.mjs (15436B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
      9  HttpServer: "chrome://remote/content/server/httpd.sys.mjs",
     10  Log: "chrome://remote/content/shared/Log.sys.mjs",
     11  PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
     12  RecommendedPreferences:
     13    "chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
     14  WebDriverBiDi: "chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs",
     15 });
     16 
     17 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
     18 
     19 const DEFAULT_HOST = "localhost";
     20 const DEFAULT_PORT = 9222;
     21 
     22 // Adds various command-line arguments as environment variables to preserve
     23 // their values when the application is restarted internally.
     24 const ENV_ALLOW_SYSTEM_ACCESS = "MOZ_REMOTE_ALLOW_SYSTEM_ACCESS";
     25 
     26 const SHARED_DATA_ACTIVE_KEY = "RemoteAgent:Active";
     27 
     28 const isRemote =
     29  Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
     30 
     31 class RemoteAgentParentProcess {
     32  #allowHosts;
     33  #allowOrigins;
     34  #allowSystemAccess;
     35  #browserStartupFinished;
     36  #enabled;
     37  #host;
     38  #port;
     39  #server;
     40 
     41  #webDriverBiDi;
     42 
     43  constructor() {
     44    this.#allowHosts = null;
     45    this.#allowOrigins = null;
     46    this.#allowSystemAccess = Services.env.exists(ENV_ALLOW_SYSTEM_ACCESS);
     47    this.#browserStartupFinished = lazy.Deferred();
     48    this.#enabled = false;
     49 
     50    // Configuration for httpd.js
     51    this.#host = DEFAULT_HOST;
     52    this.#port = DEFAULT_PORT;
     53    this.#server = null;
     54 
     55    // Supported protocols
     56    this.#webDriverBiDi = null;
     57  }
     58 
     59  get allowHosts() {
     60    if (this.#allowHosts !== null) {
     61      return this.#allowHosts;
     62    }
     63 
     64    if (this.#server) {
     65      // If the server is bound to a hostname, not an IP address, return it as
     66      // allowed host.
     67      const hostUri = Services.io.newURI(`https://${this.#host}`);
     68      if (!this.#isIPAddress(hostUri)) {
     69        return [RemoteAgent.host];
     70      }
     71 
     72      // Following Bug 1220810 localhost is guaranteed to resolve to a loopback
     73      // address (127.0.0.1 or ::1) unless network.proxy.allow_hijacking_localhost
     74      // is set to true, which should not be the case.
     75      const loopbackAddresses = ["127.0.0.1", "[::1]"];
     76 
     77      // If the server is bound to an IP address and this IP address is a localhost
     78      // loopback address, return localhost as allowed host.
     79      if (loopbackAddresses.includes(this.#host)) {
     80        return ["localhost"];
     81      }
     82    }
     83 
     84    // Otherwise return an empty array.
     85    return [];
     86  }
     87 
     88  get allowOrigins() {
     89    return this.#allowOrigins;
     90  }
     91 
     92  get allowSystemAccess() {
     93    return this.#allowSystemAccess;
     94  }
     95 
     96  set allowSystemAccess(value) {
     97    // Return early if system access is already marked being allowed.
     98    // There is also no possibility to disallow once it got allowed except
     99    // quitting Firefox and starting it again.
    100    if (this.#allowSystemAccess || !value) {
    101      return;
    102    }
    103 
    104    this.#allowSystemAccess = true;
    105    Services.env.set(ENV_ALLOW_SYSTEM_ACCESS, "1");
    106  }
    107 
    108  /**
    109   * A promise that resolves when the initial application window has been opened.
    110   *
    111   * @returns {Promise}
    112   *     Promise that resolves when the initial application window is open.
    113   */
    114  get browserStartupFinished() {
    115    return this.#browserStartupFinished.promise;
    116  }
    117  get enabled() {
    118    return this.#enabled;
    119  }
    120 
    121  get host() {
    122    return this.#host;
    123  }
    124 
    125  get port() {
    126    return this.#port;
    127  }
    128 
    129  get running() {
    130    return !!this.#server && !this.#server.isStopped();
    131  }
    132 
    133  get scheme() {
    134    return this.#server?.identity.primaryScheme;
    135  }
    136 
    137  get server() {
    138    return this.#server;
    139  }
    140 
    141  /**
    142   * Syncs the WebDriver active flag with the web content processes.
    143   *
    144   * @param {boolean} value - Flag indicating if Remote Agent is active or not.
    145   */
    146  updateWebdriverActiveFlag(value) {
    147    Services.ppmm.sharedData.set(SHARED_DATA_ACTIVE_KEY, value);
    148    Services.ppmm.sharedData.flush();
    149  }
    150 
    151  get webDriverBiDi() {
    152    return this.#webDriverBiDi;
    153  }
    154 
    155  /**
    156   * Handle the --remote-debugging-port command line argument.
    157   *
    158   * @param {nsICommandLine} cmdLine
    159   *     Instance of the command line interface.
    160   *
    161   * @returns {boolean}
    162   *     Return `true` if the command line argument has been found.
    163   */
    164  #handleRemoteDebuggingPortFlag(cmdLine) {
    165    let enabled = false;
    166 
    167    try {
    168      // Catch cases when the argument, and a port have been specified.
    169      const port = cmdLine.handleFlagWithParam("remote-debugging-port", false);
    170      if (port !== null) {
    171        enabled = true;
    172 
    173        // In case of an invalid port keep the default port
    174        const parsed = Number(port);
    175        if (!isNaN(parsed)) {
    176          this.#port = parsed;
    177        }
    178      }
    179    } catch (e) {
    180      // If no port has been given check for the existence of the argument.
    181      enabled = cmdLine.handleFlag("remote-debugging-port", false);
    182    }
    183 
    184    return enabled;
    185  }
    186 
    187  #handleAllowHostsFlag(cmdLine) {
    188    try {
    189      const hosts = cmdLine.handleFlagWithParam("remote-allow-hosts", false);
    190      return hosts.split(",");
    191    } catch (e) {
    192      return null;
    193    }
    194  }
    195 
    196  #handleAllowOriginsFlag(cmdLine) {
    197    try {
    198      const origins = cmdLine.handleFlagWithParam(
    199        "remote-allow-origins",
    200        false
    201      );
    202      return origins.split(",");
    203    } catch (e) {
    204      return null;
    205    }
    206  }
    207 
    208  #handleAllowSystemAccessFlag(cmdLine) {
    209    try {
    210      return cmdLine.handleFlag("remote-allow-system-access", false);
    211    } catch (e) {
    212      return false;
    213    }
    214  }
    215 
    216  /**
    217   * Check if the provided URI's host is an IP address.
    218   *
    219   * @param {nsIURI} uri
    220   *     The URI to check.
    221   * @returns {boolean}
    222   */
    223  #isIPAddress(uri) {
    224    try {
    225      // getBaseDomain throws an explicit error if the uri host is an IP address.
    226      Services.eTLD.getBaseDomain(uri);
    227    } catch (e) {
    228      return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
    229    }
    230    return false;
    231  }
    232 
    233  async #listen(port) {
    234    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
    235      throw Components.Exception(
    236        "May only be instantiated in parent process",
    237        Cr.NS_ERROR_LAUNCHED_CHILD_PROCESS
    238      );
    239    }
    240 
    241    if (this.running) {
    242      return;
    243    }
    244 
    245    // Try to resolve localhost to an IPv4  and / or IPv6 address so that the
    246    // server can be started on a given IP. Only fallback to use localhost if
    247    // the hostname cannot be resolved.
    248    //
    249    // Note: This doesn't force httpd.js to use the dual stack support.
    250    let isIPv4Host = false;
    251    try {
    252      const addresses = await this.#resolveHostname(DEFAULT_HOST);
    253      lazy.logger.trace(
    254        `Available local IP addresses: ${addresses.join(", ")}`
    255      );
    256 
    257      // Prefer IPv4 over IPv6 addresses.
    258      const addressesIPv4 = addresses.filter(value => !value.includes(":"));
    259      isIPv4Host = !!addressesIPv4.length;
    260      if (isIPv4Host) {
    261        this.#host = addressesIPv4[0];
    262      } else {
    263        this.#host = addresses.length ? addresses[0] : DEFAULT_HOST;
    264      }
    265    } catch (e) {
    266      this.#host = DEFAULT_HOST;
    267 
    268      lazy.logger.debug(
    269        `Failed to resolve hostname "localhost" to IP address: ${e.message}`
    270      );
    271    }
    272 
    273    // nsIServerSocket uses -1 for atomic port allocation
    274    if (port === 0) {
    275      port = -1;
    276    }
    277 
    278    try {
    279      this.#server = new lazy.HttpServer();
    280      const host = isIPv4Host ? DEFAULT_HOST : this.#host;
    281 
    282      let error;
    283      await lazy.PollPromise(
    284        (resolve, reject) => {
    285          try {
    286            this.server._start(port, host);
    287            this.#port = this.server._port;
    288            resolve();
    289          } catch (e) {
    290            error = e;
    291            lazy.logger.debug(`Could not bind to port ${port} (${error.name})`);
    292            reject();
    293          }
    294        },
    295        { interval: 250, timeout: 5000 }
    296      );
    297 
    298      if (!this.#server._socket) {
    299        throw new Error(`Failed to start HTTP server on port ${port}`);
    300      }
    301 
    302      if (isIPv4Host) {
    303        // Bug 1783938: httpd.js refuses connections when started on a IPv4
    304        // address. As workaround start on localhost and add another identity
    305        // for that IP address.
    306        this.server.identity.add("http", this.#host, this.#port);
    307      }
    308 
    309      this.updateWebdriverActiveFlag(true);
    310 
    311      Services.obs.notifyObservers(null, "remote-listening", true);
    312 
    313      await this.#webDriverBiDi?.start();
    314    } catch (e) {
    315      await this.#stop();
    316      lazy.logger.error(
    317        `Unable to start the RemoteAgent: ${e.message}, closing`,
    318        e
    319      );
    320 
    321      Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
    322    }
    323  }
    324 
    325  /**
    326   * Resolves a hostname to one or more IP addresses.
    327   *
    328   * @param {string} hostname
    329   *
    330   * @returns {Array<string>}
    331   */
    332  #resolveHostname(hostname) {
    333    return new Promise((resolve, reject) => {
    334      let originalRequest;
    335 
    336      const onLookupCompleteListener = {
    337        onLookupComplete(request, record, status) {
    338          if (request === originalRequest) {
    339            if (!Components.isSuccessCode(status)) {
    340              reject({ message: ChromeUtils.getXPCOMErrorName(status) });
    341              return;
    342            }
    343 
    344            record.QueryInterface(Ci.nsIDNSAddrRecord);
    345 
    346            const addresses = [];
    347            while (record.hasMore()) {
    348              let addr = record.getNextAddrAsString();
    349              if (addr.includes(":") && !addr.startsWith("[")) {
    350                // Make sure that the IPv6 address is wrapped with brackets.
    351                addr = `[${addr}]`;
    352              }
    353              if (!addresses.includes(addr)) {
    354                // Sometimes there are duplicate records with the same IP.
    355                addresses.push(addr);
    356              }
    357            }
    358 
    359            resolve(addresses);
    360          }
    361        },
    362      };
    363 
    364      try {
    365        originalRequest = Services.dns.asyncResolve(
    366          hostname,
    367          Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
    368          Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
    369          null,
    370          onLookupCompleteListener,
    371          null, //Services.tm.mainThread,
    372          {} /* defaultOriginAttributes */
    373        );
    374      } catch (e) {
    375        reject({ message: e.message });
    376      }
    377    });
    378  }
    379 
    380  async #stop() {
    381    if (!this.running) {
    382      return;
    383    }
    384 
    385    // Stop each protocol before stopping the HTTP server.
    386    await this.#webDriverBiDi?.stop();
    387 
    388    try {
    389      await this.#server.stop();
    390      this.#server = null;
    391 
    392      this.updateWebdriverActiveFlag(false);
    393 
    394      Services.obs.notifyObservers(null, "remote-listening");
    395    } catch (e) {
    396      // this function must never fail
    397      lazy.logger.error("Unable to stop listener", e);
    398    }
    399  }
    400 
    401  handle(cmdLine) {
    402    // All supported command line arguments have to be consumed in
    403    // nsICommandLineHandler:handle to avoid issues on macos.
    404    // See Marionette.sys.mjs::handle() for more details.
    405    // TODO: remove after Bug 1724251 is fixed.
    406    try {
    407      cmdLine.handleFlagWithParam("remote-debugging-port", false);
    408    } catch (e) {
    409      cmdLine.handleFlag("remote-debugging-port", false);
    410    }
    411 
    412    cmdLine.handleFlag("remote-allow-system-access", false);
    413    cmdLine.handleFlagWithParam("remote-allow-hosts", false);
    414    cmdLine.handleFlagWithParam("remote-allow-origins", false);
    415  }
    416 
    417  async observe(subject, topic) {
    418    if (this.#enabled) {
    419      lazy.logger.trace(`Received observer notification ${topic}`);
    420    }
    421 
    422    switch (topic) {
    423      case "profile-after-change":
    424        Services.obs.addObserver(this, "command-line-startup");
    425        break;
    426 
    427      case "command-line-startup":
    428        Services.obs.removeObserver(this, topic);
    429 
    430        this.#allowHosts = this.#handleAllowHostsFlag(subject);
    431        this.#allowOrigins = this.#handleAllowOriginsFlag(subject);
    432        this.allowSystemAccess = this.#handleAllowSystemAccessFlag(subject);
    433 
    434        this.#enabled = this.#handleRemoteDebuggingPortFlag(subject);
    435 
    436        if (this.#enabled) {
    437          // Add annotation to crash report to indicate whether the
    438          // Remote Agent was active.
    439          Services.appinfo.annotateCrashReport("RemoteAgent", true);
    440 
    441          Services.obs.addObserver(this, "final-ui-startup");
    442          Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
    443          Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
    444          Services.obs.addObserver(this, "quit-application");
    445 
    446          // Apply the common set of preferences for all supported protocols
    447          lazy.RecommendedPreferences.applyPreferences();
    448 
    449          this.#webDriverBiDi = new lazy.WebDriverBiDi(this);
    450          lazy.logger.debug("WebDriver BiDi enabled");
    451        }
    452        break;
    453 
    454      case "final-ui-startup":
    455        Services.obs.removeObserver(this, topic);
    456 
    457        try {
    458          await this.#listen(this.#port);
    459        } catch (e) {
    460          throw Error(`Unable to start remote agent: ${e}`);
    461        }
    462 
    463        break;
    464 
    465      // Used to wait until the initial application window has been opened.
    466      case "browser-idle-startup-tasks-finished":
    467      case "mail-idle-startup-tasks-finished":
    468        Services.obs.removeObserver(
    469          this,
    470          "browser-idle-startup-tasks-finished"
    471        );
    472        Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
    473        this.#browserStartupFinished.resolve();
    474        break;
    475 
    476      // Listen for application shutdown to also shutdown the Remote Agent
    477      // and a possible running instance of httpd.js.
    478      case "quit-application":
    479        Services.obs.removeObserver(this, topic);
    480        this.#stop();
    481        break;
    482    }
    483  }
    484 
    485  receiveMessage({ name }) {
    486    switch (name) {
    487      case "RemoteAgent:IsRunning":
    488        return this.running;
    489 
    490      default:
    491        lazy.logger.warn("Unknown IPC message to parent process: " + name);
    492        return null;
    493    }
    494  }
    495 
    496  // XPCOM
    497 
    498  helpInfo = `  --remote-debugging-port [<port>] Start the Firefox Remote Agent,
    499                     which is a low-level remote debugging interface used for WebDriver
    500                     BiDi. Defaults to port 9222.
    501  --remote-allow-hosts <hosts> Values of the Host header to allow for incoming requests.
    502                     Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html
    503  --remote-allow-origins <origins> Values of the Origin header to allow for incoming requests.
    504                     Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html
    505  --remote-allow-system-access Enable privileged access to the application's parent process\n`;
    506 
    507  QueryInterface = ChromeUtils.generateQI([
    508    "nsICommandLineHandler",
    509    "nsIObserver",
    510    "nsIRemoteAgent",
    511  ]);
    512 }
    513 
    514 class RemoteAgentContentProcess {
    515  get running() {
    516    return Services.cpmm.sharedData.get(SHARED_DATA_ACTIVE_KEY) ?? false;
    517  }
    518 
    519  // XPCOM
    520 
    521  QueryInterface = ChromeUtils.generateQI(["nsIRemoteAgent"]);
    522 }
    523 
    524 export var RemoteAgent;
    525 if (isRemote) {
    526  RemoteAgent = new RemoteAgentContentProcess();
    527 } else {
    528  RemoteAgent = new RemoteAgentParentProcess();
    529 }
    530 
    531 // This is used by the XPCOM codepath which expects a constructor
    532 export var RemoteAgentFactory = function () {
    533  return RemoteAgent;
    534 };