tor-browser

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

HTTPCustomRequestPanel.js (14916B)


      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 asyncStorage = require("resource://devtools/shared/async-storage.js");
     12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     13 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     14 const {
     15  connect,
     16 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     17 const {
     18  L10N,
     19 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
     20 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
     21 const {
     22  getClickedRequest,
     23 } = require("resource://devtools/client/netmonitor/src/selectors/index.js");
     24 const {
     25  getUrlQuery,
     26  parseQueryString,
     27 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
     28 const InputMap = createFactory(
     29  require("resource://devtools/client/netmonitor/src/components/new-request/InputMap.js")
     30 );
     31 const { button, div, footer, label, textarea, select, option } = dom;
     32 
     33 const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.newRequestHeaders");
     34 const CUSTOM_NEW_REQUEST_URL_LABEL = L10N.getStr(
     35  "netmonitor.custom.newRequestUrlLabel"
     36 );
     37 const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postBody");
     38 const CUSTOM_POSTDATA_PLACEHOLDER = L10N.getStr(
     39  "netmonitor.custom.postBody.placeholder"
     40 );
     41 const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.urlParameters");
     42 const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
     43 const CUSTOM_CLEAR = L10N.getStr("netmonitor.custom.clear");
     44 
     45 const FIREFOX_DEFAULT_HEADERS = [
     46  "Accept-Charset",
     47  "Accept-Encoding",
     48  "Access-Control-Request-Headers",
     49  "Access-Control-Request-Method",
     50  "Connection",
     51  "Content-Length",
     52  "Cookie",
     53  "Cookie2",
     54  "Date",
     55  "DNT",
     56  "Expect",
     57  "Feature-Policy",
     58  "Host",
     59  "Keep-Alive",
     60  "Origin",
     61  "Proxy-",
     62  "Sec-",
     63  "Referer",
     64  "TE",
     65  "Trailer",
     66  "Transfer-Encoding",
     67  "Upgrade",
     68  "Via",
     69 ];
     70 // This does not include the CONNECT method as it is restricted and special.
     71 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1769572#c2 for details
     72 const HTTP_METHODS = [
     73  "GET",
     74  "HEAD",
     75  "POST",
     76  "DELETE",
     77  "PUT",
     78  "OPTIONS",
     79  "TRACE",
     80  "PATCH",
     81 ];
     82 
     83 /*
     84 * HTTP Custom request panel component
     85 * A network request panel which enables creating and sending new requests
     86 * or selecting, editing and re-sending current requests.
     87 */
     88 class HTTPCustomRequestPanel extends Component {
     89  static get propTypes() {
     90    return {
     91      connector: PropTypes.object.isRequired,
     92      request: PropTypes.object,
     93      sendCustomRequest: PropTypes.func.isRequired,
     94    };
     95  }
     96 
     97  constructor(props) {
     98    super(props);
     99 
    100    this.state = {
    101      method: HTTP_METHODS[0],
    102      url: "",
    103      urlQueryParams: [],
    104      headers: [],
    105      postBody: "",
    106      // Flag to know the data from either the request or the async storage has
    107      // been loaded in componentDidMount
    108      _isStateDataReady: false,
    109    };
    110 
    111    this.handleInputChange = this.handleInputChange.bind(this);
    112    this.handleChangeURL = this.handleChangeURL.bind(this);
    113    this.updateInputMapItem = this.updateInputMapItem.bind(this);
    114    this.addInputMapItem = this.addInputMapItem.bind(this);
    115    this.deleteInputMapItem = this.deleteInputMapItem.bind(this);
    116    this.checkInputMapItem = this.checkInputMapItem.bind(this);
    117    this.handleClear = this.handleClear.bind(this);
    118    this.createQueryParamsListFromURL =
    119      this.createQueryParamsListFromURL.bind(this);
    120    this.onUpdateQueryParams = this.onUpdateQueryParams.bind(this);
    121  }
    122 
    123  async componentDidMount() {
    124    let { connector, request } = this.props;
    125    if (!connector.currentTarget?.targetForm?.isPrivate) {
    126      const persistedCustomRequest = await asyncStorage.getItem(
    127        "devtools.netmonitor.customRequest"
    128      );
    129      request = request || persistedCustomRequest;
    130    }
    131 
    132    if (!request) {
    133      this.setState({ _isStateDataReady: true });
    134      return;
    135    }
    136 
    137    // We need this part because in the asyncStorage we are saving the request in one format
    138    // and from the edit and resend it comes in a different form with different properties,
    139    // so we need this to nomalize the request.
    140    if (request.requestHeaders) {
    141      request.headers = request.requestHeaders.headers;
    142    }
    143 
    144    if (request.requestPostData?.postData?.text) {
    145      request.postBody = request.requestPostData.postData.text;
    146    }
    147 
    148    const headers = request.headers
    149      .map(({ name, value }) => {
    150        return {
    151          name,
    152          value,
    153          checked: true,
    154          disabled: FIREFOX_DEFAULT_HEADERS.some(i => name.startsWith(i)),
    155        };
    156      })
    157      .sort((a, b) => {
    158        if (a.disabled && !b.disabled) {
    159          return -1;
    160        }
    161        if (!a.disabled && b.disabled) {
    162          return 1;
    163        }
    164        return 0;
    165      });
    166 
    167    if (request.requestPostDataAvailable && !request.postBody) {
    168      const requestData = await connector.requestData(
    169        request.id,
    170        "requestPostData"
    171      );
    172      request.postBody = requestData.postData.text;
    173    }
    174 
    175    this.setState({
    176      method: request.method,
    177      url: request.url,
    178      urlQueryParams: this.createQueryParamsListFromURL(request.url),
    179      headers,
    180      postBody: request.postBody,
    181      _isStateDataReady: true,
    182    });
    183  }
    184 
    185  componentDidUpdate(prevProps, prevState) {
    186    // This is when the query params change in the url params input map
    187    if (
    188      prevState.urlQueryParams !== this.state.urlQueryParams &&
    189      prevState.url === this.state.url
    190    ) {
    191      this.onUpdateQueryParams();
    192    }
    193  }
    194 
    195  componentWillUnmount() {
    196    if (!this.props.connector.currentTarget?.targetForm?.isPrivate) {
    197      asyncStorage.setItem("devtools.netmonitor.customRequest", this.state);
    198    }
    199  }
    200 
    201  handleChangeURL(event) {
    202    const { value } = event.target;
    203 
    204    this.setState({
    205      url: value,
    206      urlQueryParams: this.createQueryParamsListFromURL(value),
    207    });
    208  }
    209 
    210  handleInputChange(event) {
    211    const { name, value } = event.target;
    212    const newState = {
    213      [name]: value,
    214    };
    215 
    216    // If the message body changes lets make sure we
    217    // keep the content-length up to date.
    218    if (name == "postBody") {
    219      newState.headers = this.state.headers.map(header => {
    220        if (header.name == "Content-Length") {
    221          header.value = value.length;
    222        }
    223        return header;
    224      });
    225    }
    226 
    227    this.setState(newState);
    228  }
    229 
    230  updateInputMapItem(stateName, event) {
    231    const { name, value } = event.target;
    232    const [prop, index] = name.split("-");
    233    const updatedList = [...this.state[stateName]];
    234    updatedList[Number(index)][prop] = value;
    235 
    236    this.setState({
    237      [stateName]: updatedList,
    238    });
    239  }
    240 
    241  addInputMapItem(stateName, name, value) {
    242    this.setState({
    243      [stateName]: [
    244        ...this.state[stateName],
    245        { name, value, checked: true, disabled: false },
    246      ],
    247    });
    248  }
    249 
    250  deleteInputMapItem(stateName, index) {
    251    this.setState({
    252      [stateName]: this.state[stateName].filter((_, i) => i !== index),
    253    });
    254  }
    255 
    256  checkInputMapItem(stateName, index, checked) {
    257    this.setState({
    258      [stateName]: this.state[stateName].map((item, i) => {
    259        if (index === i) {
    260          return {
    261            ...item,
    262            checked,
    263          };
    264        }
    265        return item;
    266      }),
    267    });
    268  }
    269 
    270  onUpdateQueryParams() {
    271    const { urlQueryParams, url } = this.state;
    272 
    273    const searchParams = new URLSearchParams();
    274    for (const { name, value, checked } of urlQueryParams) {
    275      // We only want to add checked parameters with a non-empty name
    276      if (checked && name) {
    277        searchParams.append(name, value);
    278      }
    279    }
    280 
    281    // We can't use `getUrl` here as `url` is the value of the input and might not be a
    282    // valid URL. We still want to try to set the params in the URL input in such case,
    283    // so append them after the first "?" char we find.
    284    let finalURL = url.split("?")[0];
    285    if (searchParams.size) {
    286      finalURL += `?${searchParams}`;
    287    }
    288 
    289    this.setState({
    290      url: finalURL,
    291    });
    292  }
    293 
    294  createQueryParamsListFromURL(url = "") {
    295    const parsedQuery = parseQueryString(getUrlQuery(url) || url.split("?")[1]);
    296    const queryArray = parsedQuery || [];
    297    return queryArray.map(({ name, value }) => {
    298      return {
    299        checked: true,
    300        name,
    301        value,
    302      };
    303    });
    304  }
    305 
    306  handleClear() {
    307    this.setState({
    308      method: HTTP_METHODS[0],
    309      url: "",
    310      urlQueryParams: [],
    311      headers: [],
    312      postBody: "",
    313    });
    314  }
    315 
    316  render() {
    317    return div(
    318      { className: "http-custom-request-panel" },
    319      div(
    320        { className: "http-custom-request-panel-content" },
    321        div(
    322          {
    323            className: "tabpanel-summary-container http-custom-method-and-url",
    324            id: "http-custom-method-and-url",
    325          },
    326          select(
    327            {
    328              className: "http-custom-method-value",
    329              id: "http-custom-method-value",
    330              name: "method",
    331              onChange: this.handleInputChange,
    332              onBlur: this.handleInputChange,
    333              value: this.state.method,
    334            },
    335 
    336            HTTP_METHODS.map(item =>
    337              option(
    338                {
    339                  value: item,
    340                  key: item,
    341                },
    342                item
    343              )
    344            )
    345          ),
    346          div(
    347            {
    348              className: "auto-growing-textarea",
    349              "data-replicated-value": this.state.url,
    350              title: this.state.url,
    351            },
    352            textarea({
    353              className: "http-custom-url-value",
    354              id: "http-custom-url-value",
    355              name: "url",
    356              placeholder: CUSTOM_NEW_REQUEST_URL_LABEL,
    357              onChange: event => {
    358                this.handleChangeURL(event);
    359              },
    360              onBlur: this.handleTextareaChange,
    361              value: this.state.url,
    362              rows: 1,
    363            })
    364          )
    365        ),
    366        div(
    367          {
    368            className: "tabpanel-summary-container http-custom-section",
    369            id: "http-custom-query",
    370          },
    371          label(
    372            {
    373              className: "http-custom-request-label",
    374              htmlFor: "http-custom-query-value",
    375            },
    376            CUSTOM_QUERY
    377          ),
    378          // This is the input map for the Url Parameters Component
    379          InputMap({
    380            list: this.state.urlQueryParams,
    381            onUpdate: event => {
    382              this.updateInputMapItem(
    383                "urlQueryParams",
    384                event,
    385                this.onUpdateQueryParams
    386              );
    387            },
    388            onAdd: (name, value) =>
    389              this.addInputMapItem(
    390                "urlQueryParams",
    391                name,
    392                value,
    393                this.onUpdateQueryParams
    394              ),
    395            onDelete: index =>
    396              this.deleteInputMapItem(
    397                "urlQueryParams",
    398                index,
    399                this.onUpdateQueryParams
    400              ),
    401            onChecked: (index, checked) => {
    402              this.checkInputMapItem(
    403                "urlQueryParams",
    404                index,
    405                checked,
    406                this.onUpdateQueryParams
    407              );
    408            },
    409          })
    410        ),
    411        div(
    412          {
    413            id: "http-custom-headers",
    414            className: "tabpanel-summary-container http-custom-section",
    415          },
    416          label(
    417            {
    418              className: "http-custom-request-label",
    419              htmlFor: "custom-headers-value",
    420            },
    421            CUSTOM_HEADERS
    422          ),
    423          // This is the input map for the Headers Component
    424          InputMap({
    425            ref: this.headersListRef,
    426            list: this.state.headers,
    427            onUpdate: event => {
    428              this.updateInputMapItem("headers", event);
    429            },
    430            onAdd: (name, value) =>
    431              this.addInputMapItem("headers", name, value),
    432            onDelete: index => this.deleteInputMapItem("headers", index),
    433            onChecked: (index, checked) => {
    434              this.checkInputMapItem("headers", index, checked);
    435            },
    436          })
    437        ),
    438        div(
    439          {
    440            id: "http-custom-postdata",
    441            className: "tabpanel-summary-container http-custom-section",
    442          },
    443          label(
    444            {
    445              className: "http-custom-request-label",
    446              htmlFor: "http-custom-postdata-value",
    447            },
    448            CUSTOM_POSTDATA
    449          ),
    450          textarea({
    451            className: "tabpanel-summary-input",
    452            id: "http-custom-postdata-value",
    453            name: "postBody",
    454            placeholder: CUSTOM_POSTDATA_PLACEHOLDER,
    455            onChange: this.handleInputChange,
    456            rows: 6,
    457            value: this.state.postBody,
    458            wrap: "off",
    459          })
    460        )
    461      ),
    462      footer(
    463        { className: "http-custom-request-button-container" },
    464        button(
    465          {
    466            className: "devtools-button",
    467            id: "http-custom-request-clear-button",
    468            onClick: this.handleClear,
    469          },
    470          CUSTOM_CLEAR
    471        ),
    472        button(
    473          {
    474            className: "devtools-button",
    475            id: "http-custom-request-send-button",
    476            disabled:
    477              !this.state._isStateDataReady ||
    478              !this.state.url ||
    479              !this.state.method,
    480            onClick: () => {
    481              const newRequest = {
    482                method: this.state.method,
    483                url: this.state.url,
    484                cause: this.props.request?.cause,
    485                urlQueryParams: this.state.urlQueryParams.map(
    486                  ({ ...params }) => params
    487                ),
    488                requestHeaders: {
    489                  headers: this.state.headers
    490                    .filter(({ checked }) => checked)
    491                    .map(({ ...headersValues }) => headersValues),
    492                },
    493              };
    494 
    495              if (this.state.postBody) {
    496                newRequest.requestPostData = {
    497                  postData: {
    498                    text: this.state.postBody,
    499                  },
    500                };
    501              }
    502              this.props.sendCustomRequest(newRequest);
    503            },
    504          },
    505          CUSTOM_SEND
    506        )
    507      )
    508    );
    509  }
    510 }
    511 
    512 module.exports = connect(
    513  state => ({ request: getClickedRequest(state) }),
    514  dispatch => ({
    515    sendCustomRequest: request =>
    516      dispatch(Actions.sendHTTPCustomRequest(request)),
    517  })
    518 )(HTTPCustomRequestPanel);