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