diff options
Diffstat (limited to 'devtools/client/netmonitor/src/components/messages/MessagePayload.js')
-rw-r--r-- | devtools/client/netmonitor/src/components/messages/MessagePayload.js | 403 |
1 files changed, 403 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/messages/MessagePayload.js b/devtools/client/netmonitor/src/components/messages/MessagePayload.js new file mode 100644 index 0000000000..b8d3f7ae33 --- /dev/null +++ b/devtools/client/netmonitor/src/components/messages/MessagePayload.js @@ -0,0 +1,403 @@ +/* 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); |