diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /remote/components/RemoteAgent.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/components/RemoteAgent.sys.mjs')
-rw-r--r-- | remote/components/RemoteAgent.sys.mjs | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/remote/components/RemoteAgent.sys.mjs b/remote/components/RemoteAgent.sys.mjs new file mode 100644 index 0000000000..31453d910a --- /dev/null +++ b/remote/components/RemoteAgent.sys.mjs @@ -0,0 +1,522 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CDP: "chrome://remote/content/cdp/CDP.sys.mjs", + Deferred: "chrome://remote/content/shared/Sync.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", + WebDriverBiDi: "chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + HttpServer: "chrome://remote/content/server/HTTPD.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); + +XPCOMUtils.defineLazyGetter(lazy, "activeProtocols", () => { + const protocols = Services.prefs.getIntPref("remote.active-protocols"); + if (protocols < 1 || protocols > 3) { + throw Error(`Invalid remote protocol identifier: ${protocols}`); + } + + return protocols; +}); + +const WEBDRIVER_BIDI_ACTIVE = 0x1; +const CDP_ACTIVE = 0x2; + +const DEFAULT_HOST = "localhost"; +const DEFAULT_PORT = 9222; + +const isRemote = + Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + +class RemoteAgentParentProcess { + #allowHosts; + #allowOrigins; + #browserStartupFinished; + #classID; + #enabled; + #host; + #port; + #server; + + #cdp; + #webDriverBiDi; + + constructor() { + this.#allowHosts = null; + this.#allowOrigins = null; + this.#browserStartupFinished = lazy.Deferred(); + this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}"); + this.#enabled = false; + + // Configuration for httpd.js + this.#host = DEFAULT_HOST; + this.#port = DEFAULT_PORT; + this.#server = null; + + // Supported protocols + this.#cdp = null; + this.#webDriverBiDi = null; + + Services.ppmm.addMessageListener("RemoteAgent:IsRunning", this); + } + + get allowHosts() { + if (this.#allowHosts !== null) { + return this.#allowHosts; + } + + if (this.#server) { + // If the server is bound to a hostname, not an IP address, return it as + // allowed host. + const hostUri = Services.io.newURI(`https://${this.#host}`); + if (!this.#isIPAddress(hostUri)) { + return [RemoteAgent.host]; + } + + // Following Bug 1220810 localhost is guaranteed to resolve to a loopback + // address (127.0.0.1 or ::1) unless network.proxy.allow_hijacking_localhost + // is set to true, which should not be the case. + const loopbackAddresses = ["127.0.0.1", "[::1]"]; + + // If the server is bound to an IP address and this IP address is a localhost + // loopback address, return localhost as allowed host. + if (loopbackAddresses.includes(this.#host)) { + return ["localhost"]; + } + } + + // Otherwise return an empty array. + return []; + } + + get allowOrigins() { + return this.#allowOrigins; + } + + /** + * A promise that resolves when the initial application window has been opened. + * + * @returns {Promise} + * Promise that resolves when the initial application window is open. + */ + get browserStartupFinished() { + return this.#browserStartupFinished.promise; + } + + get cdp() { + return this.#cdp; + } + + get debuggerAddress() { + if (!this.#server) { + return ""; + } + + return `${this.#host}:${this.#port}`; + } + + get enabled() { + return this.#enabled; + } + + get host() { + return this.#host; + } + + get port() { + return this.#port; + } + + get running() { + return !!this.#server && !this.#server.isStopped(); + } + + get scheme() { + return this.#server?.identity.primaryScheme; + } + + get server() { + return this.#server; + } + + get webDriverBiDi() { + return this.#webDriverBiDi; + } + + /** + * Check if the provided URI's host is an IP address. + * + * @param {nsIURI} uri + * The URI to check. + * @returns {boolean} + */ + #isIPAddress(uri) { + try { + // getBaseDomain throws an explicit error if the uri host is an IP address. + Services.eTLD.getBaseDomain(uri); + } catch (e) { + return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS; + } + return false; + } + + handle(cmdLine) { + // remote-debugging-port has to be consumed in nsICommandLineHandler:handle + // to avoid issues on macos. See Marionette.jsm::handle() for more details. + // TODO: remove after Bug 1724251 is fixed. + try { + cmdLine.handleFlagWithParam("remote-debugging-port", false); + } catch (e) { + cmdLine.handleFlag("remote-debugging-port", false); + } + } + + async #listen(port) { + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + throw Components.Exception( + "May only be instantiated in parent process", + Cr.NS_ERROR_LAUNCHED_CHILD_PROCESS + ); + } + + if (this.running) { + return; + } + + // Try to resolve localhost to an IPv4 and / or IPv6 address so that the + // server can be started on a given IP. Only fallback to use localhost if + // the hostname cannot be resolved. + // + // Note: This doesn't force httpd.js to use the dual stack support. + let isIPv4Host = false; + try { + const addresses = await this.#resolveHostname(DEFAULT_HOST); + lazy.logger.trace( + `Available local IP addresses: ${addresses.join(", ")}` + ); + + // Prefer IPv4 over IPv6 addresses. + const addressesIPv4 = addresses.filter(value => !value.includes(":")); + isIPv4Host = !!addressesIPv4.length; + if (isIPv4Host) { + this.#host = addressesIPv4[0]; + } else { + this.#host = addresses.length ? addresses[0] : DEFAULT_HOST; + } + } catch (e) { + this.#host = DEFAULT_HOST; + + lazy.logger.debug( + `Failed to resolve hostname "localhost" to IP address: ${e.message}` + ); + } + + // nsIServerSocket uses -1 for atomic port allocation + if (port === 0) { + port = -1; + } + + try { + // Bug 1783938: httpd.js refuses connections when started on a IPv4 + // address. As workaround start on localhost and add another identity + // for that IP address. + this.#server = new lazy.HttpServer(); + const host = isIPv4Host ? DEFAULT_HOST : this.#host; + this.server._start(port, host); + this.#port = this.server._port; + + if (isIPv4Host) { + this.server.identity.add("http", this.#host, this.#port); + } + + Services.obs.notifyObservers(null, "remote-listening", true); + + await Promise.all([this.#webDriverBiDi?.start(), this.#cdp?.start()]); + } catch (e) { + await this.#stop(); + lazy.logger.error(`Unable to start remote agent: ${e.message}`, e); + } + } + + /** + * Resolves a hostname to one or more IP addresses. + * + * @param {string} hostname + * + * @returns {Array<string>} + */ + #resolveHostname(hostname) { + return new Promise((resolve, reject) => { + let originalRequest; + + const onLookupCompleteListener = { + onLookupComplete(request, record, status) { + if (request === originalRequest) { + if (!Components.isSuccessCode(status)) { + reject({ message: ChromeUtils.getXPCOMErrorName(status) }); + return; + } + + record.QueryInterface(Ci.nsIDNSAddrRecord); + + const addresses = []; + while (record.hasMore()) { + let addr = record.getNextAddrAsString(); + if (addr.includes(":") && !addr.startsWith("[")) { + // Make sure that the IPv6 address is wrapped with brackets. + addr = `[${addr}]`; + } + if (!addresses.includes(addr)) { + // Sometimes there are duplicate records with the same IP. + addresses.push(addr); + } + } + + resolve(addresses); + } + }, + }; + + try { + originalRequest = Services.dns.asyncResolve( + hostname, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_BYPASS_CACHE, + null, + onLookupCompleteListener, + null, //Services.tm.mainThread, + {} /* defaultOriginAttributes */ + ); + } catch (e) { + reject({ message: e.message }); + } + }); + } + + async #stop() { + if (!this.running) { + return; + } + + // Stop each protocol before stopping the HTTP server. + await this.#cdp?.stop(); + await this.#webDriverBiDi?.stop(); + + try { + await this.#server.stop(); + this.#server = null; + Services.obs.notifyObservers(null, "remote-listening"); + } catch (e) { + // this function must never fail + lazy.logger.error("Unable to stop listener", e); + } + } + + /** + * Handle the --remote-debugging-port command line argument. + * + * @param {nsICommandLine} cmdLine + * Instance of the command line interface. + * + * @returns {boolean} + * Return `true` if the command line argument has been found. + */ + handleRemoteDebuggingPortFlag(cmdLine) { + let enabled = false; + + try { + // Catch cases when the argument, and a port have been specified. + const port = cmdLine.handleFlagWithParam("remote-debugging-port", false); + if (port !== null) { + enabled = true; + + // In case of an invalid port keep the default port + const parsed = Number(port); + if (!isNaN(parsed)) { + this.#port = parsed; + } + } + } catch (e) { + // If no port has been given check for the existence of the argument. + enabled = cmdLine.handleFlag("remote-debugging-port", false); + } + + return enabled; + } + + handleAllowHostsFlag(cmdLine) { + try { + const hosts = cmdLine.handleFlagWithParam("remote-allow-hosts", false); + return hosts.split(","); + } catch (e) { + return null; + } + } + + handleAllowOriginsFlag(cmdLine) { + try { + const origins = cmdLine.handleFlagWithParam( + "remote-allow-origins", + false + ); + return origins.split(","); + } catch (e) { + return null; + } + } + + async observe(subject, topic) { + if (this.#enabled) { + lazy.logger.trace(`Received observer notification ${topic}`); + } + + switch (topic) { + case "profile-after-change": + Services.obs.addObserver(this, "command-line-startup"); + break; + + case "command-line-startup": + Services.obs.removeObserver(this, topic); + + this.#enabled = this.handleRemoteDebuggingPortFlag(subject); + + if (this.#enabled) { + Services.obs.addObserver(this, "final-ui-startup"); + + this.#allowHosts = this.handleAllowHostsFlag(subject); + this.#allowOrigins = this.handleAllowOriginsFlag(subject); + + Services.obs.addObserver(this, "browser-idle-startup-tasks-finished"); + Services.obs.addObserver(this, "mail-idle-startup-tasks-finished"); + Services.obs.addObserver(this, "quit-application"); + + // With Bug 1717899 we will extend the lifetime of the Remote Agent to + // the whole Firefox session, which will be identical to Marionette. For + // now prevent logging if the component is not enabled during startup. + if ( + (lazy.activeProtocols & WEBDRIVER_BIDI_ACTIVE) === + WEBDRIVER_BIDI_ACTIVE + ) { + this.#webDriverBiDi = new lazy.WebDriverBiDi(this); + if (this.#enabled) { + lazy.logger.debug("WebDriver BiDi enabled"); + } + } + + if ((lazy.activeProtocols & CDP_ACTIVE) === CDP_ACTIVE) { + this.#cdp = new lazy.CDP(this); + if (this.#enabled) { + lazy.logger.debug("CDP enabled"); + } + } + } + break; + + case "final-ui-startup": + Services.obs.removeObserver(this, topic); + + try { + await this.#listen(this.#port); + } catch (e) { + throw Error(`Unable to start remote agent: ${e}`); + } + + break; + + // Used to wait until the initial application window has been opened. + case "browser-idle-startup-tasks-finished": + case "mail-idle-startup-tasks-finished": + Services.obs.removeObserver( + this, + "browser-idle-startup-tasks-finished" + ); + Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished"); + this.#browserStartupFinished.resolve(); + break; + + // Listen for application shutdown to also shutdown the Remote Agent + // and a possible running instance of httpd.js. + case "quit-application": + Services.obs.removeObserver(this, topic); + this.#stop(); + break; + } + } + + receiveMessage({ name }) { + switch (name) { + case "RemoteAgent:IsRunning": + return this.running; + + default: + lazy.logger.warn("Unknown IPC message to parent process: " + name); + return null; + } + } + + // XPCOM + + get classID() { + return this.#classID; + } + + get helpInfo() { + return ` --remote-debugging-port [<port>] Start the Firefox Remote Agent, + which is a low-level remote debugging interface used for WebDriver + BiDi and CDP. Defaults to port 9222. + --remote-allow-hosts <hosts> Values of the Host header to allow for incoming requests. + Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html + --remote-allow-origins <origins> Values of the Origin header to allow for incoming requests. + Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html\n`; + } + + get QueryInterface() { + return ChromeUtils.generateQI([ + "nsICommandLineHandler", + "nsIObserver", + "nsIRemoteAgent", + ]); + } +} + +class RemoteAgentContentProcess { + #classID; + + constructor() { + this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}"); + } + + get running() { + let reply = Services.cpmm.sendSyncMessage("RemoteAgent:IsRunning"); + if (!reply.length) { + lazy.logger.warn("No reply from parent process"); + return false; + } + return reply[0]; + } + + get QueryInterface() { + return ChromeUtils.generateQI(["nsIRemoteAgent"]); + } +} + +export var RemoteAgent; +if (isRemote) { + RemoteAgent = new RemoteAgentContentProcess(); +} else { + RemoteAgent = new RemoteAgentParentProcess(); +} + +// This is used by the XPCOM codepath which expects a constructor +export var RemoteAgentFactory = function () { + return RemoteAgent; +}; |