summaryrefslogtreecommitdiffstats
path: root/remote/cdp/CDPConnection.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/cdp/CDPConnection.sys.mjs')
-rw-r--r--remote/cdp/CDPConnection.sys.mjs290
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],
+ };
+}