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/. */
7 var EXPORTED_SYMBOLS = ["Connection"];
9 const { XPCOMUtils } = ChromeUtils.import(
10 "resource://gre/modules/XPCOMUtils.jsm"
13 const { truncate } = ChromeUtils.import("chrome://remote/content/Format.jsm");
14 const { Log } = ChromeUtils.import("chrome://remote/content/Log.jsm");
15 const { UnknownMethodError } = ChromeUtils.import(
16 "chrome://remote/content/Error.jsm"
19 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
20 XPCOMUtils.defineLazyServiceGetter(
23 "@mozilla.org/uuid-generator;1",
29 * @param WebSocketTransport transport
30 * @param httpd.js's Connection httpdConnection
32 constructor(transport, httpdConnection) {
33 this.id = UUIDGen.generateUUID().toString();
34 this.transport = transport;
35 this.httpdConnection = httpdConnection;
37 this.transport.hooks = this;
38 this.transport.ready();
40 this.defaultSession = null;
41 this.sessions = new Map();
45 * Register a new Session to forward the messages to.
46 * Session without any `id` attribute will be considered to be the
47 * default one, to which messages without `sessionId` attribute are
48 * forwarded to. Only one such session can be registered.
50 * @param Session session
52 registerSession(session) {
54 if (this.defaultSession) {
56 "Default session is already set on Connection," +
57 "can't register another one."
60 this.defaultSession = session;
62 this.sessions.set(session.id, session);
66 const payload = JSON.stringify(body, null, Log.verbose ? "\t" : null);
67 log.trace(truncate`<-(connection ${this.id}) ${payload}`);
68 this.transport.send(JSON.parse(payload));
72 * Send an error back to the client.
75 * Id of the packet which lead to an error.
77 * Error object with `message` and `stack` attributes.
78 * @param Number sessionId (Optional)
79 * Id of the session used to send this packet.
80 * This will be null if that was the default session.
82 onError(id, e, sessionId) {
87 this.send({ id, sessionId, error });
91 * Send the result of a call to a Domain's function.
94 * The request id being sent by the client to call the domain's method.
95 * @param Object result
96 * A JSON-serializable value which is the actual result.
97 * @param Number sessionId
98 * The sessionId from which this packet is emitted.
99 * This will be undefined for the default session.
101 onResult(id, result, sessionId) {
102 this.sendResult(id, result, sessionId);
104 // When a client attaches to a secondary target via
105 // `Target.attachToTarget`, and it executes a command via
106 // `Target.sendMessageToTarget`, we should emit an event back with the
107 // result including the `sessionId` attribute of this secondary target's
108 // session. `Target.attachToTarget` creates the secondary session and
109 // returns the session ID.
111 // Temporarily disabled due to spamming of the console (bug 1598468).
112 // Event should only be sent on protocol messages (eg. attachedToTarget)
113 // this.sendEvent("Target.receivedMessageFromTarget", {
115 // // receivedMessageFromTarget is expected to send a raw CDP packet
116 // // in the `message` property and it to be already serialized to a
118 // message: JSON.stringify({
126 sendResult(id, result, sessionId) {
128 sessionId, // this will be undefined for the default session
135 * Send an event coming from a Domain to the CDP client.
137 * @param String method
138 * The event name. This is composed by a domain name,
139 * a dot character followed by the event name.
140 * e.g. `Target.targetCreated`
141 * @param Object params
142 * A JSON-serializable value which is the payload
143 * associated with this event.
144 * @param Number sessionId
145 * The sessionId from which this packet is emitted.
146 * This will be undefined for the default session.
148 onEvent(method, params, sessionId) {
149 this.sendEvent(method, params, sessionId);
151 // When a client attaches to a secondary target via
152 // `Target.attachToTarget`, we should emit an event back with the
153 // result including the `sessionId` attribute of this secondary target's
154 // session. `Target.attachToTarget` creates the secondary session and
155 // returns the session ID.
157 // Temporarily disabled due to spamming of the console (bug 1598468).
158 // Event should only be sent on protocol messages (eg. attachedToTarget)
159 // this.sendEvent("Target.receivedMessageFromTarget", {
161 // message: JSON.stringify({
169 sendEvent(method, params, sessionId) {
171 sessionId, // this will be undefined for the default session
180 * Receive a packet from the WebSocket layer.
181 * This packet is sent by a CDP client and is meant to execute
182 * a particular function on a given Domain.
184 * @param Object packet
185 * JSON-serializable object sent by the client
187 async onPacket(packet) {
188 log.trace(`(connection ${this.id})-> ${JSON.stringify(packet)}`);
191 const { id, method, params, sessionId } = packet;
193 // First check for mandatory field in the packets
194 if (typeof id == "undefined") {
195 throw new TypeError("Message missing 'id' field");
197 if (typeof method == "undefined") {
198 throw new TypeError("Message missing 'method' field");
201 // Extract the domain name and the method name out of `method` attribute
202 const { domain, command } = Connection.splitMethod(method);
204 // If a `sessionId` field is passed, retrieve the session to which we
205 // should forward this packet. Otherwise send it to the default session.
208 if (!this.defaultSession) {
209 throw new Error(`Connection is missing a default Session.`);
211 session = this.defaultSession;
213 session = this.sessions.get(sessionId);
215 throw new Error(`Session '${sessionId}' doesn't exists.`);
219 // Bug 1600317 - Workaround to deny internal methods to be called
220 if (command.startsWith("_")) {
221 throw new UnknownMethodError(command);
224 // Finally, instruct the targeted session to execute the command
225 const result = await session.execute(id, domain, command, params);
226 this.onResult(id, result, sessionId);
228 this.onError(packet.id, e, packet.sessionId);
233 * Interpret a given CDP packet for a given Session.
235 * @param String sessionId
236 * ID of the session for which we should execute a command.
237 * @param String message
238 * JSON payload of the CDP packet stringified to a string.
239 * The CDP packet is about executing a Domain's function.
241 sendMessageToTarget(sessionId, message) {
242 const session = this.sessions.get(sessionId);
244 throw new Error(`Session '${sessionId}' doesn't exists.`);
246 // `message` is received from `Target.sendMessageToTarget` where the
247 // message attribute is a stringify JSON payload which represent a CDP
249 const packet = JSON.parse(message);
251 // The CDP packet sent by the client shouldn't have a sessionId attribute
252 // as it is passed as another argument of `Target.sendMessageToTarget`.
253 // Set it here in order to reuse the codepath of flatten session, where
254 // the client sends CDP packets with a `sessionId` attribute instead
255 // of going through the old and probably deprecated
256 // `Target.sendMessageToTarget` API.
257 packet.sessionId = sessionId;
258 this.onPacket(packet);
262 * Instruct the connection to close.
263 * This will ask the transport to shutdown the WebSocket connection
264 * and destroy all active sessions.
267 this.transport.close();
269 // In addition to the WebSocket transport, we also have to close the Connection
270 // used internaly within httpd.js. Otherwise the server doesn't shut down correctly
271 // and keep these Connection instances alive.
272 this.httpdConnection.close();
276 * This is called by the `transport` when the connection is closed.
277 * Cleanup all the registered sessions.
280 for (const session of this.sessions.values()) {
281 session.destructor();
283 this.sessions.clear();
287 * Splits a method, e.g. "Browser.getVersion",
288 * into domain ("Browser") and command ("getVersion") components.
290 static splitMethod(s) {
291 const ss = s.split(".");
292 if (ss.length != 2 || ss[0].length == 0 || ss[1].length == 0) {
293 throw new TypeError(`Invalid method format: "${s}"`);
302 return `[object Connection ${this.id}]`;