websockets.js (5093B)
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/. */ 4 5 "use strict"; 6 7 const { 8 LongStringActor, 9 } = require("resource://devtools/server/actors/string.js"); 10 11 const webSocketEventService = Cc[ 12 "@mozilla.org/websocketevent/service;1" 13 ].getService(Ci.nsIWebSocketEventService); 14 15 class WebSocketWatcher { 16 constructor() { 17 this.windowIds = new Set(); 18 // Maintains a map of all the connection channels per websocket 19 // The map item is keyed on the `webSocketSerialID` and stores 20 // the `httpChannelId` as value. 21 this.connections = new Map(); 22 this.onWindowReady = this.onWindowReady.bind(this); 23 this.onWindowDestroy = this.onWindowDestroy.bind(this); 24 } 25 26 static createResource(wsMessageType, eventParams) { 27 return { 28 wsMessageType, 29 ...eventParams, 30 }; 31 } 32 33 static prepareFramePayload(targetActor, frame) { 34 const payload = new LongStringActor(targetActor.conn, frame.payload); 35 targetActor.manage(payload); 36 return payload.form(); 37 } 38 39 watch(targetActor, { onAvailable }) { 40 this.targetActor = targetActor; 41 this.onAvailable = onAvailable; 42 43 for (const window of this.targetActor.windows) { 44 const { innerWindowId } = window.windowGlobalChild; 45 this.startListening(innerWindowId); 46 } 47 48 // On navigate/reload we should re-start listening with the 49 // new `innerWindowID` 50 this.targetActor.on("window-ready", this.onWindowReady); 51 this.targetActor.on("window-destroyed", this.onWindowDestroy); 52 } 53 54 onWindowReady({ window }) { 55 if (!this.targetActor.followWindowGlobalLifeCycle) { 56 const { innerWindowId } = window.windowGlobalChild; 57 this.startListening(innerWindowId); 58 } 59 } 60 61 onWindowDestroy({ id }) { 62 this.stopListening(id); 63 } 64 65 startListening(innerWindowId) { 66 if (!this.windowIds.has(innerWindowId)) { 67 this.windowIds.add(innerWindowId); 68 webSocketEventService.addListener(innerWindowId, this); 69 } 70 } 71 72 stopListening(innerWindowId) { 73 if (this.windowIds.has(innerWindowId)) { 74 this.windowIds.delete(innerWindowId); 75 if (!webSocketEventService.hasListenerFor(innerWindowId)) { 76 // The listener might have already been cleaned up on `window-destroy`. 77 console.warn( 78 "Already stopped listening to websocket events for this window." 79 ); 80 return; 81 } 82 webSocketEventService.removeListener(innerWindowId, this); 83 } 84 } 85 86 destroy() { 87 for (const id of this.windowIds) { 88 this.stopListening(id); 89 } 90 this.targetActor.off("window-ready", this.onWindowReady); 91 this.targetActor.off("window-destroyed", this.onWindowDestroy); 92 } 93 94 // methods for the nsIWebSocketEventService 95 webSocketCreated() {} 96 97 webSocketOpened( 98 webSocketSerialID, 99 effectiveURI, 100 protocols, 101 extensions, 102 httpChannelId 103 ) { 104 this.connections.set(webSocketSerialID, httpChannelId); 105 const resource = WebSocketWatcher.createResource("webSocketOpened", { 106 httpChannelId, 107 effectiveURI, 108 protocols, 109 extensions, 110 }); 111 112 this.onAvailable([resource]); 113 } 114 115 webSocketMessageAvailable() {} 116 117 webSocketClosed(webSocketSerialID, wasClean, code, reason) { 118 const httpChannelId = this.connections.get(webSocketSerialID); 119 this.connections.delete(webSocketSerialID); 120 121 const resource = WebSocketWatcher.createResource("webSocketClosed", { 122 httpChannelId, 123 wasClean, 124 code, 125 reason, 126 }); 127 128 this.onAvailable([resource]); 129 } 130 131 frameReceived(webSocketSerialID, frame) { 132 const httpChannelId = this.connections.get(webSocketSerialID); 133 if (!httpChannelId) { 134 return; 135 } 136 137 const payload = WebSocketWatcher.prepareFramePayload( 138 this.targetActor, 139 frame 140 ); 141 const resource = WebSocketWatcher.createResource("frameReceived", { 142 httpChannelId, 143 data: { 144 type: "received", 145 payload, 146 timeStamp: frame.timeStamp, 147 finBit: frame.finBit, 148 rsvBit1: frame.rsvBit1, 149 rsvBit2: frame.rsvBit2, 150 rsvBit3: frame.rsvBit3, 151 opCode: frame.opCode, 152 mask: frame.mask, 153 maskBit: frame.maskBit, 154 }, 155 }); 156 157 this.onAvailable([resource]); 158 } 159 160 frameSent(webSocketSerialID, frame) { 161 const httpChannelId = this.connections.get(webSocketSerialID); 162 163 if (!httpChannelId) { 164 return; 165 } 166 167 const payload = WebSocketWatcher.prepareFramePayload( 168 this.targetActor, 169 frame 170 ); 171 const resource = WebSocketWatcher.createResource("frameSent", { 172 httpChannelId, 173 data: { 174 type: "sent", 175 payload, 176 timeStamp: frame.timeStamp, 177 finBit: frame.finBit, 178 rsvBit1: frame.rsvBit1, 179 rsvBit2: frame.rsvBit2, 180 rsvBit3: frame.rsvBit3, 181 opCode: frame.opCode, 182 mask: frame.mask, 183 maskBit: frame.maskBit, 184 }, 185 }); 186 187 this.onAvailable([resource]); 188 } 189 } 190 191 module.exports = WebSocketWatcher;