tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);