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/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 CDP: "chrome://remote/content/cdp/CDP.sys.mjs",
9 Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
10 HttpServer: "chrome://remote/content/server/httpd.sys.mjs",
11 Log: "chrome://remote/content/shared/Log.sys.mjs",
12 WebDriverBiDi: "chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs",
15 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
17 ChromeUtils.defineLazyGetter(lazy, "activeProtocols", () => {
18 const protocols = Services.prefs.getIntPref("remote.active-protocols");
19 if (protocols < 1 || protocols > 3) {
20 throw Error(`Invalid remote protocol identifier: ${protocols}`);
26 const WEBDRIVER_BIDI_ACTIVE = 0x1;
27 const CDP_ACTIVE = 0x2;
29 const DEFAULT_HOST = "localhost";
30 const DEFAULT_PORT = 9222;
33 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
35 class RemoteAgentParentProcess {
38 #browserStartupFinished;
49 this.#allowHosts = null;
50 this.#allowOrigins = null;
51 this.#browserStartupFinished = lazy.Deferred();
52 this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}");
53 this.#enabled = false;
55 // Configuration for httpd.js
56 this.#host = DEFAULT_HOST;
57 this.#port = DEFAULT_PORT;
60 // Supported protocols
62 this.#webDriverBiDi = null;
64 Services.ppmm.addMessageListener("RemoteAgent:IsRunning", this);
68 if (this.#allowHosts !== null) {
69 return this.#allowHosts;
73 // If the server is bound to a hostname, not an IP address, return it as
75 const hostUri = Services.io.newURI(`https://${this.#host}`);
76 if (!this.#isIPAddress(hostUri)) {
77 return [RemoteAgent.host];
80 // Following Bug 1220810 localhost is guaranteed to resolve to a loopback
81 // address (127.0.0.1 or ::1) unless network.proxy.allow_hijacking_localhost
82 // is set to true, which should not be the case.
83 const loopbackAddresses = ["127.0.0.1", "[::1]"];
85 // If the server is bound to an IP address and this IP address is a localhost
86 // loopback address, return localhost as allowed host.
87 if (loopbackAddresses.includes(this.#host)) {
92 // Otherwise return an empty array.
97 return this.#allowOrigins;
101 * A promise that resolves when the initial application window has been opened.
104 * Promise that resolves when the initial application window is open.
106 get browserStartupFinished() {
107 return this.#browserStartupFinished.promise;
114 get debuggerAddress() {
119 return `${this.#host}:${this.#port}`;
123 return this.#enabled;
135 return !!this.#server && !this.#server.isStopped();
139 return this.#server?.identity.primaryScheme;
146 get webDriverBiDi() {
147 return this.#webDriverBiDi;
151 * Check if the provided URI's host is an IP address.
153 * @param {nsIURI} uri
159 // getBaseDomain throws an explicit error if the uri host is an IP address.
160 Services.eTLD.getBaseDomain(uri);
162 return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
168 // remote-debugging-port has to be consumed in nsICommandLineHandler:handle
169 // to avoid issues on macos. See Marionette.jsm::handle() for more details.
170 // TODO: remove after Bug 1724251 is fixed.
172 cmdLine.handleFlagWithParam("remote-debugging-port", false);
174 cmdLine.handleFlag("remote-debugging-port", false);
178 async #listen(port) {
179 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
180 throw Components.Exception(
181 "May only be instantiated in parent process",
182 Cr.NS_ERROR_LAUNCHED_CHILD_PROCESS
190 // Try to resolve localhost to an IPv4 and / or IPv6 address so that the
191 // server can be started on a given IP. Only fallback to use localhost if
192 // the hostname cannot be resolved.
194 // Note: This doesn't force httpd.js to use the dual stack support.
195 let isIPv4Host = false;
197 const addresses = await this.#resolveHostname(DEFAULT_HOST);
199 `Available local IP addresses: ${addresses.join(", ")}`
202 // Prefer IPv4 over IPv6 addresses.
203 const addressesIPv4 = addresses.filter(value => !value.includes(":"));
204 isIPv4Host = !!addressesIPv4.length;
206 this.#host = addressesIPv4[0];
208 this.#host = addresses.length ? addresses[0] : DEFAULT_HOST;
211 this.#host = DEFAULT_HOST;
214 `Failed to resolve hostname "localhost" to IP address: ${e.message}`
218 // nsIServerSocket uses -1 for atomic port allocation
224 // Bug 1783938: httpd.js refuses connections when started on a IPv4
225 // address. As workaround start on localhost and add another identity
226 // for that IP address.
227 this.#server = new lazy.HttpServer();
228 const host = isIPv4Host ? DEFAULT_HOST : this.#host;
229 this.server._start(port, host);
230 this.#port = this.server._port;
233 this.server.identity.add("http", this.#host, this.#port);
236 Services.obs.notifyObservers(null, "remote-listening", true);
238 await Promise.all([this.#webDriverBiDi?.start(), this.#cdp?.start()]);
241 lazy.logger.error(`Unable to start remote agent: ${e.message}`, e);
246 * Resolves a hostname to one or more IP addresses.
248 * @param {string} hostname
250 * @returns {Array<string>}
252 #resolveHostname(hostname) {
253 return new Promise((resolve, reject) => {
256 const onLookupCompleteListener = {
257 onLookupComplete(request, record, status) {
258 if (request === originalRequest) {
259 if (!Components.isSuccessCode(status)) {
260 reject({ message: ChromeUtils.getXPCOMErrorName(status) });
264 record.QueryInterface(Ci.nsIDNSAddrRecord);
266 const addresses = [];
267 while (record.hasMore()) {
268 let addr = record.getNextAddrAsString();
269 if (addr.includes(":") && !addr.startsWith("[")) {
270 // Make sure that the IPv6 address is wrapped with brackets.
273 if (!addresses.includes(addr)) {
274 // Sometimes there are duplicate records with the same IP.
275 addresses.push(addr);
285 originalRequest = Services.dns.asyncResolve(
287 Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
288 Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
290 onLookupCompleteListener,
291 null, //Services.tm.mainThread,
292 {} /* defaultOriginAttributes */
295 reject({ message: e.message });
305 // Stop each protocol before stopping the HTTP server.
306 await this.#cdp?.stop();
307 await this.#webDriverBiDi?.stop();
310 await this.#server.stop();
312 Services.obs.notifyObservers(null, "remote-listening");
314 // this function must never fail
315 lazy.logger.error("Unable to stop listener", e);
320 * Handle the --remote-debugging-port command line argument.
322 * @param {nsICommandLine} cmdLine
323 * Instance of the command line interface.
326 * Return `true` if the command line argument has been found.
328 handleRemoteDebuggingPortFlag(cmdLine) {
332 // Catch cases when the argument, and a port have been specified.
333 const port = cmdLine.handleFlagWithParam("remote-debugging-port", false);
337 // In case of an invalid port keep the default port
338 const parsed = Number(port);
339 if (!isNaN(parsed)) {
344 // If no port has been given check for the existence of the argument.
345 enabled = cmdLine.handleFlag("remote-debugging-port", false);
351 handleAllowHostsFlag(cmdLine) {
353 const hosts = cmdLine.handleFlagWithParam("remote-allow-hosts", false);
354 return hosts.split(",");
360 handleAllowOriginsFlag(cmdLine) {
362 const origins = cmdLine.handleFlagWithParam(
363 "remote-allow-origins",
366 return origins.split(",");
372 async observe(subject, topic) {
374 lazy.logger.trace(`Received observer notification ${topic}`);
378 case "profile-after-change":
379 Services.obs.addObserver(this, "command-line-startup");
382 case "command-line-startup":
383 Services.obs.removeObserver(this, topic);
385 this.#enabled = this.handleRemoteDebuggingPortFlag(subject);
388 Services.obs.addObserver(this, "final-ui-startup");
390 this.#allowHosts = this.handleAllowHostsFlag(subject);
391 this.#allowOrigins = this.handleAllowOriginsFlag(subject);
393 Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
394 Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
395 Services.obs.addObserver(this, "quit-application");
397 // With Bug 1717899 we will extend the lifetime of the Remote Agent to
398 // the whole Firefox session, which will be identical to Marionette. For
399 // now prevent logging if the component is not enabled during startup.
401 (lazy.activeProtocols & WEBDRIVER_BIDI_ACTIVE) ===
402 WEBDRIVER_BIDI_ACTIVE
404 this.#webDriverBiDi = new lazy.WebDriverBiDi(this);
406 lazy.logger.debug("WebDriver BiDi enabled");
410 if ((lazy.activeProtocols & CDP_ACTIVE) === CDP_ACTIVE) {
411 this.#cdp = new lazy.CDP(this);
413 lazy.logger.debug("CDP enabled");
419 case "final-ui-startup":
420 Services.obs.removeObserver(this, topic);
423 await this.#listen(this.#port);
425 throw Error(`Unable to start remote agent: ${e}`);
430 // Used to wait until the initial application window has been opened.
431 case "browser-idle-startup-tasks-finished":
432 case "mail-idle-startup-tasks-finished":
433 Services.obs.removeObserver(
435 "browser-idle-startup-tasks-finished"
437 Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
438 this.#browserStartupFinished.resolve();
441 // Listen for application shutdown to also shutdown the Remote Agent
442 // and a possible running instance of httpd.js.
443 case "quit-application":
444 Services.obs.removeObserver(this, topic);
450 receiveMessage({ name }) {
452 case "RemoteAgent:IsRunning":
456 lazy.logger.warn("Unknown IPC message to parent process: " + name);
464 return this.#classID;
468 return ` --remote-debugging-port [<port>] Start the Firefox Remote Agent,
469 which is a low-level remote debugging interface used for WebDriver
470 BiDi and CDP. Defaults to port 9222.
471 --remote-allow-hosts <hosts> Values of the Host header to allow for incoming requests.
472 Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html
473 --remote-allow-origins <origins> Values of the Origin header to allow for incoming requests.
474 Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html\n`;
477 get QueryInterface() {
478 return ChromeUtils.generateQI([
479 "nsICommandLineHandler",
486 class RemoteAgentContentProcess {
490 this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}");
494 let reply = Services.cpmm.sendSyncMessage("RemoteAgent:IsRunning");
496 lazy.logger.warn("No reply from parent process");
502 get QueryInterface() {
503 return ChromeUtils.generateQI(["nsIRemoteAgent"]);
507 export var RemoteAgent;
509 RemoteAgent = new RemoteAgentContentProcess();
511 RemoteAgent = new RemoteAgentParentProcess();
514 // This is used by the XPCOM codepath which expects a constructor
515 export var RemoteAgentFactory = function () {