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/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 CDP: "chrome://remote/content/cdp/CDP.sys.mjs",
11 Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
12 Log: "chrome://remote/content/shared/Log.sys.mjs",
13 WebDriverBiDi: "chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs",
16 XPCOMUtils.defineLazyModuleGetters(lazy, {
17 HttpServer: "chrome://remote/content/server/HTTPD.jsm",
20 XPCOMUtils.defineLazyServiceGetter(
23 "@mozilla.org/network/dns-service;1",
27 XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
29 XPCOMUtils.defineLazyGetter(lazy, "activeProtocols", () => {
30 const protocols = Services.prefs.getIntPref("remote.active-protocols");
31 if (protocols < 1 || protocols > 3) {
32 throw Error(`Invalid remote protocol identifier: ${protocols}`);
38 const WEBDRIVER_BIDI_ACTIVE = 0x1;
39 const CDP_ACTIVE = 0x2;
41 const DEFAULT_HOST = "localhost";
42 const DEFAULT_PORT = 9222;
45 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
47 class RemoteAgentParentProcess {
50 #browserStartupFinished;
61 this.#allowHosts = null;
62 this.#allowOrigins = null;
63 this.#browserStartupFinished = lazy.Deferred();
64 this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}");
65 this.#enabled = false;
67 // Configuration for httpd.js
68 this.#host = DEFAULT_HOST;
69 this.#port = DEFAULT_PORT;
72 // Supported protocols
74 this.#webDriverBiDi = null;
76 Services.ppmm.addMessageListener("RemoteAgent:IsRunning", this);
80 if (this.#allowHosts !== null) {
81 return this.#allowHosts;
85 // If the server is bound to a hostname, not an IP address, return it as
87 const hostUri = Services.io.newURI(`https://${this.#host}`);
88 if (!this.#isIPAddress(hostUri)) {
89 return [RemoteAgent.host];
92 // Following Bug 1220810 localhost is guaranteed to resolve to a loopback
93 // address (127.0.0.1 or ::1) unless network.proxy.allow_hijacking_localhost
94 // is set to true, which should not be the case.
95 const loopbackAddresses = ["127.0.0.1", "[::1]"];
97 // If the server is bound to an IP address and this IP address is a localhost
98 // loopback address, return localhost as allowed host.
99 if (loopbackAddresses.includes(this.#host)) {
100 return ["localhost"];
104 // Otherwise return an empty array.
109 return this.#allowOrigins;
113 * A promise that resolves when the initial application window has been opened.
116 * Promise that resolves when the initial application window is open.
118 get browserStartupFinished() {
119 return this.#browserStartupFinished.promise;
126 get debuggerAddress() {
131 return `${this.#host}:${this.#port}`;
135 return this.#enabled;
147 return !!this.#server && !this.#server.isStopped();
151 return this.#server?.identity.primaryScheme;
158 get webDriverBiDi() {
159 return this.#webDriverBiDi;
163 * Check if the provided URI's host is an IP address.
165 * @param {nsIURI} uri
171 // getBaseDomain throws an explicit error if the uri host is an IP address.
172 Services.eTLD.getBaseDomain(uri);
174 return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
180 // remote-debugging-port has to be consumed in nsICommandLineHandler:handle
181 // to avoid issues on macos. See Marionette.jsm::handle() for more details.
182 // TODO: remove after Bug 1724251 is fixed.
184 cmdLine.handleFlagWithParam("remote-debugging-port", false);
186 cmdLine.handleFlag("remote-debugging-port", false);
190 async #listen(port) {
191 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
192 throw Components.Exception(
193 "May only be instantiated in parent process",
194 Cr.NS_ERROR_LAUNCHED_CHILD_PROCESS
202 // Try to resolve localhost to an IPv4 and / or IPv6 address so that the
203 // server can be started on a given IP. Only fallback to use localhost if
204 // the hostname cannot be resolved.
206 // Note: This doesn't force httpd.js to use the dual stack support.
207 let isIPv4Host = false;
209 const addresses = await this.#resolveHostname(DEFAULT_HOST);
211 `Available local IP addresses: ${addresses.join(", ")}`
214 // Prefer IPv4 over IPv6 addresses.
215 const addressesIPv4 = addresses.filter(value => !value.includes(":"));
216 isIPv4Host = !!addressesIPv4.length;
218 this.#host = addressesIPv4[0];
220 this.#host = addresses.length ? addresses[0] : DEFAULT_HOST;
223 this.#host = DEFAULT_HOST;
226 `Failed to resolve hostname "localhost" to IP address: ${e.message}`
230 // nsIServerSocket uses -1 for atomic port allocation
236 // Bug 1783938: httpd.js refuses connections when started on a IPv4
237 // address. As workaround start on localhost and add another identity
238 // for that IP address.
239 this.#server = new lazy.HttpServer();
240 const host = isIPv4Host ? DEFAULT_HOST : this.#host;
241 this.server._start(port, host);
242 this.#port = this.server._port;
245 this.server.identity.add("http", this.#host, this.#port);
248 Services.obs.notifyObservers(null, "remote-listening", true);
250 await Promise.all([this.#webDriverBiDi?.start(), this.#cdp?.start()]);
253 lazy.logger.error(`Unable to start remote agent: ${e.message}`, e);
258 * Resolves a hostname to one or more IP addresses.
260 * @param {string} hostname
262 * @returns {Array<string>}
264 #resolveHostname(hostname) {
265 return new Promise((resolve, reject) => {
268 const onLookupCompleteListener = {
269 onLookupComplete(request, record, status) {
270 if (request === originalRequest) {
271 if (!Components.isSuccessCode(status)) {
272 reject({ message: ChromeUtils.getXPCOMErrorName(status) });
276 record.QueryInterface(Ci.nsIDNSAddrRecord);
278 const addresses = [];
279 while (record.hasMore()) {
280 let addr = record.getNextAddrAsString();
281 if (addr.includes(":") && !addr.startsWith("[")) {
282 // Make sure that the IPv6 address is wrapped with brackets.
285 if (!addresses.includes(addr)) {
286 // Sometimes there are duplicate records with the same IP.
287 addresses.push(addr);
297 originalRequest = lazy.DNSService.asyncResolve(
299 Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
300 Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
302 onLookupCompleteListener,
303 null, //Services.tm.mainThread,
304 {} /* defaultOriginAttributes */
307 reject({ message: e.message });
318 // Stop the CDP support before stopping the server.
319 // Otherwise the HTTP server will fail to stop.
320 await this.#cdp?.stop();
321 await this.#webDriverBiDi?.stop();
323 await this.#server.stop();
325 Services.obs.notifyObservers(null, "remote-listening");
327 // this function must never fail
328 lazy.logger.error("unable to stop listener", e);
333 * Handle the --remote-debugging-port command line argument.
335 * @param {nsICommandLine} cmdLine
336 * Instance of the command line interface.
339 * Return `true` if the command line argument has been found.
341 handleRemoteDebuggingPortFlag(cmdLine) {
345 // Catch cases when the argument, and a port have been specified.
346 const port = cmdLine.handleFlagWithParam("remote-debugging-port", false);
350 // In case of an invalid port keep the default port
351 const parsed = Number(port);
352 if (!isNaN(parsed)) {
357 // If no port has been given check for the existence of the argument.
358 enabled = cmdLine.handleFlag("remote-debugging-port", false);
364 handleAllowHostsFlag(cmdLine) {
366 const hosts = cmdLine.handleFlagWithParam("remote-allow-hosts", false);
367 return hosts.split(",");
373 handleAllowOriginsFlag(cmdLine) {
375 const origins = cmdLine.handleFlagWithParam(
376 "remote-allow-origins",
379 return origins.split(",");
385 async observe(subject, topic) {
387 lazy.logger.trace(`Received observer notification ${topic}`);
391 case "profile-after-change":
392 Services.obs.addObserver(this, "command-line-startup");
395 case "command-line-startup":
396 Services.obs.removeObserver(this, topic);
398 this.#enabled = this.handleRemoteDebuggingPortFlag(subject);
401 Services.obs.addObserver(this, "final-ui-startup");
403 this.#allowHosts = this.handleAllowHostsFlag(subject);
404 this.#allowOrigins = this.handleAllowOriginsFlag(subject);
406 Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
407 Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
408 Services.obs.addObserver(this, "quit-application");
410 // With Bug 1717899 we will extend the lifetime of the Remote Agent to
411 // the whole Firefox session, which will be identical to Marionette. For
412 // now prevent logging if the component is not enabled during startup.
414 (lazy.activeProtocols & WEBDRIVER_BIDI_ACTIVE) ===
415 WEBDRIVER_BIDI_ACTIVE
417 this.#webDriverBiDi = new lazy.WebDriverBiDi(this);
419 lazy.logger.debug("WebDriver BiDi enabled");
423 if ((lazy.activeProtocols & CDP_ACTIVE) === CDP_ACTIVE) {
424 this.#cdp = new lazy.CDP(this);
426 lazy.logger.debug("CDP enabled");
432 case "final-ui-startup":
433 Services.obs.removeObserver(this, topic);
436 await this.#listen(this.#port);
438 throw Error(`Unable to start remote agent: ${e}`);
443 // Used to wait until the initial application window has been opened.
444 case "browser-idle-startup-tasks-finished":
445 case "mail-idle-startup-tasks-finished":
446 Services.obs.removeObserver(
448 "browser-idle-startup-tasks-finished"
450 Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
451 this.#browserStartupFinished.resolve();
454 // Listen for application shutdown to also shutdown the Remote Agent
455 // and a possible running instance of httpd.js.
456 case "quit-application":
457 Services.obs.removeObserver(this, topic);
463 receiveMessage({ name }) {
465 case "RemoteAgent:IsRunning":
469 lazy.logger.warn("Unknown IPC message to parent process: " + name);
477 return this.#classID;
481 return ` --remote-debugging-port [<port>] Start the Firefox Remote Agent,
482 which is a low-level remote debugging interface used for WebDriver
483 BiDi and CDP. Defaults to port 9222.
484 --remote-allow-hosts <hosts> Values of the Host header to allow for incoming requests.
485 Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html
486 --remote-allow-origins <origins> Values of the Origin header to allow for incoming requests.
487 Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html\n`;
490 get QueryInterface() {
491 return ChromeUtils.generateQI([
492 "nsICommandLineHandler",
499 class RemoteAgentContentProcess {
503 this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}");
507 let reply = Services.cpmm.sendSyncMessage("RemoteAgent:IsRunning");
509 lazy.logger.warn("No reply from parent process");
515 get QueryInterface() {
516 return ChromeUtils.generateQI(["nsIRemoteAgent"]);
520 export var RemoteAgent;
522 RemoteAgent = new RemoteAgentContentProcess();
524 RemoteAgent = new RemoteAgentParentProcess();
527 // This is used by the XPCOM codepath which expects a constructor
528 export var RemoteAgentFactory = function() {