diff options
Diffstat (limited to '')
-rw-r--r-- | remote/Connection.jsm | 304 |
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}]`; + } +} |