/* 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 { Component, createFactory, } = require("resource://devtools/client/shared/vendor/react.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const { div, input, label, span, h2 } = dom; const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const { connect, } = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); const { L10N, } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); const { getMessagePayload, getResponseHeader, parseJSON, } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); const { getFormattedSize, } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); const MESSAGE_DATA_LIMIT = Services.prefs.getIntPref( "devtools.netmonitor.msg.messageDataLimit" ); const MESSAGE_DATA_TRUNCATED = L10N.getStr("messageDataTruncated"); const SocketIODecoder = require("resource://devtools/client/netmonitor/src/components/messages/parsers/socket-io/index.js"); const { JsonHubProtocol, HandshakeProtocol, } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/signalr/index.js"); const { parseSockJS, } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/sockjs/index.js"); const { parseStompJs, } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/stomp/index.js"); const { wampSerializers, } = require("resource://devtools/client/netmonitor/src/components/messages/parsers/wamp/serializers.js"); const { getRequestByChannelId, } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); // Components const RawData = createFactory( require("resource://devtools/client/netmonitor/src/components/messages/RawData.js") ); loader.lazyGetter(this, "PropertiesView", function () { return createFactory( require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") ); }); const RAW_DATA = L10N.getStr("netmonitor.response.raw"); /** * Shows the full payload of a message. * The payload is unwrapped from the LongStringActor object. */ class MessagePayload extends Component { static get propTypes() { return { connector: PropTypes.object.isRequired, selectedMessage: PropTypes.object, request: PropTypes.object.isRequired, }; } constructor(props) { super(props); this.state = { payload: "", isFormattedData: false, formattedData: {}, formattedDataTitle: "", rawDataDisplayed: false, }; this.toggleRawData = this.toggleRawData.bind(this); this.renderRawDataBtn = this.renderRawDataBtn.bind(this); } componentDidMount() { this.updateMessagePayload(); } componentDidUpdate(prevProps) { if (this.props.selectedMessage !== prevProps.selectedMessage) { this.updateMessagePayload(); } } updateMessagePayload() { const { selectedMessage, connector } = this.props; getMessagePayload(selectedMessage.payload, connector.getLongString).then( async payload => { const { formattedData, formattedDataTitle } = await this.parsePayload( payload ); this.setState({ payload, isFormattedData: !!formattedData, formattedData, formattedDataTitle, }); } ); } async parsePayload(payload) { const { connector, selectedMessage, request } = this.props; // Don't apply formatting to control frames // Control frame check can be done using opCode as specified here: // https://tools.ietf.org/html/rfc6455 const controlFrames = [0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf]; const isControlFrame = controlFrames.includes(selectedMessage.opCode); if (isControlFrame) { return { formattedData: null, formattedDataTitle: "", }; } // Make sure that request headers are fetched from the backend before // looking for `Sec-WebSocket-Protocol` header. const responseHeaders = await connector.requestData( request.id, "responseHeaders" ); const wsProtocol = getResponseHeader( { responseHeaders }, "Sec-WebSocket-Protocol" ); const wampSerializer = wampSerializers[wsProtocol]; if (wampSerializer) { const wampPayload = wampSerializer.deserializeMessage(payload); return { formattedData: wampPayload, formattedDataTitle: wampSerializer.description, }; } // socket.io payload const socketIOPayload = this.parseSocketIOPayload(payload); if (socketIOPayload) { return { formattedData: socketIOPayload, formattedDataTitle: "Socket.IO", }; } // sockjs payload const sockJSPayload = parseSockJS(payload); if (sockJSPayload) { let formattedData = sockJSPayload.data; if (sockJSPayload.type === "message") { if (Array.isArray(formattedData)) { formattedData = formattedData.map( message => parseStompJs(message) || message ); } else { formattedData = parseStompJs(formattedData) || formattedData; } } return { formattedData, formattedDataTitle: "SockJS", }; } // signalr payload const signalRPayload = this.parseSignalR(payload); if (signalRPayload) { return { formattedData: signalRPayload, formattedDataTitle: "SignalR", }; } // STOMP const stompPayload = parseStompJs(payload); if (stompPayload) { return { formattedData: stompPayload, formattedDataTitle: "STOMP", }; } // json payload let { json } = parseJSON(payload); if (json) { const { data, identifier } = json; // A json payload MAY be an "Action cable" if it // contains either a `data` or an `identifier` property // which are also json strings and would need to be parsed. // See https://medium.com/codequest/actioncable-in-rails-api-f087b65c860d if ( (data && typeof data == "string") || (identifier && typeof identifier == "string") ) { const actionCablePayload = this.parseActionCable(json); return { formattedData: actionCablePayload, formattedDataTitle: "Action Cable", }; } if (Array.isArray(json)) { json = json.map(message => parseStompJs(message) || message); } return { formattedData: json, formattedDataTitle: "JSON", }; } return { formattedData: null, formattedDataTitle: "", }; } parseSocketIOPayload(payload) { let result; // Try decoding socket.io frames try { const decoder = new SocketIODecoder(); decoder.on("decoded", decodedPacket => { if ( decodedPacket && !decodedPacket.data.includes("parser error") && decodedPacket.type ) { result = decodedPacket; } }); decoder.add(payload); return result; } catch (err) { // Ignore errors } return null; } parseSignalR(payload) { // attempt to parse as HandshakeResponseMessage let decoder; try { decoder = new HandshakeProtocol(); const [remainingData, responseMessage] = decoder.parseHandshakeResponse(payload); if (responseMessage) { return { handshakeResponse: responseMessage, remainingData: this.parseSignalR(remainingData), }; } } catch (err) { // ignore errors; } // attempt to parse as JsonHubProtocolMessage try { decoder = new JsonHubProtocol(); const msgs = decoder.parseMessages(payload, null); if (msgs?.length) { return msgs; } } catch (err) { // ignore errors; } // MVP Signalr if (payload.endsWith("\u001e")) { const { json } = parseJSON(payload.slice(0, -1)); if (json) { return json; } } return null; } parseActionCable(payload) { const identifier = payload.identifier && parseJSON(payload.identifier).json; const data = payload.data && parseJSON(payload.data).json; if (identifier) { payload.identifier = identifier; } if (data) { payload.data = data; } return payload; } toggleRawData() { this.setState({ rawDataDisplayed: !this.state.rawDataDisplayed, }); } renderRawDataBtn(key, checked, onChange) { return [ label( { key: `${key}RawDataBtn`, className: "raw-data-toggle", htmlFor: `raw-${key}-checkbox`, onClick: event => { // stop the header click event event.stopPropagation(); }, }, span({ className: "raw-data-toggle-label" }, RAW_DATA), span( { className: "raw-data-toggle-input" }, input({ id: `raw-${key}-checkbox`, checked, className: "devtools-checkbox-toggle", onChange, type: "checkbox", }) ) ), ]; } renderData(component, componentProps) { return component(componentProps); } render() { let component; let componentProps; let dataLabel; let { payload, rawDataDisplayed } = this.state; let isTruncated = false; if (this.state.payload.length >= MESSAGE_DATA_LIMIT) { payload = payload.substring(0, MESSAGE_DATA_LIMIT); isTruncated = true; } if ( !isTruncated && this.state.isFormattedData && !this.state.rawDataDisplayed ) { component = PropertiesView; componentProps = { object: this.state.formattedData, }; dataLabel = this.state.formattedDataTitle; } else { component = RawData; componentProps = { payload }; dataLabel = L10N.getFormatStrWithNumbers( "netmonitor.ws.rawData.header", getFormattedSize(this.state.payload.length) ); } return div( { className: "message-payload", }, isTruncated && div( { className: "truncated-data-message", }, MESSAGE_DATA_TRUNCATED ), h2({ className: "data-header", role: "heading" }, [ span({ key: "data-label", className: "data-label" }, dataLabel), !isTruncated && this.state.isFormattedData && this.renderRawDataBtn("data", rawDataDisplayed, this.toggleRawData), ]), this.renderData(component, componentProps) ); } } module.exports = connect(state => ({ request: getRequestByChannelId(state, state.messages.currentChannelId), }))(MessagePayload);