summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources/websockets.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/resources/websockets.js')
-rw-r--r--devtools/server/actors/resources/websockets.js196
1 files changed, 196 insertions, 0 deletions
diff --git a/devtools/server/actors/resources/websockets.js b/devtools/server/actors/resources/websockets.js
new file mode 100644
index 0000000000..5845357a9c
--- /dev/null
+++ b/devtools/server/actors/resources/websockets.js
@@ -0,0 +1,196 @@
+/* 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";
+
+const {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+const {
+ TYPES: { WEBSOCKET },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const webSocketEventService = Cc[
+ "@mozilla.org/websocketevent/service;1"
+].getService(Ci.nsIWebSocketEventService);
+
+class WebSocketWatcher {
+ constructor() {
+ this.windowIds = new Set();
+ // Maintains a map of all the connection channels per websocket
+ // The map item is keyed on the `webSocketSerialID` and stores
+ // the `httpChannelId` as value.
+ this.connections = new Map();
+ this.onWindowReady = this.onWindowReady.bind(this);
+ this.onWindowDestroy = this.onWindowDestroy.bind(this);
+ }
+
+ static createResource(wsMessageType, eventParams) {
+ return {
+ resourceType: WEBSOCKET,
+ wsMessageType,
+ ...eventParams,
+ };
+ }
+
+ static prepareFramePayload(targetActor, frame) {
+ const payload = new LongStringActor(targetActor.conn, frame.payload);
+ targetActor.manage(payload);
+ return payload.form();
+ }
+
+ watch(targetActor, { onAvailable }) {
+ this.targetActor = targetActor;
+ this.onAvailable = onAvailable;
+
+ for (const window of this.targetActor.windows) {
+ const { innerWindowId } = window.windowGlobalChild;
+ this.startListening(innerWindowId);
+ }
+
+ // On navigate/reload we should re-start listening with the
+ // new `innerWindowID`
+ this.targetActor.on("window-ready", this.onWindowReady);
+ this.targetActor.on("window-destroyed", this.onWindowDestroy);
+ }
+
+ onWindowReady({ window }) {
+ if (!this.targetActor.followWindowGlobalLifeCycle) {
+ const { innerWindowId } = window.windowGlobalChild;
+ this.startListening(innerWindowId);
+ }
+ }
+
+ onWindowDestroy({ id }) {
+ this.stopListening(id);
+ }
+
+ startListening(innerWindowId) {
+ if (!this.windowIds.has(innerWindowId)) {
+ this.windowIds.add(innerWindowId);
+ webSocketEventService.addListener(innerWindowId, this);
+ }
+ }
+
+ stopListening(innerWindowId) {
+ if (this.windowIds.has(innerWindowId)) {
+ this.windowIds.delete(innerWindowId);
+ if (!webSocketEventService.hasListenerFor(innerWindowId)) {
+ // The listener might have already been cleaned up on `window-destroy`.
+ console.warn(
+ "Already stopped listening to websocket events for this window."
+ );
+ return;
+ }
+ webSocketEventService.removeListener(innerWindowId, this);
+ }
+ }
+
+ destroy() {
+ for (const id of this.windowIds) {
+ this.stopListening(id);
+ }
+ this.targetActor.off("window-ready", this.onWindowReady);
+ this.targetActor.off("window-destroyed", this.onWindowDestroy);
+ }
+
+ // methods for the nsIWebSocketEventService
+ webSocketCreated(webSocketSerialID, uri, protocols) {}
+
+ webSocketOpened(
+ webSocketSerialID,
+ effectiveURI,
+ protocols,
+ extensions,
+ httpChannelId
+ ) {
+ this.connections.set(webSocketSerialID, httpChannelId);
+ const resource = WebSocketWatcher.createResource("webSocketOpened", {
+ httpChannelId,
+ effectiveURI,
+ protocols,
+ extensions,
+ });
+
+ this.onAvailable([resource]);
+ }
+
+ webSocketMessageAvailable(webSocketSerialID, data, messageType) {}
+
+ webSocketClosed(webSocketSerialID, wasClean, code, reason) {
+ const httpChannelId = this.connections.get(webSocketSerialID);
+ this.connections.delete(webSocketSerialID);
+
+ const resource = WebSocketWatcher.createResource("webSocketClosed", {
+ httpChannelId,
+ wasClean,
+ code,
+ reason,
+ });
+
+ this.onAvailable([resource]);
+ }
+
+ frameReceived(webSocketSerialID, frame) {
+ const httpChannelId = this.connections.get(webSocketSerialID);
+ if (!httpChannelId) {
+ return;
+ }
+
+ const payload = WebSocketWatcher.prepareFramePayload(
+ this.targetActor,
+ frame
+ );
+ const resource = WebSocketWatcher.createResource("frameReceived", {
+ httpChannelId,
+ data: {
+ type: "received",
+ payload,
+ timeStamp: frame.timeStamp,
+ finBit: frame.finBit,
+ rsvBit1: frame.rsvBit1,
+ rsvBit2: frame.rsvBit2,
+ rsvBit3: frame.rsvBit3,
+ opCode: frame.opCode,
+ mask: frame.mask,
+ maskBit: frame.maskBit,
+ },
+ });
+
+ this.onAvailable([resource]);
+ }
+
+ frameSent(webSocketSerialID, frame) {
+ const httpChannelId = this.connections.get(webSocketSerialID);
+
+ if (!httpChannelId) {
+ return;
+ }
+
+ const payload = WebSocketWatcher.prepareFramePayload(
+ this.targetActor,
+ frame
+ );
+ const resource = WebSocketWatcher.createResource("frameSent", {
+ httpChannelId,
+ data: {
+ type: "sent",
+ payload,
+ timeStamp: frame.timeStamp,
+ finBit: frame.finBit,
+ rsvBit1: frame.rsvBit1,
+ rsvBit2: frame.rsvBit2,
+ rsvBit3: frame.rsvBit3,
+ opCode: frame.opCode,
+ mask: frame.mask,
+ maskBit: frame.maskBit,
+ },
+ });
+
+ this.onAvailable([resource]);
+ }
+}
+
+module.exports = WebSocketWatcher;