mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
ecaac10c8e
Differential Revision: https://phabricator.services.mozilla.com/D204187
516 lines
14 KiB
JavaScript
516 lines
14 KiB
JavaScript
/* 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
CDP: "chrome://remote/content/cdp/CDP.sys.mjs",
|
|
Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
|
|
HttpServer: "chrome://remote/content/server/httpd.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
RecommendedPreferences:
|
|
"chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
|
|
WebDriverBiDi: "chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
|
|
|
|
ChromeUtils.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.sys.mjs::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");
|
|
|
|
// Apply the common set of preferences for all supported protocols
|
|
lazy.RecommendedPreferences.applyPreferences();
|
|
|
|
// 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 {
|
|
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;
|
|
};
|