tor-browser

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

request-utils.js (25945B)


      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  getUnicodeUrl,
      9  getUnicodeUrlPath,
     10  getUnicodeHostname,
     11 } = require("resource://devtools/client/shared/unicode-url.js");
     12 
     13 const lazy = {};
     14 ChromeUtils.defineESModuleGetters(
     15  lazy,
     16  {
     17    parseJsonLossless:
     18      "resource://devtools/client/shared/components/reps/reps/rep-utils.mjs",
     19    JSON_NUMBER:
     20      "resource://devtools/client/shared/components/reps/reps/constants.mjs",
     21  },
     22  { global: "contextual" }
     23 );
     24 
     25 loader.lazyRequireGetter(
     26  this,
     27  "L10N",
     28  "resource://devtools/client/netmonitor/src/utils/l10n.js",
     29  true
     30 );
     31 
     32 const {
     33  UPDATE_PROPS,
     34 } = require("resource://devtools/client/netmonitor/src/constants.js");
     35 
     36 const CONTENT_MIME_TYPE_ABBREVIATIONS = new Map([
     37  ["ecmascript", "js"],
     38  ["javascript", "js"],
     39  ["x-javascript", "js"],
     40  ["event-stream", "eventsource"],
     41 ]);
     42 
     43 /**
     44 * Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
     45 * POST request.
     46 *
     47 * @param {object} headers - the "requestHeaders".
     48 * @param {object} uploadHeaders - the "requestHeadersFromUploadStream".
     49 * @param {object} postData - the "requestPostData".
     50 * @return {Array} a promise list that is resolved with the extracted form data.
     51 */
     52 async function getFormDataSections(
     53  headers,
     54  uploadHeaders,
     55  postData,
     56  getLongString
     57 ) {
     58  const formDataSections = [];
     59 
     60  const requestHeaders = headers.headers;
     61  const payloadHeaders = uploadHeaders ? uploadHeaders.headers : [];
     62  const allHeaders = [...payloadHeaders, ...requestHeaders];
     63 
     64  const contentTypeHeader = allHeaders.find(e => {
     65    return e.name.toLowerCase() == "content-type";
     66  });
     67 
     68  const contentTypeLongString = contentTypeHeader
     69    ? contentTypeHeader.value
     70    : "";
     71 
     72  const contentType = await getLongString(contentTypeLongString);
     73 
     74  if (contentType && contentType.includes("x-www-form-urlencoded")) {
     75    const postDataLongString = postData.postData.text;
     76    const text = await getLongString(postDataLongString);
     77 
     78    for (const section of text.trim().split(/\r\n|\r|\n/)) {
     79      // Before displaying it, make sure this section of the POST data
     80      // isn't a line containing upload stream headers.
     81      if (payloadHeaders.every(header => !section.startsWith(header.name))) {
     82        formDataSections.push(section);
     83      }
     84    }
     85  }
     86 
     87  return formDataSections;
     88 }
     89 
     90 /**
     91 * Fetch headers full content from actor server
     92 *
     93 * @param {object} headers - a object presents headers data
     94 * @return {object} a headers object with updated content payload
     95 */
     96 async function fetchHeaders(headers, getLongString) {
     97  for (const { value } of headers.headers) {
     98    headers.headers.value = await getLongString(value);
     99  }
    100  return headers;
    101 }
    102 
    103 /**
    104 * Fetch network event update packets from actor server
    105 * Expect to fetch a couple of network update packets from a given request.
    106 *
    107 * @param {function} requestData - requestData function for lazily fetch data
    108 * @param {object} request - request object
    109 * @param {Array} updateTypes - a list of network event update types
    110 */
    111 function fetchNetworkUpdatePacket(requestData, request, updateTypes) {
    112  const promises = [];
    113  if (request) {
    114    updateTypes.forEach(updateType => {
    115      // stackTrace needs to be handled specially as the property to lookup
    116      // on the request object follows a slightly different convention.
    117      // i.e `stacktrace` not `stackTrace`
    118      if (updateType === "stackTrace") {
    119        if (request.cause.stacktraceAvailable && !request.stacktrace) {
    120          promises.push(requestData(request.id, updateType));
    121        }
    122        return;
    123      }
    124      // responseContent only checks the availiability flag as there can
    125      // be multiple response content events
    126      if (updateType === "responseContent") {
    127        if (request.responseContentAvailable) {
    128          promises.push(requestData(request.id, updateType));
    129        }
    130        return;
    131      }
    132 
    133      if (request[`${updateType}Available`] && !request[updateType]) {
    134        promises.push(requestData(request.id, updateType));
    135      }
    136    });
    137  }
    138 
    139  return Promise.all(promises);
    140 }
    141 
    142 /**
    143 * Form a data: URI given a mime type, encoding, and some text.
    144 *
    145 * @param {string} mimeType - mime type
    146 * @param {string} encoding - encoding to use; if not set, the
    147 *                            text will be base64-encoded.
    148 * @param {string} text - text of the URI.
    149 * @return {string} a data URI
    150 */
    151 function formDataURI(mimeType, encoding, text) {
    152  if (!encoding) {
    153    encoding = "base64";
    154    text = btoa(unescape(encodeURIComponent(text)));
    155  }
    156  return "data:" + mimeType + ";" + encoding + "," + text;
    157 }
    158 
    159 /**
    160 * Write out a list of headers into a chunk of text
    161 *
    162 * @param {Array} headers - array of headers info { name, value }
    163 * @param {string} preHeaderText - first line of the headers request/response
    164 * @return {string} list of headers in text format
    165 */
    166 function writeHeaderText(headers, preHeaderText) {
    167  let result = "";
    168  if (preHeaderText) {
    169    result += preHeaderText + "\r\n";
    170  }
    171  result += headers.map(({ name, value }) => name + ": " + value).join("\r\n");
    172  result += "\r\n\r\n";
    173  return result;
    174 }
    175 
    176 /**
    177 * Decode base64 string.
    178 *
    179 * @param {string} url - a string
    180 * @return {string} decoded string
    181 */
    182 function decodeUnicodeBase64(string) {
    183  try {
    184    return decodeURIComponent(atob(string));
    185  } catch (err) {
    186    // Ignore error and return input string directly.
    187  }
    188  return string;
    189 }
    190 
    191 /**
    192 * Helper for getting an abbreviated string for a mime type.
    193 *
    194 * @param {string} mimeType - mime type
    195 * @return {string} abbreviated mime type
    196 */
    197 function getAbbreviatedMimeType(mimeType) {
    198  if (!mimeType) {
    199    return "";
    200  }
    201  const abbrevType = (
    202    mimeType.toLowerCase().split(";")[0].split("/")[1] || ""
    203  ).split("+")[0];
    204  return CONTENT_MIME_TYPE_ABBREVIATIONS.get(abbrevType) || abbrevType;
    205 }
    206 
    207 /**
    208 * Helpers for getting a filename from a mime type.
    209 *
    210 * @param {string} baseNameWithQuery - unicode basename and query of a url
    211 * @return {string} unicode filename portion of a url
    212 */
    213 function getFileName(baseNameWithQuery) {
    214  const basename = baseNameWithQuery && baseNameWithQuery.split("?")[0];
    215  return basename && basename.includes(".") ? basename : null;
    216 }
    217 
    218 /**
    219 * Helpers for retrieving a URL object from a string
    220 *
    221 * @param {string|URL} url - unvalidated url string or already a URL object
    222 * @return {URL} The URL object
    223 */
    224 function getUrl(url) {
    225  if (URL.isInstance(url)) {
    226    return url;
    227  }
    228  return URL.parse(url);
    229 }
    230 
    231 /**
    232 * Helpers for retrieving the value of a URL object property
    233 *
    234 * @param {string|URL} input - unvalidated url string or URL instance
    235 * @param {string} string - desired property in the URL object
    236 * @return {string} unicode query of a url
    237 */
    238 function getUrlProperty(input, property) {
    239  const url = getUrl(input);
    240  return url?.[property] ?? "";
    241 }
    242 
    243 /**
    244 * Helpers for getting the last portion of a url.
    245 * For example helper returns "basename" from http://domain.com/path/basename
    246 * If basename portion is empty, it returns the url pathname.
    247 *
    248 * @param {string|URL} url - unvalidated url string or URL instance
    249 * @return {string} unicode basename of a url
    250 */
    251 function getUrlBaseName(url) {
    252  const pathname = getUrlProperty(url, "pathname");
    253  return getUnicodeUrlPath(pathname.replace(/\S*\//, "") || pathname || "/");
    254 }
    255 
    256 /**
    257 * Helpers for getting the query portion of a url.
    258 *
    259 * @param {string|URL} url - unvalidated url string or URL instance
    260 * @return {string} unicode query of a url
    261 */
    262 function getUrlQuery(url) {
    263  return getUrlProperty(url, "search").replace(/^\?/, "");
    264 }
    265 
    266 /**
    267 * Helpers for getting unicode name and query portions of a url.
    268 *
    269 * @param {URL} urlObject - the URL instance
    270 * @return {string} unicode basename and query portions of a url
    271 */
    272 function getUrlBaseNameWithQuery(urlObject) {
    273  if (urlObject.href.startsWith("data:")) {
    274    // For data URIs, no basename can be extracted from the URL so just reuse
    275    // the full url.
    276    return urlObject.href;
    277  }
    278 
    279  const basename = getUrlBaseName(urlObject);
    280  const search = getUrlProperty(urlObject, "search");
    281  return basename + getUnicodeUrlPath(search);
    282 }
    283 
    284 /**
    285 * Helpers for getting hostname portion of an URL.
    286 *
    287 * @param {string|URL} url - unvalidated url string or URL instance
    288 * @return {string} unicode hostname of a url
    289 */
    290 function getUrlHostName(url) {
    291  return getUrlProperty(url, "hostname");
    292 }
    293 
    294 /**
    295 * Helpers for getting host portion of an URL.
    296 *
    297 * @param {string|URL} url - unvalidated url string or URL instance
    298 * @return {string} unicode host of a url
    299 */
    300 function getUrlHost(url) {
    301  return getUrlProperty(url, "host");
    302 }
    303 
    304 /**
    305 * Helpers for getting the shceme portion of a url.
    306 * For example helper returns "http" from http://domain.com/path/basename
    307 *
    308 * @param {string|URL} url - unvalidated url string or URL instance
    309 * @return {string} string scheme of a url
    310 */
    311 function getUrlScheme(url) {
    312  const protocol = getUrlProperty(url, "protocol");
    313  return protocol.replace(":", "").toLowerCase();
    314 }
    315 
    316 /**
    317 * Helpers for getting the full path portion of a url.
    318 *
    319 * @param {string|URL} url - unvalidated url string or URL instance
    320 * @return {string} string path of a url
    321 */
    322 function getUrlPath(url) {
    323  const href = getUrlProperty(url, "href");
    324  const origin = getUrlProperty(url, "origin");
    325  return href.replace(origin, "");
    326 }
    327 
    328 /**
    329 * Extract several details fields from a URL at once.
    330 */
    331 function getUrlDetails(url) {
    332  const urlObject = getUrl(url);
    333  const baseNameWithQuery = getUrlBaseNameWithQuery(urlObject);
    334  let host = getUrlHost(urlObject);
    335  const hostname = getUrlHostName(urlObject);
    336  const unicodeUrl = getUnicodeUrl(urlObject);
    337  const scheme = getUrlScheme(urlObject);
    338  const path = getUrlPath(urlObject);
    339 
    340  // If the hostname contains unreadable ASCII characters, we need to do the
    341  // following two steps:
    342  // 1. Converting the unreadable hostname to a readable Unicode domain name.
    343  //    For example, converting xn--g6w.xn--8pv into a Unicode domain name.
    344  // 2. Replacing the unreadable hostname portion in the `host` with the
    345  //    readable hostname.
    346  //    For example, replacing xn--g6w.xn--8pv:8000 with [Unicode domain]:8000
    347  // After finishing the two steps, we get a readable `host`.
    348  const unicodeHostname = getUnicodeHostname(hostname);
    349  if (unicodeHostname !== hostname) {
    350    host = host.replace(hostname, unicodeHostname);
    351  }
    352 
    353  // Mark local hosts specially, where "local" is  as defined in the W3C
    354  // spec for secure contexts.
    355  // http://www.w3.org/TR/powerful-features/
    356  //
    357  //  * If the name falls under 'localhost'
    358  //  * If the name is an IPv4 address within 127.0.0.0/8
    359  //  * If the name is an IPv6 address within ::1/128
    360  //
    361  // IPv6 parsing is a little sloppy; it assumes that the address has
    362  // been validated before it gets here.
    363  const isLocal =
    364    /^(.+\.)?localhost$/.test(hostname) ||
    365    /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) ||
    366    /^\[[0:]+1\]$/.test(hostname);
    367 
    368  return {
    369    baseNameWithQuery,
    370    host,
    371    scheme,
    372    unicodeUrl,
    373    isLocal,
    374    url,
    375    path,
    376  };
    377 }
    378 
    379 /**
    380 * Helpers for retrieving the value of a URL tooltip
    381 *
    382 * @param {object} urlDetails - a urlDetails object
    383 * @returns
    384 */
    385 function getUrlToolTip(urlDetails) {
    386  const url = urlDetails.url;
    387  const decodedURL = urlDetails.unicodeUrl;
    388 
    389  // The `originalFileURL` below refers to "File" because it was initially created for use in the File column.
    390  // Now it is also being used in the Path and URL columns, while retaining the original name.
    391  const ORIGINAL_URL = L10N.getFormatStr(
    392    "netRequest.originalFileURL.tooltip",
    393    url
    394  );
    395  const DECODED_URL = L10N.getFormatStr(
    396    "netRequest.decodedFileURL.tooltip",
    397    decodedURL
    398  );
    399  const toolTip =
    400    url === decodedURL ? url : ORIGINAL_URL + "\n\n" + DECODED_URL;
    401  return toolTip;
    402 }
    403 
    404 /**
    405 * Parse a url's query string into its components
    406 *
    407 * @param {string} query - query string of a url portion
    408 * @return {Array} array of query params { name, value }
    409 */
    410 function parseQueryString(query) {
    411  if (!query) {
    412    return null;
    413  }
    414  return query
    415    .replace(/^[?&]/, "")
    416    .split("&")
    417    .map(e => {
    418      const param = e.split("=");
    419      return {
    420        name: param[0] ? getUnicodeUrlPath(param[0].replace(/\+/g, " ")) : "",
    421        value: param[1]
    422          ? getUnicodeUrlPath(param.slice(1).join("=").replace(/\+/g, " "))
    423          : "",
    424      };
    425    });
    426 }
    427 
    428 /**
    429 * Parse a string of formdata sections into its components
    430 *
    431 * @param {string} sections - sections of formdata joined by &
    432 * @return {Array} array of formdata params { name, value }
    433 */
    434 function parseFormData(sections) {
    435  if (!sections) {
    436    return [];
    437  }
    438 
    439  return sections
    440    .replace(/^&/, "")
    441    .split("&")
    442    .map(e => {
    443      const firstEqualSignIndex = e.indexOf("=");
    444      const paramName =
    445        firstEqualSignIndex !== -1 ? e.slice(0, firstEqualSignIndex) : e;
    446      const paramValue =
    447        firstEqualSignIndex !== -1 ? e.slice(firstEqualSignIndex + 1) : "";
    448      return {
    449        name: paramName ? getUnicodeUrlPath(paramName) : "",
    450        value: paramValue ? getUnicodeUrlPath(paramValue) : "",
    451      };
    452    });
    453 }
    454 
    455 /**
    456 * Reduces an IP address into a number for easier sorting
    457 *
    458 * @param {string} ip - IP address to reduce
    459 * @return {number} the number representing the IP address
    460 */
    461 function ipToLong(ip) {
    462  if (!ip) {
    463    // Invalid IP
    464    return -1;
    465  }
    466 
    467  let base;
    468  let octets = ip.split(".");
    469 
    470  if (octets.length === 4) {
    471    // IPv4
    472    base = 10;
    473  } else if (ip.includes(":")) {
    474    // IPv6
    475    const numberOfZeroSections =
    476      8 - ip.replace(/^:+|:+$/g, "").split(/:+/g).length;
    477    octets = ip
    478      .replace("::", `:${"0:".repeat(numberOfZeroSections)}`)
    479      .replace(/^:|:$/g, "")
    480      .split(":");
    481    base = 16;
    482  } else {
    483    // Invalid IP
    484    return -1;
    485  }
    486  return octets
    487    .map((val, ix, arr) => {
    488      return parseInt(val, base) * Math.pow(256, arr.length - 1 - ix);
    489    })
    490    .reduce((sum, val) => {
    491      return sum + val;
    492    }, 0);
    493 }
    494 
    495 /**
    496 * Compare two objects on a subset of their properties
    497 */
    498 function propertiesEqual(props, item1, item2) {
    499  return item1 === item2 || props.every(p => item1[p] === item2[p]);
    500 }
    501 
    502 /**
    503 * Calculate the start time of a request, which is the time from start
    504 * of 1st request until the start of this request.
    505 *
    506 * Without a firstRequestStartedMs argument the wrong time will be returned.
    507 * However, it can be omitted when comparing two start times and neither supplies
    508 * a firstRequestStartedMs.
    509 */
    510 function getStartTime(item, firstRequestStartedMs = 0) {
    511  return item.startedMs - firstRequestStartedMs;
    512 }
    513 
    514 /**
    515 * Calculate the end time of a request, which is the time from start
    516 * of 1st request until the end of this response.
    517 *
    518 * Without a firstRequestStartedMs argument the wrong time will be returned.
    519 * However, it can be omitted when comparing two end times and neither supplies
    520 * a firstRequestStartedMs.
    521 */
    522 function getEndTime(item, firstRequestStartedMs = 0) {
    523  const { startedMs, totalTime } = item;
    524  return startedMs + totalTime - firstRequestStartedMs;
    525 }
    526 
    527 /**
    528 * Calculate the response time of a request, which is the time from start
    529 * of 1st request until the beginning of download of this response.
    530 *
    531 * Without a firstRequestStartedMs argument the wrong time will be returned.
    532 * However, it can be omitted when comparing two response times and neither supplies
    533 * a firstRequestStartedMs.
    534 */
    535 function getResponseTime(item, firstRequestStartedMs = 0) {
    536  const { startedMs, totalTime, eventTimings = { timings: {} } } = item;
    537  return (
    538    startedMs + totalTime - firstRequestStartedMs - eventTimings.timings.receive
    539  );
    540 }
    541 
    542 /**
    543 * Format the protocols used by the request.
    544 */
    545 function getFormattedProtocol(item) {
    546  const { httpVersion = "", responseHeaders = { headers: [] } } = item;
    547  const protocol = [httpVersion];
    548  responseHeaders.headers.some(h => {
    549    if (h.hasOwnProperty("name") && h.name.toLowerCase() === "x-firefox-spdy") {
    550      /**
    551       * First we make sure h.value is defined and not an empty string.
    552       * Then check that HTTP version and x-firefox-spdy == "http/1.1".
    553       * If not, check that HTTP version and x-firefox-spdy have the same
    554       * numeric value when of the forms "http/<x>" and "h<x>" respectively.
    555       * If not, will push to protocol the non-standard x-firefox-spdy value.
    556       *
    557       * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1501357
    558       */
    559      if (h.value !== undefined && h.value.length) {
    560        if (
    561          h.value.toLowerCase() !== "http/1.1" ||
    562          protocol[0].toLowerCase() !== "http/1.1"
    563        ) {
    564          if (
    565            parseFloat(h.value.toLowerCase().split("")[1]) !==
    566            parseFloat(protocol[0].toLowerCase().split("/")[1])
    567          ) {
    568            protocol.push(h.value);
    569            return true;
    570          }
    571        }
    572      }
    573    }
    574    return false;
    575  });
    576  return protocol.join("+");
    577 }
    578 
    579 /**
    580 * Get the value of a particular response header, or null if not
    581 * present.
    582 */
    583 function getResponseHeader(item, header) {
    584  const { responseHeaders } = item;
    585  if (!responseHeaders?.headers?.length) {
    586    return null;
    587  }
    588  header = header.toLowerCase();
    589  for (const responseHeader of responseHeaders.headers) {
    590    if (responseHeader.name.toLowerCase() == header) {
    591      return responseHeader.value;
    592    }
    593  }
    594  return null;
    595 }
    596 
    597 /**
    598 * Get the value of a particular request header, or null if not
    599 * present.
    600 */
    601 function getRequestHeader(item, header) {
    602  const { requestHeaders } = item;
    603  if (!requestHeaders?.headers?.length) {
    604    return null;
    605  }
    606  header = header.toLowerCase();
    607  for (const requestHeader of requestHeaders.headers) {
    608    if (requestHeader.name.toLowerCase() == header) {
    609      return requestHeader.value;
    610    }
    611  }
    612  return null;
    613 }
    614 
    615 /**
    616 * Extracts any urlencoded form data sections from a POST request.
    617 */
    618 async function updateFormDataSections(props) {
    619  const { connector, request = {}, updateRequest } = props;
    620  let {
    621    id,
    622    formDataSections,
    623    requestHeaders,
    624    requestHeadersAvailable,
    625    requestHeadersFromUploadStream,
    626    requestPostData,
    627    requestPostDataAvailable,
    628  } = request;
    629 
    630  if (requestHeadersAvailable && !requestHeaders) {
    631    requestHeaders = await connector.requestData(id, "requestHeaders");
    632  }
    633 
    634  if (requestPostDataAvailable && !requestPostData) {
    635    requestPostData = await connector.requestData(id, "requestPostData");
    636  }
    637 
    638  if (
    639    !formDataSections &&
    640    requestHeaders &&
    641    requestPostData &&
    642    requestHeadersFromUploadStream
    643  ) {
    644    formDataSections = await getFormDataSections(
    645      requestHeaders,
    646      requestHeadersFromUploadStream,
    647      requestPostData,
    648      connector.getLongString
    649    );
    650 
    651    updateRequest(request.id, { formDataSections }, true);
    652  }
    653 }
    654 
    655 /**
    656 * This helper function helps to resolve the full payload of a message
    657 * that is wrapped in a LongStringActor object.
    658 */
    659 async function getMessagePayload(payload, getLongString) {
    660  const result = await getLongString(payload);
    661  return result;
    662 }
    663 
    664 /**
    665 * This helper function is used for additional processing of
    666 * incoming network update packets. It makes sure the only valid
    667 * update properties and the values are correct.
    668 * It's used by Network and Console panel reducers.
    669 *
    670 * @param {object} update
    671 *        The new update payload
    672 * @param {object} request
    673 *        The current request in the state
    674 */
    675 function processNetworkUpdates(update) {
    676  const newRequest = {};
    677  for (const [key, value] of Object.entries(update)) {
    678    if (UPDATE_PROPS.includes(key)) {
    679      newRequest[key] = value;
    680      if (key == "requestPostData") {
    681        newRequest.requestHeadersFromUploadStream = value.uploadHeaders;
    682      }
    683    }
    684  }
    685  return newRequest;
    686 }
    687 
    688 /**
    689 * This method checks that the response is base64 encoded by
    690 * comparing these 2 values:
    691 * 1. The original response
    692 * 2. The value of doing a base64 decode on the
    693 * response and then base64 encoding the result.
    694 * If the values are different or an error is thrown,
    695 * the method will return false.
    696 */
    697 function isBase64(payload) {
    698  try {
    699    return btoa(atob(payload)) == payload;
    700  } catch (err) {
    701    return false;
    702  }
    703 }
    704 
    705 /**
    706 * Checks if the payload is of JSON type.
    707 * This function also handles JSON with XSSI-escaping characters by stripping them
    708 * and returning the stripped chars in the strippedChars property
    709 * This function also handles Base64 encoded JSON.
    710 *
    711 * @returns {object} shape:
    712 *  {Object} json: parsed JSON object
    713 *  {Error} error: JSON parsing error
    714 *  {string} strippedChars: XSSI stripped chars removed from JSON payload
    715 */
    716 function parseJSON(payloadUnclean) {
    717  let json;
    718  const jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
    719  const [, jsonpCallback, jsonp] = payloadUnclean.match(jsonpRegex) || [];
    720  if (jsonpCallback && jsonp) {
    721    let error;
    722    try {
    723      json = parseJSON(jsonp).json;
    724    } catch (err) {
    725      error = err;
    726    }
    727    return { json, error, jsonpCallback };
    728  }
    729 
    730  let { payload, strippedChars, error } = removeXSSIString(payloadUnclean);
    731 
    732  try {
    733    json = lazy.parseJsonLossless(payload);
    734  } catch (err) {
    735    if (isBase64(payload)) {
    736      try {
    737        json = JSON.parse(atob(payload));
    738      } catch (err64) {
    739        error = err64;
    740      }
    741    } else {
    742      error = err;
    743    }
    744  }
    745 
    746  // Do not present JSON primitives (e.g. boolean, strings in quotes, numbers)
    747  // as JSON expandable tree.
    748  if (
    749    !error &&
    750    (typeof json !== "object" ||
    751      json === null ||
    752      // Parsed JSON numbers might be different than the source, for example
    753      // JSON.parse("1516340399466235648") returns 1516340399466235600. In such case,
    754      // parseJsonLossless will return an object with `type: JSON_NUMBER` property.
    755      // We still want to display those numbers as the other numbers here.
    756      json?.type === lazy.JSON_NUMBER)
    757  ) {
    758    return {};
    759  }
    760 
    761  return {
    762    json,
    763    error,
    764    strippedChars,
    765  };
    766 }
    767 
    768 /**
    769 * Removes XSSI prevention sequences from JSON payloads
    770 *
    771 * @param {string} payloadUnclean: JSON payload that may or may have a
    772 *                                 XSSI prevention sequence
    773 * @returns {object} Shape:
    774 *   {string} payload: the JSON witht the XSSI prevention sequence removed
    775 *   {string} strippedChars: XSSI string that was removed, null if no XSSI
    776 *                           prevention sequence was found
    777 *   {Error} error: error attempting to strip XSSI prevention sequence
    778 */
    779 function removeXSSIString(payloadUnclean) {
    780  // Regex that finds the XSSI protection sequences )]}'\n for(;;); and while(1);
    781  const xssiRegex = /(^\)\]\}',?\n)|(^for ?\(;;\);?)|(^while ?\(1\);?)/;
    782  let payload, strippedChars, error;
    783  const xssiRegexMatch = payloadUnclean.match(xssiRegex);
    784 
    785  // Remove XSSI string if there was one found
    786  if (xssiRegexMatch?.length) {
    787    const xssiLen = xssiRegexMatch[0].length;
    788    try {
    789      // substring the payload by the length of the XSSI match to remove it
    790      // and save the match to report
    791      payload = payloadUnclean.substring(xssiLen);
    792      strippedChars = xssiRegexMatch[0];
    793    } catch (err) {
    794      error = err;
    795      payload = payloadUnclean;
    796    }
    797  } else {
    798    // if there was no XSSI match just return the raw payload
    799    payload = payloadUnclean;
    800  }
    801  return {
    802    payload,
    803    strippedChars,
    804    error,
    805  };
    806 }
    807 
    808 /**
    809 * Computes the request headers of an HTTP request
    810 *
    811 * @param {string} method: request method
    812 * @param {string} httpVersion: request http version
    813 * @param {object} requestHeaders: request headers
    814 * @param {object} urlDetails: request url details
    815 *
    816 * @return {string} the request headers
    817 */
    818 function getRequestHeadersRawText(
    819  method,
    820  httpVersion,
    821  requestHeaders,
    822  urlDetails
    823 ) {
    824  const url = getUrl(urlDetails.url);
    825  const path = url ? `${url.pathname}${url.search}` : "<unknown>";
    826  const preHeaderText = `${method} ${path} ${httpVersion}`;
    827  return writeHeaderText(requestHeaders.headers, preHeaderText).trim();
    828 }
    829 
    830 /**
    831 * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
    832 * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
    833 *
    834 * @param object
    835 *        An object containing the { responseHeaders, status } properties.
    836 * @return boolean
    837 *         True if the response is fresh and loaded from cache.
    838 */
    839 function responseIsFresh({ responseHeaders, status }) {
    840  // Check for a "304 Not Modified" status and response headers availability.
    841  if (status != 304 || !responseHeaders) {
    842    return false;
    843  }
    844 
    845  const list = responseHeaders.headers;
    846  const cacheControl = list.find(e => e.name.toLowerCase() === "cache-control");
    847  const expires = list.find(e => e.name.toLowerCase() === "expires");
    848 
    849  // Check the "Cache-Control" header for a maximum age value.
    850  if (cacheControl) {
    851    const maxAgeMatch =
    852      cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
    853      cacheControl.value.match(/max-age\s*=\s*(\d+)/);
    854 
    855    if (maxAgeMatch && maxAgeMatch.pop() > 0) {
    856      return true;
    857    }
    858  }
    859 
    860  // Check the "Expires" header for a valid date.
    861  if (expires && Date.parse(expires.value)) {
    862    return true;
    863  }
    864 
    865  return false;
    866 }
    867 
    868 module.exports = {
    869  decodeUnicodeBase64,
    870  getFormDataSections,
    871  fetchHeaders,
    872  fetchNetworkUpdatePacket,
    873  formDataURI,
    874  writeHeaderText,
    875  getAbbreviatedMimeType,
    876  getFileName,
    877  getEndTime,
    878  getFormattedProtocol,
    879  getMessagePayload,
    880  getRequestHeader,
    881  getResponseHeader,
    882  getResponseTime,
    883  getStartTime,
    884  getUrl,
    885  getUrlBaseName,
    886  getUrlDetails,
    887  getUrlHost,
    888  getUrlHostName,
    889  getUrlQuery,
    890  getUrlScheme,
    891  getUrlToolTip,
    892  parseQueryString,
    893  parseFormData,
    894  updateFormDataSections,
    895  processNetworkUpdates,
    896  propertiesEqual,
    897  ipToLong,
    898  parseJSON,
    899  getRequestHeadersRawText,
    900  responseIsFresh,
    901 };