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 { Log } = ChromeUtils.import("chrome://remote/content/Log.jsm");
10 const { XPCOMUtils } = ChromeUtils.import(
11 "resource://gre/modules/XPCOMUtils.jsm"
14 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
15 XPCOMUtils.defineLazyServiceGetter(
18 "@mozilla.org/uuid-generator;1",
24 * @param WebSocketTransport transport
25 * @param httpd.js's Connection httpdConnection
27 constructor(transport, httpdConnection) {
28 this.id = UUIDGen.generateUUID().toString();
29 this.transport = transport;
30 this.httpdConnection = httpdConnection;
32 this.transport.hooks = this;
33 this.transport.ready();
35 this.defaultSession = null;
36 this.sessions = new Map();
40 * Register a new Session to forward the messages to.
41 * Session without any `id` attribute will be considered to be the
42 * default one, to which messages without `sessionId` attribute are
43 * forwarded to. Only one such session can be registered.
45 * @param Session session
47 registerSession(session) {
49 if (this.defaultSession) {
51 "Default session is already set on Connection," +
52 "can't register another one."
55 this.defaultSession = session;
57 this.sessions.set(session.id, session);
61 log.trace(`<-(connection ${this.id}) ${JSON.stringify(message)}`);
62 this.transport.send(message);
66 * Send an error back to the client.
69 * Id of the packet which lead to an error.
71 * Error object with `message` and `stack` attributes.
72 * @param Number sessionId (Optional)
73 * Id of the session used to send this packet.
74 * This will be null if that was the default session.
76 onError(id, e, sessionId) {
81 this.send({ id, sessionId, error });
85 * Send the result of a call to a Domain's function.
88 * The request id being sent by the client to call the domain's method.
89 * @param Object result
90 * A JSON-serializable value which is the actual result.
91 * @param Number sessionId
92 * The sessionId from which this packet is emitted.
93 * This will be undefined for the default session.
95 onResult(id, result, sessionId) {
96 this.sendResult(id, result, sessionId);
98 // When a client attaches to a secondary target via
99 // `Target.attachToTarget`, and it executes a command via
100 // `Target.sendMessageToTarget`, we should emit an event back with the
101 // result including the `sessionId` attribute of this secondary target's
102 // session. `Target.attachToTarget` creates the secondary session and
103 // returns the session ID.
105 this.sendEvent("Target.receivedMessageFromTarget", {
107 // receivedMessageFromTarget is expected to send a raw CDP packet
108 // in the `message` property and it to be already serialized to a
110 message: JSON.stringify({
118 sendResult(id, result, sessionId) {
120 sessionId, // this will be undefined for the default session
127 * Send an event coming from a Domain to the CDP client.
129 * @param String method
130 * The event name. This is composed by a domain name,
131 * a dot character followed by the event name.
132 * e.g. `Target.targetCreated`
133 * @param Object params
134 * A JSON-serializable value which is the payload
135 * associated with this event.
136 * @param Number sessionId
137 * The sessionId from which this packet is emitted.
138 * This will be undefined for the default session.
140 onEvent(method, params, sessionId) {
141 this.sendEvent(method, params, sessionId);
143 // When a client attaches to a secondary target via
144 // `Target.attachToTarget`, we should emit an event back with the
145 // result including the `sessionId` attribute of this secondary target's
146 // session. `Target.attachToTarget` creates the secondary session and
147 // returns the session ID.
149 this.sendEvent("Target.receivedMessageFromTarget", {
151 message: JSON.stringify({
159 sendEvent(method, params, sessionId) {
161 sessionId, // this will be undefined for the default session
170 * Receive a packet from the WebSocket layer.
171 * This packet is sent by a CDP client and is meant to execute
172 * a particular function on a given Domain.
174 * @param Object packet
175 * JSON-serializable object sent by the client
177 async onPacket(packet) {
178 log.trace(`(connection ${this.id})-> ${JSON.stringify(packet)}`);
181 const { id, method, params, sessionId } = packet;
183 // First check for mandatory field in the packets
184 if (typeof id == "undefined") {
185 throw new TypeError("Message missing 'id' field");
187 if (typeof method == "undefined") {
188 throw new TypeError("Message missing 'method' field");
191 // Extract the domain name and the method name out of `method` attribute
192 const { domain, command } = Connection.splitMethod(method);
194 // If a `sessionId` field is passed, retrieve the session to which we
195 // should forward this packet. Otherwise send it to the default session.
198 if (!this.defaultSession) {
199 throw new Error(`Connection is missing a default Session.`);
201 session = this.defaultSession;
203 session = this.sessions.get(sessionId);
205 throw new Error(`Session '${sessionId}' doesn't exists.`);
209 // Finally, instruct the targeted session to execute the command
210 const result = await session.execute(id, domain, command, params);
211 this.onResult(id, result, sessionId);
214 this.onError(packet.id, e, packet.sessionId);
219 * Interpret a given CDP packet for a given Session.
221 * @param String sessionId
222 * ID of the session for which we should execute a command.
223 * @param String message
224 * JSON payload of the CDP packet stringified to a string.
225 * The CDP packet is about executing a Domain's function.
227 sendMessageToTarget(sessionId, message) {
228 const session = this.sessions.get(sessionId);
230 throw new Error(`Session '${sessionId}' doesn't exists.`);
232 // `message` is received from `Target.sendMessageToTarget` where the
233 // message attribute is a stringify JSON payload which represent a CDP
235 const packet = JSON.parse(message);
237 // The CDP packet sent by the client shouldn't have a sessionId attribute
238 // as it is passed as another argument of `Target.sendMessageToTarget`.
239 // Set it here in order to reuse the codepath of flatten session, where
240 // the client sends CDP packets with a `sessionId` attribute instead
241 // of going through the old and probably deprecated
242 // `Target.sendMessageToTarget` API.
243 packet.sessionId = sessionId;
244 this.onPacket(packet);
248 * Instruct the connection to close.
249 * This will ask the transport to shutdown the WebSocket connection
250 * and destroy all active sessions.
253 this.transport.close();
255 // In addition to the WebSocket transport, we also have to close the Connection
256 // used internaly within httpd.js. Otherwise the server doesn't shut down correctly
257 // and keep these Connection instances alive.
258 this.httpdConnection.close();
262 * This is called by the `transport` when the connection is closed.
263 * Cleanup all the registered sessions.
266 for (const session of this.sessions.values()) {
267 session.destructor();
269 this.sessions.clear();
273 * Splits a method, e.g. "Browser.getVersion",
274 * into domain ("Browser") and command ("getVersion") components.
276 static splitMethod(s) {
277 const ss = s.split(".");
278 if (ss.length != 2 || ss[0].length == 0 || ss[1].length == 0) {
279 throw new TypeError(`Invalid method format: "${s}"`);
288 return `[object Connection ${this.id}]`;