gecko-dev/remote/Connection.jsm

305 lines
9.6 KiB
JavaScript

/* 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}]`;
}
}