/* 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/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", Log: "chrome://remote/content/shared/Log.sys.mjs", truncate: "chrome://remote/content/shared/Format.sys.mjs", WebSocketTransport: "chrome://remote/content/server/WebSocketTransport.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); XPCOMUtils.defineLazyPreferenceGetter( lazy, "truncateLog", "remote.log.truncate", false ); const MAX_LOG_LENGTH = 2500; export class WebSocketConnection { /** * @param {WebSocket} webSocket * The WebSocket server connection to wrap. * @param {Connection} httpdConnection * Reference to the httpd.js's connection needed for clean-up. */ constructor(webSocket, httpdConnection) { this.id = lazy.generateUUID(); this.httpdConnection = httpdConnection; this.transport = new lazy.WebSocketTransport(webSocket); this.transport.hooks = this; this.transport.ready(); lazy.logger.debug(`${this.constructor.name} ${this.id} accepted`); } #log(direction, data) { if (lazy.Log.isDebugLevelOrMore) { function replacer(key, value) { if (typeof value === "string") { return lazy.truncate`${value}`; } return value; } let payload = JSON.stringify( data, replacer, lazy.Log.verbose ? "\t" : null ); if (lazy.truncateLog && payload.length > MAX_LOG_LENGTH) { // Even if we truncate individual values, the resulting message might be // huge if we are serializing big objects with many properties or items. // Truncate the overall message to avoid issues in logs. const truncated = payload.substring(0, MAX_LOG_LENGTH); payload = `${truncated} [... truncated after ${MAX_LOG_LENGTH} characters]`; } lazy.logger.debug( `${this.constructor.name} ${this.id} ${direction} ${payload}` ); } } /** * Close the WebSocket connection. */ close() { this.transport.close(); } /** * Register a new Session to forward the messages to. * * Needs to be implemented in the sub class. */ registerSession() { throw new Error("Not implemented"); } /** * Send the JSON-serializable object to the client. * * @param {object} data * The object to be sent. */ send(data) { this.#log("<-", data); this.transport.send(data); } /** * Send an error back to the client. * * Needs to be implemented in the sub class. */ sendError() { throw new Error("Not implemented"); } /** * Send an event back to the client. * * Needs to be implemented in the sub class. */ sendEvent() { throw new Error("Not implemented"); } /** * Send the result of a call to a method back to the client. * * Needs to be implemented in the sub class. */ sendResult() { throw new Error("Not implemented"); } toString() { return `[object ${this.constructor.name} ${this.id}]`; } // Transport hooks /** * Called by the `transport` when the connection is closed. */ onConnectionClose() { lazy.logger.debug(`${this.constructor.name} ${this.id} closed`); } /** * Called when the socket is closed. */ onSocketClose() { // In addition to the WebSocket transport, we also have to close the // connection used internally within httpd.js. Otherwise the server doesn't // shut down correctly, and keeps these Connection instances alive. this.httpdConnection.close(); } /** * Receive a packet from the WebSocket layer. * * This packet is sent by a WebSocket client and is meant to execute * a particular method. * * Needs to be implemented in the sub class. * * @param {object} packet * JSON-serializable object sent by the client. */ async onPacket(packet) { this.#log("->", packet); } }