Source code

Revision control

Copy as Markdown

Other Tools

/* 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:
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"
);
if (!responseHeaders.headers) {
// If the network event actor was destroyed while retrieving the request
// data, no headers will be available.
return {
formattedData: null,
formattedDataTitle: "",
};
}
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.
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);