summaryrefslogtreecommitdiffstats
path: root/remote/Connection.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--remote/Connection.jsm304
1 files changed, 304 insertions, 0 deletions
diff --git a/remote/Connection.jsm b/remote/Connection.jsm
new file mode 100644
index 0000000000..a87d6a9a3b
--- /dev/null
+++ b/remote/Connection.jsm
@@ -0,0 +1,304 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["Connection"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { truncate } = ChromeUtils.import("chrome://remote/content/Format.jsm");
+const { Log } = ChromeUtils.import("chrome://remote/content/Log.jsm");
+const { UnknownMethodError } = ChromeUtils.import(
+ "chrome://remote/content/Error.jsm"
+);
+
+XPCOMUtils.defineLazyGetter(this, "log", Log.get);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "UUIDGen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator"
+);
+
+class Connection {
+ /**
+ * @param WebSocketTransport transport
+ * @param httpd.js's Connection httpdConnection
+ */
+ constructor(transport, httpdConnection) {
+ this.id = UUIDGen.generateUUID().toString();
+ this.transport = transport;
+ this.httpdConnection = httpdConnection;
+
+ this.transport.hooks = this;
+ this.transport.ready();
+
+ this.defaultSession = null;
+ this.sessions = new Map();
+ }
+
+ /**
+ * Register a new Session to forward the messages to.
+ * 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
+ */
+ registerSession(session) {
+ 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(body) {
+ const payload = JSON.stringify(body, null, Log.verbose ? "\t" : null);
+ log.trace(truncate`<-(connection ${this.id}) ${payload}`);
+ this.transport.send(JSON.parse(payload));
+ }
+
+ /**
+ * Send an error back to the client.
+ *
+ * @param Number id
+ * Id of the packet which lead to an error.
+ * @param Error e
+ * Error object with `message` and `stack` attributes.
+ * @param Number sessionId (Optional)
+ * Id of the session used to send this packet.
+ * This will be null if that was the default session.
+ */
+ onError(id, e, sessionId) {
+ const error = {
+ message: e.message,
+ data: e.stack,
+ };
+ this.send({ id, sessionId, error });
+ }
+
+ /**
+ * Send the result of a call to a Domain's function.
+ *
+ * @param Number id
+ * The request id being sent by the client to call the domain's method.
+ * @param Object result
+ * A JSON-serializable value which is the actual result.
+ * @param Number sessionId
+ * The sessionId from which this packet is emitted.
+ * This will be undefined for the default session.
+ */
+ onResult(id, result, sessionId) {
+ this.sendResult(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) {
+ // Temporarily disabled due to spamming of the console (bug 1598468).
+ // Event should only be sent on protocol messages (eg. attachedToTarget)
+ // this.sendEvent("Target.receivedMessageFromTarget", {
+ // sessionId,
+ // // receivedMessageFromTarget is expected to send a raw CDP packet
+ // // in the `message` property and it to be already serialized to a
+ // // string
+ // message: JSON.stringify({
+ // id,
+ // result,
+ // }),
+ // });
+ }
+ }
+
+ sendResult(id, result, sessionId) {
+ this.send({
+ sessionId, // this will be undefined for the default session
+ id,
+ result: typeof result != "undefined" ? result : {},
+ });
+ }
+
+ /**
+ * 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 value which is the payload
+ * associated with this event.
+ * @param Number sessionId
+ * The sessionId from which this packet is emitted.
+ * This will be undefined for the default session.
+ */
+ onEvent(method, params, sessionId) {
+ this.sendEvent(method, params, sessionId);
+
+ // 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) {
+ // Temporarily disabled due to spamming of the console (bug 1598468).
+ // Event should only be sent on protocol messages (eg. attachedToTarget)
+ // this.sendEvent("Target.receivedMessageFromTarget", {
+ // sessionId,
+ // message: JSON.stringify({
+ // method,
+ // params,
+ // }),
+ // });
+ }
+ }
+
+ sendEvent(method, params, sessionId) {
+ this.send({
+ sessionId, // this will be undefined for the default session
+ method,
+ params,
+ });
+ }
+
+ // transport hooks
+
+ /**
+ * 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) {
+ log.trace(`(connection ${this.id})-> ${JSON.stringify(packet)}`);
+
+ try {
+ const { id, method, params, sessionId } = packet;
+
+ // 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 } = Connection.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 UnknownMethodError(command);
+ }
+
+ // Finally, instruct the targeted session to execute the command
+ const result = await session.execute(id, domain, command, params);
+ this.onResult(id, result, sessionId);
+ } catch (e) {
+ this.onError(packet.id, e, packet.sessionId);
+ }
+ }
+
+ /**
+ * 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
+ * JSON payload of the CDP packet stringified to a string.
+ * The CDP packet 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 exists.`);
+ }
+ // `message` is received from `Target.sendMessageToTarget` where the
+ // message attribute is a stringify 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);
+ }
+
+ /**
+ * Instruct the connection to close.
+ * This will ask the transport to shutdown the WebSocket connection
+ * and destroy all active sessions.
+ */
+ close() {
+ this.transport.close();
+
+ // In addition to the WebSocket transport, we also have to close the Connection
+ // used internaly within httpd.js. Otherwise the server doesn't shut down correctly
+ // and keep these Connection instances alive.
+ this.httpdConnection.close();
+ }
+
+ /**
+ * This is called by the `transport` when the connection is closed.
+ * Cleanup all the registered sessions.
+ */
+ onClosed(status) {
+ for (const session of this.sessions.values()) {
+ session.destructor();
+ }
+ this.sessions.clear();
+ }
+
+ /**
+ * Splits a method, e.g. "Browser.getVersion",
+ * into domain ("Browser") and command ("getVersion") components.
+ */
+ static splitMethod(s) {
+ const ss = s.split(".");
+ if (ss.length != 2 || ss[0].length == 0 || ss[1].length == 0) {
+ throw new TypeError(`Invalid method format: "${s}"`);
+ }
+ return {
+ domain: ss[0],
+ command: ss[1],
+ };
+ }
+
+ toString() {
+ return `[object Connection ${this.id}]`;
+ }
+}