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) {
43 // CDP is not compatible with Fission by default, check the appropriate
44 // preferences are set to ensure compatibility.
46 Services.prefs.getIntPref("fission.webContentIsolationStrategy") !== 0 ||
47 Services.prefs.getBoolPref("fission.bfcacheInParent")
50 `Invalid browser preferences for CDP. Set "fission.webContentIsolationStrategy"` +
51 `to 0 and "fission.bfcacheInParent" to false before Firefox starts.`
56 if (this.defaultSession) {
58 "Default session is already set on Connection, " +
59 "can't register another one."
62 this.defaultSession = session;
65 this.sessions.set(session.id, session);
69 * Send an error back to the CDP client.
72 * Id of the packet which lead to an error.
74 * Error object with `message` and `stack` attributes.
75 * @param {string=} sessionId
76 * Id of the session used to send this packet. Falls back to the
77 * default session if not specified.
79 sendError(id, err, sessionId) {
85 this.send({ id, error, sessionId });
89 * Send an event coming from a Domain to the CDP client.
91 * @param {string} method
92 * The event name. This is composed by a domain name, a dot character
93 * followed by the event name, e.g. `Target.targetCreated`.
94 * @param {object} params
95 * A JSON-serializable object, which is the payload of this event.
96 * @param {string=} sessionId
97 * The sessionId from which this packet is emitted. Falls back to the
98 * default session if not specified.
100 sendEvent(method, params, sessionId) {
101 this.send({ method, params, sessionId });
103 if (Services.profiler?.IsActive()) {
104 ChromeUtils.addProfilerMarker(
106 { category: "Remote-Protocol" },
111 // When a client attaches to a secondary target via
112 // `Target.attachToTarget`, we should emit an event back with the
113 // result including the `sessionId` attribute of this secondary target's
114 // session. `Target.attachToTarget` creates the secondary session and
115 // returns the session ID.
117 // receivedMessageFromTarget is expected to send a raw CDP packet
118 // in the `message` property and it to be already serialized to a
121 method: "Target.receivedMessageFromTarget",
122 params: { sessionId, message: JSON.stringify({ method, params }) },
128 * Interpret a given CDP packet for a given Session.
130 * @param {string} sessionId
131 * ID of the session for which we should execute a command.
132 * @param {string} message
133 * The stringified JSON payload of the CDP packet, which is about
134 * executing a Domain's function.
136 sendMessageToTarget(sessionId, message) {
137 const session = this.sessions.get(sessionId);
139 throw new Error(`Session '${sessionId}' doesn't exist.`);
141 // `message` is received from `Target.sendMessageToTarget` where the
142 // message attribute is a stringified JSON payload which represent a CDP
144 const packet = JSON.parse(message);
146 // The CDP packet sent by the client shouldn't have a sessionId attribute
147 // as it is passed as another argument of `Target.sendMessageToTarget`.
148 // Set it here in order to reuse the codepath of flatten session, where
149 // the client sends CDP packets with a `sessionId` attribute instead
150 // of going through the old and probably deprecated
151 // `Target.sendMessageToTarget` API.
152 packet.sessionId = sessionId;
153 this.onPacket(packet);
157 * Send the result of a call to a Domain's function back to the CDP client.
160 * The request id being sent by the client to call the domain's method.
161 * @param {object} result
162 * A JSON-serializable object, which is the actual result.
163 * @param {string=} sessionId
164 * The sessionId from which this packet is emitted. Falls back to the
165 * default session if not specified.
167 sendResult(id, result, sessionId) {
168 result = typeof result != "undefined" ? result : {};
169 this.send({ id, result, sessionId });
171 // When a client attaches to a secondary target via
172 // `Target.attachToTarget`, and it executes a command via
173 // `Target.sendMessageToTarget`, we should emit an event back with the
174 // result including the `sessionId` attribute of this secondary target's
175 // session. `Target.attachToTarget` creates the secondary session and
176 // returns the session ID.
178 // receivedMessageFromTarget is expected to send a raw CDP packet
179 // in the `message` property and it to be already serialized to a
182 method: "Target.receivedMessageFromTarget",
183 params: { sessionId, message: JSON.stringify({ id, result }) },
191 * Called by the `transport` when the connection is closed.
193 onConnectionClose() {
194 // Cleanup all the registered sessions.
195 for (const session of this.sessions.values()) {
196 session.destructor();
198 this.sessions.clear();
200 super.onConnectionClose();
204 * Receive a packet from the WebSocket layer.
206 * This packet is sent by a CDP client and is meant to execute
207 * a particular function on a given Domain.
209 * @param {object} packet
210 * JSON-serializable object sent by the client.
212 async onPacket(packet) {
213 super.onPacket(packet);
215 const { id, method, params, sessionId } = packet;
216 const startTime = Cu.now();
219 // First check for mandatory field in the packets
220 if (typeof id == "undefined") {
221 throw new TypeError("Message missing 'id' field");
223 if (typeof method == "undefined") {
224 throw new TypeError("Message missing 'method' field");
227 // Extract the domain name and the method name out of `method` attribute
228 const { domain, command } = splitMethod(method);
230 // If a `sessionId` field is passed, retrieve the session to which we
231 // should forward this packet. Otherwise send it to the default session.
234 if (!this.defaultSession) {
235 throw new Error("Connection is missing a default Session.");
237 session = this.defaultSession;
239 session = this.sessions.get(sessionId);
241 throw new Error(`Session '${sessionId}' doesn't exists.`);
245 // Bug 1600317 - Workaround to deny internal methods to be called
246 if (command.startsWith("_")) {
247 throw new lazy.UnknownMethodError(command);
250 // Finally, instruct the targeted session to execute the command
251 const result = await session.execute(id, domain, command, params);
252 this.sendResult(id, result, sessionId);
254 this.sendError(id, e, packet.sessionId);
257 if (Services.profiler?.IsActive()) {
258 ChromeUtils.addProfilerMarker(
260 { startTime, category: "Remote-Protocol" },
268 * Splits a CDP method into domain and command components.
270 * @param {string} method
271 * Name of the method to split, e.g. "Browser.getVersion".
273 * @returns {Object<string, string>}
274 * Object with the domain ("Browser") and command ("getVersion")
277 export function splitMethod(method) {
278 const parts = method.split(".");
280 if (parts.length != 2 || !parts[0].length || !parts[1].length) {
281 throw new TypeError(`Invalid method format: '${method}'`);