diff options
Diffstat (limited to 'remote/cdp/CDPConnection.sys.mjs')
-rw-r--r-- | remote/cdp/CDPConnection.sys.mjs | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/remote/cdp/CDPConnection.sys.mjs b/remote/cdp/CDPConnection.sys.mjs new file mode 100644 index 0000000000..dfbb94ee09 --- /dev/null +++ b/remote/cdp/CDPConnection.sys.mjs @@ -0,0 +1,290 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Log: "chrome://remote/content/shared/Log.sys.mjs", + UnknownMethodError: "chrome://remote/content/cdp/Error.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.CDP) +); + +export class CDPConnection extends WebSocketConnection { + /** + * @param {WebSocket} webSocket + * The WebSocket server connection to wrap. + * @param {Connection} httpdConnection + * Reference to the httpd.js's connection needed for clean-up. + */ + constructor(webSocket, httpdConnection) { + super(webSocket, httpdConnection); + + this.sessions = new Map(); + this.defaultSession = null; + } + + /** + * Register a new Session to forward the messages to. + * + * A session without any `id` attribute will be considered to be the + * default one, to which messages without `sessionId` attribute are + * forwarded to. Only one such session can be registered. + * + * @param {Session} session + * The session to register. + */ + registerSession(session) { + // CDP is not compatible with Fission by default, check the appropriate + // preferences are set to ensure compatibility. + if ( + Services.prefs.getIntPref("fission.webContentIsolationStrategy") !== 0 || + Services.prefs.getBoolPref("fission.bfcacheInParent") + ) { + lazy.logger.warn( + `Invalid browser preferences for CDP. Set "fission.webContentIsolationStrategy"` + + `to 0 and "fission.bfcacheInParent" to false before Firefox starts.` + ); + } + + if (!session.id) { + if (this.defaultSession) { + throw new Error( + "Default session is already set on Connection, " + + "can't register another one." + ); + } + this.defaultSession = session; + } + + this.sessions.set(session.id, session); + } + + /** + * Send an error back to the CDP client. + * + * @param {number} id + * Id of the packet which lead to an error. + * @param {Error} err + * Error object with `message` and `stack` attributes. + * @param {string=} sessionId + * Id of the session used to send this packet. Falls back to the + * default session if not specified. + */ + sendError(id, err, sessionId) { + const error = { + message: err.message, + data: err.stack, + }; + + this.send({ id, error, sessionId }); + } + + /** + * Send an event coming from a Domain to the CDP client. + * + * @param {string} method + * The event name. This is composed by a domain name, a dot character + * followed by the event name, e.g. `Target.targetCreated`. + * @param {object} params + * A JSON-serializable object, which is the payload of this event. + * @param {string=} sessionId + * The sessionId from which this packet is emitted. Falls back to the + * default session if not specified. + */ + sendEvent(method, params, sessionId) { + this.send({ method, params, sessionId }); + + if (Services.profiler?.IsActive()) { + ChromeUtils.addProfilerMarker( + "CDP: Event", + { category: "Remote-Protocol" }, + method + ); + } + + // When a client attaches to a secondary target via + // `Target.attachToTarget`, we should emit an event back with the + // result including the `sessionId` attribute of this secondary target's + // session. `Target.attachToTarget` creates the secondary session and + // returns the session ID. + if (sessionId) { + // receivedMessageFromTarget is expected to send a raw CDP packet + // in the `message` property and it to be already serialized to a + // string + this.send({ + method: "Target.receivedMessageFromTarget", + params: { sessionId, message: JSON.stringify({ method, params }) }, + }); + } + } + + /** + * Interpret a given CDP packet for a given Session. + * + * @param {string} sessionId + * ID of the session for which we should execute a command. + * @param {string} message + * The stringified JSON payload of the CDP packet, which is about + * executing a Domain's function. + */ + sendMessageToTarget(sessionId, message) { + const session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`Session '${sessionId}' doesn't exist.`); + } + // `message` is received from `Target.sendMessageToTarget` where the + // message attribute is a stringified JSON payload which represent a CDP + // packet. + const packet = JSON.parse(message); + + // The CDP packet sent by the client shouldn't have a sessionId attribute + // as it is passed as another argument of `Target.sendMessageToTarget`. + // Set it here in order to reuse the codepath of flatten session, where + // the client sends CDP packets with a `sessionId` attribute instead + // of going through the old and probably deprecated + // `Target.sendMessageToTarget` API. + packet.sessionId = sessionId; + this.onPacket(packet); + } + + /** + * Send the result of a call to a Domain's function back to the CDP client. + * + * @param {number} id + * The request id being sent by the client to call the domain's method. + * @param {object} result + * A JSON-serializable object, which is the actual result. + * @param {string=} sessionId + * The sessionId from which this packet is emitted. Falls back to the + * default session if not specified. + */ + sendResult(id, result, sessionId) { + result = typeof result != "undefined" ? result : {}; + this.send({ id, result, sessionId }); + + // When a client attaches to a secondary target via + // `Target.attachToTarget`, and it executes a command via + // `Target.sendMessageToTarget`, we should emit an event back with the + // result including the `sessionId` attribute of this secondary target's + // session. `Target.attachToTarget` creates the secondary session and + // returns the session ID. + if (sessionId) { + // receivedMessageFromTarget is expected to send a raw CDP packet + // in the `message` property and it to be already serialized to a + // string + this.send({ + method: "Target.receivedMessageFromTarget", + params: { sessionId, message: JSON.stringify({ id, result }) }, + }); + } + } + + // Transport hooks + + /** + * Called by the `transport` when the connection is closed. + */ + onClosed() { + // Cleanup all the registered sessions. + for (const session of this.sessions.values()) { + session.destructor(); + } + this.sessions.clear(); + + super.onClosed(); + } + + /** + * Receive a packet from the WebSocket layer. + * + * This packet is sent by a CDP client and is meant to execute + * a particular function on a given Domain. + * + * @param {object} packet + * JSON-serializable object sent by the client. + */ + async onPacket(packet) { + super.onPacket(packet); + + const { id, method, params, sessionId } = packet; + const startTime = Cu.now(); + + try { + // First check for mandatory field in the packets + if (typeof id == "undefined") { + throw new TypeError("Message missing 'id' field"); + } + if (typeof method == "undefined") { + throw new TypeError("Message missing 'method' field"); + } + + // Extract the domain name and the method name out of `method` attribute + const { domain, command } = splitMethod(method); + + // If a `sessionId` field is passed, retrieve the session to which we + // should forward this packet. Otherwise send it to the default session. + let session; + if (!sessionId) { + if (!this.defaultSession) { + throw new Error("Connection is missing a default Session."); + } + session = this.defaultSession; + } else { + session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`Session '${sessionId}' doesn't exists.`); + } + } + + // Bug 1600317 - Workaround to deny internal methods to be called + if (command.startsWith("_")) { + throw new lazy.UnknownMethodError(command); + } + + // Finally, instruct the targeted session to execute the command + const result = await session.execute(id, domain, command, params); + this.sendResult(id, result, sessionId); + } catch (e) { + this.sendError(id, e, packet.sessionId); + } + + if (Services.profiler?.IsActive()) { + ChromeUtils.addProfilerMarker( + "CDP: Command", + { startTime, category: "Remote-Protocol" }, + `${method} (${id})` + ); + } + } +} + +/** + * Splits a CDP method into domain and command components. + * + * @param {string} method + * Name of the method to split, e.g. "Browser.getVersion". + * + * @returns {Object<string, string>} + * Object with the domain ("Browser") and command ("getVersion") + * as properties. + */ +export function splitMethod(method) { + const parts = method.split("."); + + if (parts.length != 2 || !parts[0].length || !parts[1].length) { + throw new TypeError(`Invalid method format: '${method}'`); + } + + return { + domain: parts[0], + command: parts[1], + }; +} |