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 var EXPORTED_SYMBOLS = ["RemoteAgent", "RemoteAgentFactory"];
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10 const { XPCOMUtils } = ChromeUtils.import(
11 "resource://gre/modules/XPCOMUtils.jsm"
14 XPCOMUtils.defineLazyModuleGetters(this, {
15 FatalError: "chrome://remote/content/Error.jsm",
16 HttpServer: "chrome://remote/content/server/HTTPD.jsm",
17 JSONHandler: "chrome://remote/content/JSONHandler.jsm",
18 Log: "chrome://remote/content/Log.jsm",
19 NetUtil: "resource://gre/modules/NetUtil.jsm",
20 Observer: "chrome://remote/content/Observer.jsm",
21 Preferences: "resource://gre/modules/Preferences.jsm",
22 RecommendedPreferences: "chrome://remote/content/RecommendedPreferences.jsm",
23 Targets: "chrome://remote/content/targets/Targets.jsm",
25 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
27 const ENABLED = "remote.enabled";
28 const FORCE_LOCAL = "remote.force-local";
30 const DEFAULT_HOST = "localhost";
31 const DEFAULT_PORT = 9222;
32 const LOOPBACKS = ["localhost", "127.0.0.1", "[::1]"];
34 class RemoteAgentClass {
36 if (!Preferences.get(ENABLED, false)) {
37 throw new Error("Remote agent is disabled by its preference");
39 if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
41 "Remote agent can only be instantiated from the parent process"
49 this.server = new HttpServer();
50 this.targets = new Targets();
52 // Register the static HTTP endpoints like /json/version or /json/list
53 this.server.registerPrefixHandler("/json/", new JSONHandler(this));
55 // Register the dynamic HTTP endpoint of each target.
56 // These are WebSocket URL where the HTTP request will be morphed
57 // into a WebSocket connectiong after an handshake.
58 this.targets.on("target-created", (eventName, target) => {
60 throw new Error(`Target is missing 'path' attribute: ${target}`);
62 this.server.registerPathHandler(target.path, target);
64 this.targets.on("target-destroyed", (eventName, target) => {
65 // TODO: This removes the entry added by registerPathHandler, should rather expose
66 // an unregisterPathHandler method on nsHttpServer.
67 delete this.server._handler._overridePaths[target.path];
72 return !!this.server && !this.server._socketClosed;
75 async listen(address) {
76 if (!(address instanceof Ci.nsIURI)) {
77 throw new TypeError(`Expected nsIURI: ${address}`);
80 let { host, port } = address;
81 if (Preferences.get(FORCE_LOCAL) && !LOOPBACKS.includes(host)) {
82 throw new Error("Restricted to loopback devices");
85 // nsIServerSocket uses -1 for atomic port allocation
96 // Start watching for targets *after* registering the target listeners
97 // as this will fire event for already-existing targets.
98 await this.targets.watchForTargets();
101 // Immediatly instantiate the main process target in order
102 // to be accessible via HTTP endpoint on startup
103 const mainTarget = this.targets.getMainProcessTarget();
105 this.server._start(port, host);
106 dump(`DevTools listening on ${mainTarget.wsDebuggerURL}\n`);
108 throw new Error(`Unable to start remote agent: ${e.message}`, e);
111 Preferences.set(RecommendedPreferences);
115 if (this.listening) {
117 // Destroy all the targets first in order to ensure closing all pending
118 // connections first. Otherwise Httpd's stop is not going to resolve.
119 this.targets.destructor();
121 await this.server.stop();
123 Preferences.reset(Object.keys(RecommendedPreferences));
125 throw new Error(`Unable to stop agent: ${e.message}`, e);
128 log.info("Stopped listening");
136 return this.server.identity.primaryScheme;
143 return this.server.identity.primaryHost;
150 return this.server.identity.primaryPort;
153 // nsICommandLineHandler
155 async handle(cmdLine) {
156 function flag(name) {
157 const caseSensitive = true;
159 return cmdLine.handleFlagWithParam(name, caseSensitive);
161 return cmdLine.handleFlag(name, caseSensitive);
165 const remoteDebugger = flag("remote-debugger");
166 const remoteDebuggingPort = flag("remote-debugging-port");
168 if (remoteDebugger && remoteDebuggingPort) {
170 "Conflicting flags --remote-debugger and --remote-debugging-port"
172 cmdLine.preventDefault = true;
176 if (!remoteDebugger && !remoteDebuggingPort) {
181 if (typeof remoteDebugger == "string") {
182 [host, port] = remoteDebugger.split(":");
183 } else if (typeof remoteDebuggingPort == "string") {
184 port = remoteDebuggingPort;
189 addr = NetUtil.newURI(
190 `http://${host || DEFAULT_HOST}:${port || DEFAULT_PORT}/`
194 `Expected address syntax [<host>]:<port>: ${remoteDebugger ||
195 remoteDebuggingPort}`
197 cmdLine.preventDefault = true;
201 await Observer.once("sessionstore-windows-restored");
207 throw new FatalError(
208 `Unable to start remote agent on ${addr.spec}: ${e.message}`,
216 " --remote-debugger [<host>][:<port>]\n" +
217 " --remote-debugging-port <port> Start the Firefox remote agent, which is \n" +
218 " a low-level debugging interface based on the CDP protocol.\n" +
219 " Defaults to listen on localhost:9222.\n"
225 get QueryInterface() {
226 return ChromeUtils.generateQI([Ci.nsICommandLineHandler]);
230 var RemoteAgent = new RemoteAgentClass();
232 // This is used by the XPCOM codepath which expects a constructor
233 var RemoteAgentFactory = function() {