MessagePayload.js (11299B)
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 Component, 9 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const { div, input, label, span, h2 } = dom; 13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 14 15 const { 16 connect, 17 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 18 19 const { 20 L10N, 21 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 22 const { 23 getMessagePayload, 24 getResponseHeader, 25 parseJSON, 26 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 27 const { 28 getFormattedSize, 29 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); 30 const MESSAGE_DATA_LIMIT = Services.prefs.getIntPref( 31 "devtools.netmonitor.msg.messageDataLimit" 32 ); 33 const MESSAGE_DATA_TRUNCATED = L10N.getStr("messageDataTruncated"); 34 const SocketIODecoder = require("resource://devtools/client/netmonitor/src/components/messages/parsers/socket-io/index.js"); 35 const { 36 JsonHubProtocol, 37 HandshakeProtocol, 38 } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/signalr/index.js"); 39 const { 40 parseSockJS, 41 } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/sockjs/index.js"); 42 const { 43 parseStompJs, 44 } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/stomp/index.js"); 45 const { 46 wampSerializers, 47 } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/wamp/serializers.js"); 48 const { 49 getRequestByChannelId, 50 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 51 52 // Components 53 const RawData = createFactory( 54 require("resource://devtools/client/netmonitor/src/components/messages/RawData.js") 55 ); 56 loader.lazyGetter(this, "PropertiesView", function () { 57 return createFactory( 58 require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") 59 ); 60 }); 61 62 const RAW_DATA = L10N.getStr("netmonitor.response.raw"); 63 64 /** 65 * Shows the full payload of a message. 66 * The payload is unwrapped from the LongStringActor object. 67 */ 68 class MessagePayload extends Component { 69 static get propTypes() { 70 return { 71 connector: PropTypes.object.isRequired, 72 selectedMessage: PropTypes.object, 73 request: PropTypes.object.isRequired, 74 }; 75 } 76 77 constructor(props) { 78 super(props); 79 80 this.state = { 81 payload: "", 82 isFormattedData: false, 83 formattedData: {}, 84 formattedDataTitle: "", 85 rawDataDisplayed: false, 86 }; 87 88 this.toggleRawData = this.toggleRawData.bind(this); 89 this.renderRawDataBtn = this.renderRawDataBtn.bind(this); 90 } 91 92 componentDidMount() { 93 this.updateMessagePayload(); 94 } 95 96 componentDidUpdate(prevProps) { 97 if (this.props.selectedMessage !== prevProps.selectedMessage) { 98 this.updateMessagePayload(); 99 } 100 } 101 102 updateMessagePayload() { 103 const { selectedMessage, connector } = this.props; 104 105 getMessagePayload(selectedMessage.payload, connector.getLongString).then( 106 async payload => { 107 const { formattedData, formattedDataTitle } = 108 await this.parsePayload(payload); 109 this.setState({ 110 payload, 111 isFormattedData: !!formattedData, 112 formattedData, 113 formattedDataTitle, 114 }); 115 } 116 ); 117 } 118 119 async parsePayload(payload) { 120 const { connector, selectedMessage, request } = this.props; 121 122 // Don't apply formatting to control frames 123 // Control frame check can be done using opCode as specified here: 124 // https://tools.ietf.org/html/rfc6455 125 const controlFrames = [0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf]; 126 const isControlFrame = controlFrames.includes(selectedMessage.opCode); 127 if (isControlFrame) { 128 return { 129 formattedData: null, 130 formattedDataTitle: "", 131 }; 132 } 133 134 // Make sure that request headers are fetched from the backend before 135 // looking for `Sec-WebSocket-Protocol` header. 136 const responseHeaders = await connector.requestData( 137 request.id, 138 "responseHeaders" 139 ); 140 141 if (!responseHeaders.headers) { 142 // If the network event actor was destroyed while retrieving the request 143 // data, no headers will be available. 144 return { 145 formattedData: null, 146 formattedDataTitle: "", 147 }; 148 } 149 150 const wsProtocol = getResponseHeader( 151 { responseHeaders }, 152 "Sec-WebSocket-Protocol" 153 ); 154 155 const wampSerializer = wampSerializers[wsProtocol]; 156 if (wampSerializer) { 157 const wampPayload = wampSerializer.deserializeMessage(payload); 158 159 return { 160 formattedData: wampPayload, 161 formattedDataTitle: wampSerializer.description, 162 }; 163 } 164 165 // socket.io payload 166 const socketIOPayload = this.parseSocketIOPayload(payload); 167 168 if (socketIOPayload) { 169 return { 170 formattedData: socketIOPayload, 171 formattedDataTitle: "Socket.IO", 172 }; 173 } 174 // sockjs payload 175 const sockJSPayload = parseSockJS(payload); 176 if (sockJSPayload) { 177 let formattedData = sockJSPayload.data; 178 179 if (sockJSPayload.type === "message") { 180 if (Array.isArray(formattedData)) { 181 formattedData = formattedData.map( 182 message => parseStompJs(message) || message 183 ); 184 } else { 185 formattedData = parseStompJs(formattedData) || formattedData; 186 } 187 } 188 189 return { 190 formattedData, 191 formattedDataTitle: "SockJS", 192 }; 193 } 194 // signalr payload 195 const signalRPayload = this.parseSignalR(payload); 196 if (signalRPayload) { 197 return { 198 formattedData: signalRPayload, 199 formattedDataTitle: "SignalR", 200 }; 201 } 202 // STOMP 203 const stompPayload = parseStompJs(payload); 204 if (stompPayload) { 205 return { 206 formattedData: stompPayload, 207 formattedDataTitle: "STOMP", 208 }; 209 } 210 211 // json payload 212 let { json } = parseJSON(payload); 213 if (json) { 214 const { data, identifier } = json; 215 // A json payload MAY be an "Action cable" if it 216 // contains either a `data` or an `identifier` property 217 // which are also json strings and would need to be parsed. 218 // See https://medium.com/codequest/actioncable-in-rails-api-f087b65c860d 219 if ( 220 (data && typeof data == "string") || 221 (identifier && typeof identifier == "string") 222 ) { 223 const actionCablePayload = this.parseActionCable(json); 224 return { 225 formattedData: actionCablePayload, 226 formattedDataTitle: "Action Cable", 227 }; 228 } 229 230 if (Array.isArray(json)) { 231 json = json.map(message => parseStompJs(message) || message); 232 } 233 234 return { 235 formattedData: json, 236 formattedDataTitle: "JSON", 237 }; 238 } 239 return { 240 formattedData: null, 241 formattedDataTitle: "", 242 }; 243 } 244 245 parseSocketIOPayload(payload) { 246 let result; 247 // Try decoding socket.io frames 248 try { 249 const decoder = new SocketIODecoder(); 250 decoder.on("decoded", decodedPacket => { 251 if ( 252 decodedPacket && 253 !decodedPacket.data.includes("parser error") && 254 decodedPacket.type 255 ) { 256 result = decodedPacket; 257 } 258 }); 259 decoder.add(payload); 260 return result; 261 } catch (err) { 262 // Ignore errors 263 } 264 return null; 265 } 266 267 parseSignalR(payload) { 268 // attempt to parse as HandshakeResponseMessage 269 let decoder; 270 try { 271 decoder = new HandshakeProtocol(); 272 const [remainingData, responseMessage] = 273 decoder.parseHandshakeResponse(payload); 274 275 if (responseMessage) { 276 return { 277 handshakeResponse: responseMessage, 278 remainingData: this.parseSignalR(remainingData), 279 }; 280 } 281 } catch (err) { 282 // ignore errors; 283 } 284 285 // attempt to parse as JsonHubProtocolMessage 286 try { 287 decoder = new JsonHubProtocol(); 288 const msgs = decoder.parseMessages(payload, null); 289 if (msgs?.length) { 290 return msgs; 291 } 292 } catch (err) { 293 // ignore errors; 294 } 295 296 // MVP Signalr 297 if (payload.endsWith("\u001e")) { 298 const { json } = parseJSON(payload.slice(0, -1)); 299 if (json) { 300 return json; 301 } 302 } 303 304 return null; 305 } 306 307 parseActionCable(payload) { 308 const identifier = payload.identifier && parseJSON(payload.identifier).json; 309 const data = payload.data && parseJSON(payload.data).json; 310 311 if (identifier) { 312 payload.identifier = identifier; 313 } 314 if (data) { 315 payload.data = data; 316 } 317 return payload; 318 } 319 320 toggleRawData() { 321 this.setState({ 322 rawDataDisplayed: !this.state.rawDataDisplayed, 323 }); 324 } 325 326 renderRawDataBtn(key, checked, onChange) { 327 return [ 328 label( 329 { 330 key: `${key}RawDataBtn`, 331 className: "raw-data-toggle", 332 htmlFor: `raw-${key}-checkbox`, 333 onClick: event => { 334 // stop the header click event 335 event.stopPropagation(); 336 }, 337 }, 338 span({ className: "raw-data-toggle-label" }, RAW_DATA), 339 span( 340 { className: "raw-data-toggle-input" }, 341 input({ 342 id: `raw-${key}-checkbox`, 343 checked, 344 className: "devtools-checkbox-toggle", 345 onChange, 346 type: "checkbox", 347 }) 348 ) 349 ), 350 ]; 351 } 352 353 renderData(component, componentProps) { 354 return component(componentProps); 355 } 356 357 render() { 358 let component; 359 let componentProps; 360 let dataLabel; 361 let { payload, rawDataDisplayed } = this.state; 362 let isTruncated = false; 363 if (this.state.payload.length >= MESSAGE_DATA_LIMIT) { 364 payload = payload.substring(0, MESSAGE_DATA_LIMIT); 365 isTruncated = true; 366 } 367 368 if ( 369 !isTruncated && 370 this.state.isFormattedData && 371 !this.state.rawDataDisplayed 372 ) { 373 component = PropertiesView; 374 componentProps = { 375 object: this.state.formattedData, 376 }; 377 dataLabel = this.state.formattedDataTitle; 378 } else { 379 component = RawData; 380 componentProps = { payload }; 381 dataLabel = L10N.getFormatStrWithNumbers( 382 "netmonitor.ws.rawData.header", 383 getFormattedSize(this.state.payload.length) 384 ); 385 } 386 387 return div( 388 { 389 className: "message-payload", 390 }, 391 isTruncated && 392 div( 393 { 394 className: "truncated-data-message", 395 }, 396 MESSAGE_DATA_TRUNCATED 397 ), 398 h2({ className: "data-header", role: "heading" }, [ 399 span({ key: "data-label", className: "data-label" }, dataLabel), 400 !isTruncated && 401 this.state.isFormattedData && 402 this.renderRawDataBtn("data", rawDataDisplayed, this.toggleRawData), 403 ]), 404 this.renderData(component, componentProps) 405 ); 406 } 407 } 408 409 module.exports = connect(state => ({ 410 request: getRequestByChannelId(state, state.messages.currentChannelId), 411 }))(MessagePayload);