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 { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 Log: "chrome://remote/content/shared/Log.sys.mjs",
11 UnknownMethodError: "chrome://remote/content/cdp/Error.sys.mjs",
14 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
15 lazy.Log.get(lazy.Log.TYPES.CDP)
18 export class CDPConnection extends WebSocketConnection {
20 * @param {WebSocket} webSocket
21 * The WebSocket server connection to wrap.
22 * @param {Connection} httpdConnection
23 * Reference to the httpd.js's connection needed for clean-up.
25 constructor(webSocket, httpdConnection) {
26 super(webSocket, httpdConnection);
28 this.sessions = new Map();
29 this.defaultSession = null;
33 * Register a new Session to forward the messages to.
35 * A session without any `id` attribute will be considered to be the
36 * default one, to which messages without `sessionId` attribute are
37 * forwarded to. Only one such session can be registered.
39 * @param {Session} session
40 * The session to register.
42 registerSession(session) {
44 `Support for the Chrome DevTools Protocol (CDP) in Firefox will be deprecated after Firefox 128 (ESR) ` +
45 `and will be removed in a later release. CDP users should consider migrating ` +
46 `to WebDriver BiDi. See https://bugzilla.mozilla.org/show_bug.cgi?id=1872254`
49 // CDP is not compatible with Fission by default, check the appropriate
50 // preferences are set to ensure compatibility.
52 Services.prefs.getIntPref("fission.webContentIsolationStrategy") !== 0 ||
53 Services.prefs.getBoolPref("fission.bfcacheInParent")
56 `Invalid browser preferences for CDP. Set "fission.webContentIsolationStrategy"` +
57 `to 0 and "fission.bfcacheInParent" to false before Firefox starts.`
62 if (this.defaultSession) {
64 "Default session is already set on Connection, " +
65 "can't register another one."
68 this.defaultSession = session;
71 this.sessions.set(session.id, session);
75 * Send an error back to the CDP client.
78 * Id of the packet which lead to an error.
80 * Error object with `message` and `stack` attributes.
81 * @param {string=} sessionId
82 * Id of the session used to send this packet. Falls back to the
83 * default session if not specified.
85 sendError(id, err, sessionId) {
91 this.send({ id, error, sessionId });
95 * Send an event coming from a Domain to the CDP client.
97 * @param {string} method
98 * The event name. This is composed by a domain name, a dot character
99 * followed by the event name, e.g. `Target.targetCreated`.
100 * @param {object} params
101 * A JSON-serializable object, which is the payload of this event.
102 * @param {string=} sessionId
103 * The sessionId from which this packet is emitted. Falls back to the
104 * default session if not specified.
106 sendEvent(method, params, sessionId) {
107 this.send({ method, params, sessionId });
109 if (Services.profiler?.IsActive()) {
110 ChromeUtils.addProfilerMarker(
112 { category: "Remote-Protocol" },
117 // When a client attaches to a secondary target via
118 // `Target.attachToTarget`, we should emit an event back with the
119 // result including the `sessionId` attribute of this secondary target's
120 // session. `Target.attachToTarget` creates the secondary session and
121 // returns the session ID.
123 // receivedMessageFromTarget is expected to send a raw CDP packet
124 // in the `message` property and it to be already serialized to a
127 method: "Target.receivedMessageFromTarget",
128 params: { sessionId, message: JSON.stringify({ method, params }) },
134 * Interpret a given CDP packet for a given Session.
136 * @param {string} sessionId
137 * ID of the session for which we should execute a command.
138 * @param {string} message
139 * The stringified JSON payload of the CDP packet, which is about
140 * executing a Domain's function.
142 sendMessageToTarget(sessionId, message) {
143 const session = this.sessions.get(sessionId);
145 throw new Error(`Session '${sessionId}' doesn't exist.`);
147 // `message` is received from `Target.sendMessageToTarget` where the
148 // message attribute is a stringified JSON payload which represent a CDP
150 const packet = JSON.parse(message);
152 // The CDP packet sent by the client shouldn't have a sessionId attribute
153 // as it is passed as another argument of `Target.sendMessageToTarget`.
154 // Set it here in order to reuse the codepath of flatten session, where
155 // the client sends CDP packets with a `sessionId` attribute instead
156 // of going through the old and probably deprecated
157 // `Target.sendMessageToTarget` API.
158 packet.sessionId = sessionId;
159 this.onPacket(packet);
163 * Send the result of a call to a Domain's function back to the CDP client.
166 * The request id being sent by the client to call the domain's method.
167 * @param {object} result
168 * A JSON-serializable object, which is the actual result.
169 * @param {string=} sessionId
170 * The sessionId from which this packet is emitted. Falls back to the
171 * default session if not specified.
173 sendResult(id, result, sessionId) {
174 result = typeof result != "undefined" ? result : {};
175 this.send({ id, result, sessionId });
177 // When a client attaches to a secondary target via
178 // `Target.attachToTarget`, and it executes a command via
179 // `Target.sendMessageToTarget`, we should emit an event back with the
180 // result including the `sessionId` attribute of this secondary target's
181 // session. `Target.attachToTarget` creates the secondary session and
182 // returns the session ID.
184 // receivedMessageFromTarget is expected to send a raw CDP packet
185 // in the `message` property and it to be already serialized to a
188 method: "Target.receivedMessageFromTarget",
189 params: { sessionId, message: JSON.stringify({ id, result }) },
197 * Called by the `transport` when the connection is closed.
199 onConnectionClose() {
200 // Cleanup all the registered sessions.
201 for (const session of this.sessions.values()) {
202 session.destructor();
204 this.sessions.clear();
206 super.onConnectionClose();
210 * Receive a packet from the WebSocket layer.
212 * This packet is sent by a CDP client and is meant to execute
213 * a particular function on a given Domain.
215 * @param {object} packet
216 * JSON-serializable object sent by the client.
218 async onPacket(packet) {
219 super.onPacket(packet);
221 const { id, method, params, sessionId } = packet;
222 const startTime = Cu.now();
225 // First check for mandatory field in the packets
226 if (typeof id == "undefined") {
227 throw new TypeError("Message missing 'id' field");
229 if (typeof method == "undefined") {
230 throw new TypeError("Message missing 'method' field");
233 // Extract the domain name and the method name out of `method` attribute
234 const { domain, command } = splitMethod(method);
236 // If a `sessionId` field is passed, retrieve the session to which we
237 // should forward this packet. Otherwise send it to the default session.
240 if (!this.defaultSession) {
241 throw new Error("Connection is missing a default Session.");
243 session = this.defaultSession;
245 session = this.sessions.get(sessionId);
247 throw new Error(`Session '${sessionId}' doesn't exists.`);
251 // Bug 1600317 - Workaround to deny internal methods to be called
252 if (command.startsWith("_")) {
253 throw new lazy.UnknownMethodError(command);
256 // Finally, instruct the targeted session to execute the command
257 const result = await session.execute(id, domain, command, params);
258 this.sendResult(id, result, sessionId);
260 this.sendError(id, e, packet.sessionId);
263 if (Services.profiler?.IsActive()) {
264 ChromeUtils.addProfilerMarker(
266 { startTime, category: "Remote-Protocol" },
274 * Splits a CDP method into domain and command components.
276 * @param {string} method
277 * Name of the method to split, e.g. "Browser.getVersion".
279 * @returns {Record<string, string>}
280 * Object with the domain ("Browser") and command ("getVersion")
283 export function splitMethod(method) {
284 const parts = method.split(".");
286 if (parts.length != 2 || !parts[0].length || !parts[1].length) {
287 throw new TypeError(`Invalid method format: '${method}'`);