summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/src/utils')
-rw-r--r--devtools/client/netmonitor/src/utils/context-menu-utils.js32
-rw-r--r--devtools/client/netmonitor/src/utils/doc-utils.js224
-rw-r--r--devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js209
-rw-r--r--devtools/client/netmonitor/src/utils/filter-predicates.js137
-rw-r--r--devtools/client/netmonitor/src/utils/filter-text-utils.js291
-rw-r--r--devtools/client/netmonitor/src/utils/firefox/moz.build8
-rw-r--r--devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js67
-rw-r--r--devtools/client/netmonitor/src/utils/format-utils.js132
-rw-r--r--devtools/client/netmonitor/src/utils/headers-provider.js90
-rw-r--r--devtools/client/netmonitor/src/utils/l10n.js11
-rw-r--r--devtools/client/netmonitor/src/utils/moz.build27
-rw-r--r--devtools/client/netmonitor/src/utils/open-request-in-tab.js63
-rw-r--r--devtools/client/netmonitor/src/utils/powershell.js142
-rw-r--r--devtools/client/netmonitor/src/utils/prefs.js18
-rw-r--r--devtools/client/netmonitor/src/utils/request-blocking.js13
-rw-r--r--devtools/client/netmonitor/src/utils/request-utils.js769
-rw-r--r--devtools/client/netmonitor/src/utils/sort-predicates.js319
-rw-r--r--devtools/client/netmonitor/src/utils/sort-utils.js42
-rw-r--r--devtools/client/netmonitor/src/utils/tooltips.js18
19 files changed, 2612 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/utils/context-menu-utils.js b/devtools/client/netmonitor/src/utils/context-menu-utils.js
new file mode 100644
index 0000000000..3b44ff20cc
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/context-menu-utils.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * The default format for the content copied to the
+ * clipboard when the `Copy Value` option is selected.
+ */
+function baseCopyFormatter({ name, value, object, hasChildren }) {
+ if (hasChildren) {
+ return baseCopyAllFormatter({ [name]: value });
+ }
+ return `${value}`;
+}
+
+/**
+ * The default format for the content copied to the
+ * clipboard when the `Copy All` option is selected.
+ * @param {Object} object The whole data object
+ */
+function baseCopyAllFormatter(object) {
+ return JSON.stringify(object, null, "\t");
+}
+
+module.exports = {
+ contextMenuFormatters: {
+ baseCopyFormatter,
+ baseCopyAllFormatter,
+ },
+};
diff --git a/devtools/client/netmonitor/src/utils/doc-utils.js b/devtools/client/netmonitor/src/utils/doc-utils.js
new file mode 100644
index 0000000000..c08d96c453
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/doc-utils.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ SUPPORTED_HTTP_CODES,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+/**
+ * A mapping of header names to external documentation. Any header included
+ * here will show a MDN link alongside it.
+ */
+const SUPPORTED_HEADERS = [
+ "Accept",
+ "Accept-Charset",
+ "Accept-Encoding",
+ "Accept-Language",
+ "Accept-Ranges",
+ "Access-Control-Allow-Credentials",
+ "Access-Control-Allow-Headers",
+ "Access-Control-Allow-Methods",
+ "Access-Control-Allow-Origin",
+ "Access-Control-Expose-Headers",
+ "Access-Control-Max-Age",
+ "Access-Control-Request-Headers",
+ "Access-Control-Request-Method",
+ "Age",
+ "Allow",
+ "Authorization",
+ "Cache-Control",
+ "Clear-Site-Data",
+ "Connection",
+ "Content-Disposition",
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Length",
+ "Content-Location",
+ "Content-Range",
+ "Content-Security-Policy",
+ "Content-Security-Policy-Report-Only",
+ "Content-Type",
+ "Cookie",
+ "Cookie2",
+ "DNT",
+ "Date",
+ "ETag",
+ "Early-Data",
+ "Expect",
+ "Expect-CT",
+ "Expires",
+ "Feature-Policy",
+ "Forwarded",
+ "From",
+ "Host",
+ "If-Match",
+ "If-Modified-Since",
+ "If-None-Match",
+ "If-Range",
+ "If-Unmodified-Since",
+ "Keep-Alive",
+ "Last-Modified",
+ "Location",
+ "Origin",
+ "Pragma",
+ "Proxy-Authenticate",
+ "Proxy-Authorization",
+ "Public-Key-Pins",
+ "Public-Key-Pins-Report-Only",
+ "Range",
+ "Referer",
+ "Referrer-Policy",
+ "Retry-After",
+ "Save-Data",
+ "Sec-Fetch-Dest",
+ "Sec-Fetch-Mode",
+ "Sec-Fetch-Site",
+ "Sec-Fetch-User",
+ "Sec-GPC",
+ "Server",
+ "Server-Timing",
+ "Set-Cookie",
+ "Set-Cookie2",
+ "SourceMap",
+ "Strict-Transport-Security",
+ "TE",
+ "Timing-Allow-Origin",
+ "Tk",
+ "Trailer",
+ "Transfer-Encoding",
+ "Upgrade-Insecure-Requests",
+ "User-Agent",
+ "Vary",
+ "Via",
+ "WWW-Authenticate",
+ "Warning",
+ "X-Content-Type-Options",
+ "X-DNS-Prefetch-Control",
+ "X-Forwarded-For",
+ "X-Forwarded-Host",
+ "X-Forwarded-Proto",
+ "X-Frame-Options",
+ "X-XSS-Protection",
+];
+
+const MDN_URL = "https://developer.mozilla.org/docs/";
+const MDN_STATUS_CODES_LIST_URL = `${MDN_URL}Web/HTTP/Status`;
+const getGAParams = (panelId = "netmonitor") => {
+ return `?utm_source=mozilla&utm_medium=devtools-${panelId}&utm_campaign=default`;
+};
+
+// Base URL to DevTools user docs
+const USER_DOC_URL = "https://firefox-source-docs.mozilla.org/devtools-user/";
+
+/**
+ * Get the MDN URL for the specified header.
+ *
+ * @param {string} header Name of the header for the baseURL to use.
+ *
+ * @return {string} The MDN URL for the header, or null if not available.
+ */
+function getHeadersURL(header) {
+ const lowerCaseHeader = header.toLowerCase();
+ const idx = SUPPORTED_HEADERS.findIndex(
+ item => item.toLowerCase() === lowerCaseHeader
+ );
+ return idx > -1
+ ? `${MDN_URL}Web/HTTP/Headers/${SUPPORTED_HEADERS[idx] + getGAParams()}`
+ : null;
+}
+
+/**
+ * Get the MDN URL for the specified HTTP status code.
+ *
+ * @param {string} HTTP status code for the baseURL to use.
+ *
+ * @return {string} The MDN URL for the HTTP status code, or null if not available.
+ */
+function getHTTPStatusCodeURL(statusCode, panelId) {
+ return (
+ (SUPPORTED_HTTP_CODES.includes(statusCode)
+ ? `${MDN_URL}Web/HTTP/Status/${statusCode}`
+ : MDN_STATUS_CODES_LIST_URL) + getGAParams(panelId)
+ );
+}
+
+/**
+ * Get the URL of the Timings tag for Network Monitor.
+ *
+ * @return {string} the URL of the Timings tag for Network Monitor.
+ */
+function getNetMonitorTimingsURL() {
+ return `${USER_DOC_URL}network_monitor/request_details/#network-monitor-request-details-timings-tab`;
+}
+
+/**
+ * Get the URL for Performance Analysis
+ *
+ * @return {string} The URL for the documentation of Performance Analysis.
+ */
+function getPerformanceAnalysisURL() {
+ return `${USER_DOC_URL}network_monitor/performance_analysis/`;
+}
+
+/**
+ * Get the URL for Filter box
+ *
+ * @return {string} The URL for the documentation of Filter box.
+ */
+function getFilterBoxURL() {
+ return `${USER_DOC_URL}network_monitor/request_list/#filtering-by-properties`;
+}
+
+/**
+ * Get the MDN URL for Tracking Protection
+ *
+ * @return {string} The MDN URL for the documentation of Tracking Protection.
+ */
+function getTrackingProtectionURL() {
+ return `${MDN_URL}Mozilla/Firefox/Privacy/Tracking_Protection${getGAParams()}`;
+}
+
+/**
+ * Get the MDN URL for CORS error reason, falls back to generic cors error page
+ * if reason is not understood.
+ *
+ * @param {int} reason: Blocked Reason message from `netmonitor/src/constants.js`
+ *
+ * @returns {string} the MDN URL for the documentation of CORS errors
+ */
+function getCORSErrorURL(reason) {
+ // Map from blocked reasons from netmonitor/src/constants.js to the correct
+ // URL fragment to append to MDN_URL
+ const reasonMap = new Map([
+ [1001, "CORSDisabled"],
+ [1002, "CORSDidNotSucceed"],
+ [1003, "CORSRequestNotHttp"],
+ [1004, "CORSMultipleAllowOriginNotAllowed"],
+ [1005, "CORSMissingAllowOrigin"],
+ [1006, "CORSNotSupportingCredentials"],
+ [1007, "CORSAllowOriginNotMatchingOrigin"],
+ [1008, "CORSMIssingAllowCredentials"],
+ [1009, "CORSOriginHeaderNotAdded"],
+ [1010, "CORSExternalRedirectNotAllowed"],
+ [1011, "CORSPreflightDidNotSucceed"],
+ [1012, "CORSInvalidAllowMethod"],
+ [1013, "CORSMethodNotFound"],
+ [1014, "CORSInvalidAllowHeader"],
+ [1015, "CORSMissingAllowHeaderFromPreflight"],
+ ]);
+ const urlFrag = reasonMap.get(reason) || "";
+ return `${MDN_URL}Web/HTTP/CORS/Errors/${urlFrag}`;
+}
+
+module.exports = {
+ getHeadersURL,
+ getHTTPStatusCodeURL,
+ getNetMonitorTimingsURL,
+ getPerformanceAnalysisURL,
+ getFilterBoxURL,
+ getTrackingProtectionURL,
+ getCORSErrorURL,
+};
diff --git a/devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js b/devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js
new file mode 100644
index 0000000000..991027c67b
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ FILTER_FLAGS,
+ SUPPORTED_HTTP_CODES,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const {
+ getRequestPriorityAsText,
+} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js");
+
+/**
+ * Generates a value for the given filter
+ * ie. if flag = status-code, will generate "200" from the given request item.
+ * For flags related to cookies, it might generate an array based on the request
+ * ie. ["cookie-name-1", "cookie-name-2", ...]
+ *
+ * @param {string} flag - flag specified in filter, ie. "status-code"
+ * @param {object} request - Network request item
+ * @return {string|Array} - The output is a string or an array based on the request
+ */
+function getAutocompleteValuesForFlag(flag, request) {
+ let values = [];
+ let { responseCookies = { cookies: [] } } = request;
+ responseCookies = responseCookies.cookies || responseCookies;
+
+ switch (flag) {
+ case "status-code":
+ // Sometimes status comes as Number
+ values.push(String(request.status));
+ break;
+ case "scheme":
+ values.push(request.urlDetails.scheme);
+ break;
+ case "domain":
+ values.push(request.urlDetails.host);
+ break;
+ case "remote-ip":
+ values.push(request.remoteAddress);
+ break;
+ case "cause":
+ values.push(request.cause.type);
+ break;
+ case "mime-type":
+ values.push((request.mimeType || "").replace(/;.+/, ""));
+ break;
+ case "set-cookie-name":
+ values = responseCookies.map(c => c.name);
+ break;
+ case "set-cookie-value":
+ values = responseCookies.map(c => c.value);
+ break;
+ case "priority":
+ values.push(getRequestPriorityAsText(request.priority));
+ break;
+ case "set-cookie-domain":
+ values = responseCookies.map(c =>
+ c.hasOwnProperty("domain") ? c.domain : request.urlDetails.host
+ );
+ break;
+ case "is":
+ values = ["cached", "from-cache", "running"];
+ break;
+ case "has-response-header":
+ // Some requests not having responseHeaders..?
+ values = request.responseHeaders
+ ? request.responseHeaders.headers.map(h => h.name)
+ : [];
+ break;
+ case "protocol":
+ values.push(request.httpVersion);
+ break;
+ case "method":
+ default:
+ values.push(request[flag]);
+ }
+
+ return values;
+}
+
+/**
+ * For a given lastToken passed ie. "is:", returns an array of populated flag
+ * values for consumption in autocompleteProvider
+ * ie. ["is:cached", "is:running", "is:from-cache"]
+ *
+ * @param {string} lastToken - lastToken parsed from filter input, ie "is:"
+ * @param {object} requests - List of requests from which values are generated
+ * @return {Array} - array of autocomplete values
+ */
+function getLastTokenFlagValues(lastToken, requests) {
+ // The last token must be a string like "method:GET" or "method:", Any token
+ // without a ":" cant be used to parse out flag values
+ if (!lastToken.includes(":")) {
+ return [];
+ }
+
+ // Parse out possible flag from lastToken
+ let [flag, typedFlagValue] = lastToken.split(":");
+ let isNegativeFlag = false;
+
+ // Check if flag is used with negative match
+ if (flag.startsWith("-")) {
+ flag = flag.slice(1);
+ isNegativeFlag = true;
+ }
+
+ // Flag is some random string, return
+ if (!FILTER_FLAGS.includes(flag)) {
+ return [];
+ }
+
+ let values = [];
+ for (const request of requests) {
+ values.push(...getAutocompleteValuesForFlag(flag, request));
+ }
+ values = [...new Set(values)];
+
+ return values
+ .filter(value => value)
+ .filter(value => {
+ if (typedFlagValue && value) {
+ const lowerTyped = typedFlagValue.toLowerCase();
+ const lowerValue = value.toLowerCase();
+ return lowerValue.includes(lowerTyped) && lowerValue !== lowerTyped;
+ }
+ return (
+ typeof value !== "undefined" && value !== "" && value !== "undefined"
+ );
+ })
+ .sort()
+ .map(value => (isNegativeFlag ? `-${flag}:${value}` : `${flag}:${value}`));
+}
+
+/**
+ * Generates an autocomplete list for the search-box for network monitor
+ *
+ * It expects an entire string of the searchbox ie "is:cached pr".
+ * The string is then tokenized into "is:cached" and "pr"
+ *
+ * @param {string} filter - The entire search string of the search box
+ * @param {object} requests - Iteratable object of requests displayed
+ * @return {Array} - The output is an array of objects as below
+ * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
+ * `value` is used to update the search-box input box for given item
+ * `displayValue` is used to render the autocomplete list
+ */
+function autocompleteProvider(filter, requests) {
+ if (!filter) {
+ return [];
+ }
+
+ const negativeAutocompleteList = FILTER_FLAGS.map(item => `-${item}`);
+ const baseList = [...FILTER_FLAGS, ...negativeAutocompleteList].map(
+ item => `${item}:`
+ );
+
+ // The last token is used to filter the base autocomplete list
+ const tokens = filter.split(/\s+/g);
+ const lastToken = tokens[tokens.length - 1];
+ const previousTokens = tokens.slice(0, tokens.length - 1);
+
+ // Autocomplete list is not generated for empty lastToken
+ if (!lastToken) {
+ return [];
+ }
+
+ let autocompleteList;
+ const availableValues = getLastTokenFlagValues(lastToken, requests);
+ if (availableValues.length) {
+ autocompleteList = availableValues;
+ } else {
+ const isNegativeFlag = lastToken.startsWith("-");
+
+ // Stores list of HTTP codes that starts with value of lastToken
+ const filteredStatusCodes = SUPPORTED_HTTP_CODES.filter(item => {
+ item = isNegativeFlag ? item.substr(1) : item;
+ return item.toLowerCase().startsWith(lastToken.toLowerCase());
+ });
+
+ if (filteredStatusCodes.length) {
+ // Shows an autocomplete list of "status-code" values from filteredStatusCodes
+ autocompleteList = isNegativeFlag
+ ? filteredStatusCodes.map(item => `-status-code:${item}`)
+ : filteredStatusCodes.map(item => `status-code:${item}`);
+ } else {
+ // Shows an autocomplete list of values from baseList
+ // that starts with value of lastToken
+ autocompleteList = baseList.filter(item => {
+ return (
+ item.toLowerCase().startsWith(lastToken.toLowerCase()) &&
+ item.toLowerCase() !== lastToken.toLowerCase()
+ );
+ });
+ }
+ }
+
+ return autocompleteList.sort().map(item => ({
+ value: [...previousTokens, item].join(" "),
+ displayValue: item,
+ }));
+}
+
+module.exports = {
+ autocompleteProvider,
+};
diff --git a/devtools/client/netmonitor/src/utils/filter-predicates.js b/devtools/client/netmonitor/src/utils/filter-predicates.js
new file mode 100644
index 0000000000..63105dec0c
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/filter-predicates.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ isFreetextMatch,
+} = require("resource://devtools/client/netmonitor/src/utils/filter-text-utils.js");
+
+/**
+ * Predicates used when filtering items.
+ *
+ * @param object item
+ * The filtered item.
+ * @return boolean
+ * True if the item should be visible, false otherwise.
+ */
+function all() {
+ return true;
+}
+
+function isHtml({ mimeType }) {
+ return (
+ mimeType && (mimeType.includes("/html") || mimeType.includes("/xhtml+xml"))
+ );
+}
+
+function isCss({ mimeType }) {
+ return mimeType && mimeType.includes("/css");
+}
+
+function isJs({ mimeType }) {
+ return (
+ mimeType &&
+ (mimeType.includes("/ecmascript") ||
+ mimeType.includes("/javascript") ||
+ mimeType.includes("/x-javascript"))
+ );
+}
+
+function isXHR(item) {
+ // Show the request it is XHR, except if the request is a WS upgrade
+ return item.isXHR && !isWS(item);
+}
+
+function isFont({ url, mimeType }) {
+ // Fonts are a mess.
+ return (
+ (mimeType && (mimeType.includes("font/") || mimeType.includes("/font"))) ||
+ url.includes(".eot") ||
+ url.includes(".ttf") ||
+ url.includes(".otf") ||
+ url.includes(".woff")
+ );
+}
+
+function isImage({ mimeType, cause }) {
+ // We check cause.type so anything loaded via "img", "imageset", "lazy-img", etc. is in the right category
+ // When mimeType is not set to "image/", we still "detect" the image with cause.type (Bug-1654257)
+ return (
+ mimeType?.includes("image/") ||
+ cause?.type.includes("img") ||
+ cause?.type.includes("image")
+ );
+}
+
+function isMedia({ mimeType }) {
+ // Not including images.
+ return (
+ mimeType &&
+ (mimeType.includes("audio/") ||
+ mimeType.includes("video/") ||
+ mimeType.includes("model/") ||
+ mimeType === "application/vnd.apple.mpegurl" ||
+ mimeType === "application/x-mpegurl" ||
+ mimeType === "application/ogg")
+ );
+}
+
+function isWS({ requestHeaders, responseHeaders, cause }) {
+ // For the first call, the requestHeaders is not ready(empty),
+ // so checking for cause.type instead (Bug-1454962)
+ if (typeof cause.type === "string" && cause.type === "websocket") {
+ return true;
+ }
+ // Detect a websocket upgrade if request has an Upgrade header with value 'websocket'
+ if (!requestHeaders || !Array.isArray(requestHeaders.headers)) {
+ return false;
+ }
+
+ // Find the 'upgrade' header.
+ let upgradeHeader = requestHeaders.headers.find(header => {
+ return header.name.toLowerCase() == "upgrade";
+ });
+
+ // If no header found on request, check response - mainly to get
+ // something we can unit test, as it is impossible to set
+ // the Upgrade header on outgoing XHR as per the spec.
+ if (
+ !upgradeHeader &&
+ responseHeaders &&
+ Array.isArray(responseHeaders.headers)
+ ) {
+ upgradeHeader = responseHeaders.headers.find(header => {
+ return header.name.toLowerCase() == "upgrade";
+ });
+ }
+
+ // Return false if there is no such header or if its value isn't 'websocket'.
+ if (!upgradeHeader || upgradeHeader.value != "websocket") {
+ return false;
+ }
+
+ return true;
+}
+
+function isOther(item) {
+ const tests = [isHtml, isCss, isJs, isXHR, isFont, isImage, isMedia, isWS];
+ return tests.every(is => !is(item));
+}
+
+module.exports = {
+ Filters: {
+ all,
+ html: isHtml,
+ css: isCss,
+ js: isJs,
+ xhr: isXHR,
+ fonts: isFont,
+ images: isImage,
+ media: isMedia,
+ ws: isWS,
+ other: isOther,
+ },
+ isFreetextMatch,
+};
diff --git a/devtools/client/netmonitor/src/utils/filter-text-utils.js b/devtools/client/netmonitor/src/utils/filter-text-utils.js
new file mode 100644
index 0000000000..911e70e4bd
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/filter-text-utils.js
@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 2013 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+"use strict";
+
+const {
+ FILTER_FLAGS,
+ SUPPORTED_HTTP_CODES,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const {
+ getFormattedIPAndPort,
+ getRequestPriorityAsText,
+} = require("resource://devtools/client/netmonitor/src/utils/format-utils.js");
+const {
+ getUnicodeUrl,
+} = require("resource://devtools/client/shared/unicode-url.js");
+const {
+ getUrlBaseName,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+/*
+ The function `parseFilters` is from:
+ https://github.com/ChromeDevTools/devtools-frontend/
+
+ front_end/network/FilterSuggestionBuilder.js#L138-L163
+ Commit f340aefd7ec9b702de9366a812288cfb12111fce
+*/
+
+function parseFilters(query) {
+ const flags = [];
+ const text = [];
+ const parts = query.split(/\s+/);
+
+ for (const part of parts) {
+ if (!part) {
+ continue;
+ }
+ const colonIndex = part.indexOf(":");
+ if (colonIndex === -1) {
+ const isNegative = part.startsWith("-");
+ // Stores list of HTTP codes that starts with value of lastToken
+ const filteredStatusCodes = SUPPORTED_HTTP_CODES.filter(item => {
+ item = isNegative ? item.substr(1) : item;
+ return item.toLowerCase().startsWith(part.toLowerCase());
+ });
+
+ if (filteredStatusCodes.length) {
+ flags.push({
+ type: "status-code", // a standard key before a colon
+ value: isNegative ? part.substring(1) : part,
+ isNegative,
+ });
+ continue;
+ }
+
+ // Value of lastToken is just text that does not correspond to status codes
+ text.push(part);
+ continue;
+ }
+ let key = part.substring(0, colonIndex);
+ const negative = key.startsWith("-");
+ if (negative) {
+ key = key.substring(1);
+ }
+ if (!FILTER_FLAGS.includes(key)) {
+ text.push(part);
+ continue;
+ }
+ let value = part.substring(colonIndex + 1);
+ value = processFlagFilter(key, value);
+ flags.push({
+ type: key,
+ value,
+ negative,
+ });
+ }
+
+ return { text, flags };
+}
+
+function processFlagFilter(type, value) {
+ switch (type) {
+ case "regexp":
+ return value;
+ case "size":
+ case "transferred":
+ case "larger-than":
+ case "transferred-larger-than":
+ let multiplier = 1;
+ if (value.endsWith("k")) {
+ multiplier = 1000;
+ value = value.substring(0, value.length - 1);
+ } else if (value.endsWith("m")) {
+ multiplier = 1000 * 1000;
+ value = value.substring(0, value.length - 1);
+ }
+ const quantity = Number(value);
+ if (isNaN(quantity)) {
+ return null;
+ }
+ return quantity * multiplier;
+ default:
+ return value.toLowerCase();
+ }
+}
+
+function isFlagFilterMatch(item, { type, value, negative }) {
+ if (value == null) {
+ return false;
+ }
+
+ // Ensures when filter token is exactly a flag ie. "remote-ip:", all values are shown
+ if (value.length < 1) {
+ return true;
+ }
+
+ let match = true;
+ let { responseCookies = { cookies: [] } } = item;
+ responseCookies = responseCookies.cookies || responseCookies;
+
+ const matchers = {
+ "status-code": () =>
+ item.status && item.status.toString().startsWith(value),
+ method: () => item.method.toLowerCase() === value,
+ protocol: () => {
+ const protocol = item.httpVersion;
+ return typeof protocol === "string"
+ ? protocol.toLowerCase().includes(value)
+ : false;
+ },
+ domain: () => item.urlDetails.host.toLowerCase().includes(value),
+ "remote-ip": () => {
+ const data = getFormattedIPAndPort(item.remoteAddress, item.remotePort);
+ return data ? data.toLowerCase().includes(value) : false;
+ },
+ "has-response-header": () => {
+ if (typeof item.responseHeaders === "object") {
+ const { headers } = item.responseHeaders;
+ return headers.findIndex(h => h.name.toLowerCase() === value) > -1;
+ }
+ return false;
+ },
+ cause: () => {
+ const causeType = item.cause.type;
+ return typeof causeType === "string"
+ ? causeType.toLowerCase().includes(value)
+ : false;
+ },
+ initiator: () => {
+ const initiator = item.cause.lastFrame
+ ? getUrlBaseName(item.cause.lastFrame.filename) +
+ ":" +
+ item.cause.lastFrame.lineNumber
+ : "";
+ return typeof initiator === "string"
+ ? initiator.toLowerCase().includes(value)
+ : !value;
+ },
+ transferred: () => {
+ if (item.fromCache) {
+ return false;
+ }
+ return isSizeMatch(value, item.transferredSize);
+ },
+ size: () => isSizeMatch(value, item.contentSize),
+ "larger-than": () => item.contentSize > value,
+ "transferred-larger-than": () => {
+ if (item.fromCache) {
+ return false;
+ }
+ return item.transferredSize > value;
+ },
+ "mime-type": () => {
+ if (!item.mimeType) {
+ return false;
+ }
+ return item.mimeType.includes(value);
+ },
+ is: () => {
+ if (value === "from-cache" || value === "cached") {
+ return item.fromCache || item.status === "304";
+ }
+ if (value === "running") {
+ return !item.status;
+ }
+ return match;
+ },
+ scheme: () => item.urlDetails.scheme === value,
+ regexp: () => {
+ try {
+ const pattern = new RegExp(value);
+ return pattern.test(item.url);
+ } catch (e) {
+ return false;
+ }
+ },
+ priority: () =>
+ getRequestPriorityAsText(item.priority).toLowerCase() == value,
+ "set-cookie-domain": () => {
+ if (responseCookies.length) {
+ const { host } = item.urlDetails;
+ const i = responseCookies.findIndex(c => {
+ const domain = c.hasOwnProperty("domain") ? c.domain : host;
+ return domain.includes(value);
+ });
+ return i > -1;
+ }
+ return false;
+ },
+ "set-cookie-name": () =>
+ responseCookies.findIndex(c => c.name.toLowerCase().includes(value)) > -1,
+ "set-cookie-value": () =>
+ responseCookies.findIndex(c => c.value.toLowerCase().includes(value)) >
+ -1,
+ };
+
+ const matcher = matchers[type];
+ if (matcher) {
+ match = matcher();
+ }
+
+ return negative ? !match : match;
+}
+
+function isSizeMatch(value, size) {
+ return value >= size - size / 10 && value <= size + size / 10;
+}
+
+function isTextFilterMatch({ url }, text) {
+ const lowerCaseUrl = getUnicodeUrl(url).toLowerCase();
+ let lowerCaseText = text.toLowerCase();
+ const textLength = text.length;
+ // Support negative filtering
+ if (text.startsWith("-") && textLength > 1) {
+ lowerCaseText = lowerCaseText.substring(1, textLength);
+ return !lowerCaseUrl.includes(lowerCaseText);
+ }
+
+ // no text is a positive match
+ return !text || lowerCaseUrl.includes(lowerCaseText);
+}
+
+function isFreetextMatch(item, text) {
+ if (!text) {
+ return true;
+ }
+
+ const filters = parseFilters(text);
+ let match = true;
+
+ for (const textFilter of filters.text) {
+ match = match && isTextFilterMatch(item, textFilter);
+ }
+
+ for (const flagFilter of filters.flags) {
+ match = match && isFlagFilterMatch(item, flagFilter);
+ }
+
+ return match;
+}
+
+module.exports = {
+ isFreetextMatch,
+};
diff --git a/devtools/client/netmonitor/src/utils/firefox/moz.build b/devtools/client/netmonitor/src/utils/firefox/moz.build
new file mode 100644
index 0000000000..bb0acafdf7
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/firefox/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "open-request-in-tab.js",
+)
diff --git a/devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js b/devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js
new file mode 100644
index 0000000000..20ea3dcba2
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is a chrome-API-dependent version of the module
+// devtools/client/netmonitor/src/utils/open-request-in-tab.js, so that it can
+// take advantage of utilizing chrome APIs. But because of this, it isn't
+// intended to be used in Chrome-API-free applications, such as the Launchpad.
+//
+// Please keep in mind that if the feature in this file has changed, don't
+// forget to also change that accordingly in
+// devtools/client/netmonitor/src/utils/open-request-in-tab.js.
+
+"use strict";
+
+const {
+ gDevTools,
+} = require("resource://devtools/client/framework/devtools.js");
+
+/**
+ * Opens given request in a new tab.
+ */
+function openRequestInTab(url, requestHeaders, requestPostData) {
+ const win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ const rawData = requestPostData ? requestPostData.postData : null;
+ let postData;
+
+ if (rawData?.text) {
+ const stringStream = getInputStreamFromString(rawData.text);
+ postData = Cc["@mozilla.org/network/mime-input-stream;1"].createInstance(
+ Ci.nsIMIMEInputStream
+ );
+
+ const contentTypeHeader = requestHeaders.headers.find(e => {
+ return e.name.toLowerCase() === "content-type";
+ });
+
+ postData.addHeader(
+ "Content-Type",
+ contentTypeHeader
+ ? contentTypeHeader.value
+ : "application/x-www-form-urlencoded"
+ );
+ postData.setData(stringStream);
+ }
+ const { userContextId } = win.gBrowser.contentPrincipal;
+ win.gBrowser.selectedTab = win.gBrowser.addWebTab(url, {
+ // TODO this should be using the original request principal
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
+ userContextId,
+ }),
+ userContextId,
+ postData,
+ });
+}
+
+function getInputStreamFromString(data) {
+ const stringStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ stringStream.data = data;
+ return stringStream;
+}
+
+module.exports = {
+ openRequestInTab,
+};
diff --git a/devtools/client/netmonitor/src/utils/format-utils.js b/devtools/client/netmonitor/src/utils/format-utils.js
new file mode 100644
index 0000000000..c0d5c29818
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/format-utils.js
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+
+// Constants for formatting bytes.
+const BYTES_IN_KB = 1000;
+const BYTES_IN_MB = Math.pow(BYTES_IN_KB, 2);
+const BYTES_IN_GB = Math.pow(BYTES_IN_KB, 3);
+const MAX_BYTES_SIZE = 1000;
+const MAX_KB_SIZE = 1000 * BYTES_IN_KB;
+const MAX_MB_SIZE = 1000 * BYTES_IN_MB;
+
+// Constants for formatting time.
+const MAX_MILLISECOND = 1000;
+const MAX_SECOND = 60 * MAX_MILLISECOND;
+
+const REQUEST_DECIMALS = 2;
+
+// Constants for formatting the priority, derived from nsISupportsPriority.idl
+const PRIORITY_HIGH = -10;
+const PRIORITY_NORMAL = 0;
+const PRIORITY_LOW = 10;
+
+function getSizeWithDecimals(size, decimals = REQUEST_DECIMALS) {
+ return L10N.numberWithDecimals(size, decimals);
+}
+
+function getTimeWithDecimals(time) {
+ return L10N.numberWithDecimals(time, REQUEST_DECIMALS);
+}
+
+function formatDecimals(size, decimals) {
+ return size % 1 > 0 ? decimals : 0;
+}
+
+/**
+ * Get a human-readable string from a number of bytes, with the B, kB, MB, or
+ * GB value.
+ */
+function getFormattedSize(bytes, decimals = REQUEST_DECIMALS) {
+ if (bytes < MAX_BYTES_SIZE) {
+ return L10N.getFormatStr("networkMenu.sizeB", bytes);
+ }
+ if (bytes < MAX_KB_SIZE) {
+ const kb = bytes / BYTES_IN_KB;
+ const formattedDecimals = formatDecimals(kb, decimals);
+
+ return L10N.getFormatStr(
+ "networkMenu.size.kB",
+ getSizeWithDecimals(kb, formattedDecimals)
+ );
+ }
+ if (bytes < MAX_MB_SIZE) {
+ const mb = bytes / BYTES_IN_MB;
+ const formattedDecimals = formatDecimals(mb, decimals);
+ return L10N.getFormatStr(
+ "networkMenu.sizeMB",
+ getSizeWithDecimals(mb, formattedDecimals)
+ );
+ }
+ const gb = bytes / BYTES_IN_GB;
+ const formattedDecimals = formatDecimals(gb, decimals);
+ return L10N.getFormatStr(
+ "networkMenu.sizeGB",
+ getSizeWithDecimals(gb, formattedDecimals)
+ );
+}
+
+/**
+ * Get a human-readable string from a number of time, with the ms, s, or min
+ * value.
+ */
+function getFormattedTime(ms) {
+ if (ms < MAX_MILLISECOND) {
+ return L10N.getFormatStr("networkMenu.millisecond", ms | 0);
+ }
+ if (ms < MAX_SECOND) {
+ const sec = ms / MAX_MILLISECOND;
+ return L10N.getFormatStr("networkMenu.second", getTimeWithDecimals(sec));
+ }
+ const min = ms / MAX_SECOND;
+ return L10N.getFormatStr("networkMenu.minute", getTimeWithDecimals(min));
+}
+
+/**
+ * Formats IP (v4 and v6) and port
+ *
+ * @param {string} ip - IP address
+ * @param {string} port
+ * @return {string} the formatted IP + port
+ */
+function getFormattedIPAndPort(ip, port) {
+ if (!port) {
+ return ip;
+ }
+ return ip.match(/:+/) ? `[${ip}]:${port}` : `${ip}:${port}`;
+}
+
+/**
+ * Formats the priority of a request
+ * Based on unix conventions
+ * See xpcom/threads/nsISupportsPriority.idl
+ *
+ * @param {Number} priority - request priority
+ */
+function getRequestPriorityAsText(priority) {
+ if (priority < PRIORITY_HIGH) {
+ return "Highest";
+ } else if (priority >= PRIORITY_HIGH && priority < PRIORITY_NORMAL) {
+ return "High";
+ } else if (priority === PRIORITY_NORMAL) {
+ return "Normal";
+ } else if (priority > PRIORITY_NORMAL && priority <= PRIORITY_LOW) {
+ return "Low";
+ }
+ return "Lowest";
+}
+
+module.exports = {
+ getFormattedIPAndPort,
+ getFormattedSize,
+ getFormattedTime,
+ getSizeWithDecimals,
+ getTimeWithDecimals,
+ getRequestPriorityAsText,
+};
diff --git a/devtools/client/netmonitor/src/utils/headers-provider.js b/devtools/client/netmonitor/src/utils/headers-provider.js
new file mode 100644
index 0000000000..a76e7d1c0e
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/headers-provider.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ ObjectProvider,
+} = require("resource://devtools/client/shared/components/tree/ObjectProvider.js");
+
+/**
+ * Custom tree provider.
+ *
+ * This provider is used to provide set of headers and is
+ * utilized by the HeadersPanel.
+ * The default ObjectProvider can't be used since it doesn't
+ * allow duplicities by design and so it can't support duplicity
+ * headers (more headers with the same name).
+ */
+var HeadersProvider = {
+ ...ObjectProvider,
+
+ getChildren(object) {
+ if (object && object.value instanceof HeaderList) {
+ return object.value.headers.map(
+ (header, index) => new Header(header.name, header.value, index)
+ );
+ }
+ return ObjectProvider.getChildren(object);
+ },
+
+ hasChildren(object) {
+ if (object.value instanceof HeaderList) {
+ return !!object.value.headers.length;
+ } else if (object instanceof Header) {
+ return false;
+ }
+ return ObjectProvider.hasChildren(object);
+ },
+
+ getLabel(object) {
+ if (object instanceof Header) {
+ return object.name;
+ }
+ return ObjectProvider.getLabel(object);
+ },
+
+ getValue(object) {
+ if (object instanceof Header) {
+ return object.value;
+ }
+ return ObjectProvider.getValue(object);
+ },
+
+ getKey(object) {
+ if (object instanceof Header) {
+ return object.key;
+ }
+ return ObjectProvider.getKey(object);
+ },
+
+ getType(object) {
+ if (object instanceof Header) {
+ return "string";
+ }
+ return ObjectProvider.getType(object);
+ },
+};
+
+/**
+ * Helper data structures for list of headers.
+ */
+function HeaderList(headers) {
+ // Clone, so the sort doesn't affect the original array.
+ this.headers = headers.slice(0);
+ this.headers.sort((a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+ });
+}
+
+function Header(name, value, key) {
+ this.name = name;
+ this.value = value;
+ this.key = key;
+}
+
+module.exports = {
+ HeadersProvider,
+ HeaderList,
+};
diff --git a/devtools/client/netmonitor/src/utils/l10n.js b/devtools/client/netmonitor/src/utils/l10n.js
new file mode 100644
index 0000000000..74c8b22299
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/l10n.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
+
+exports.L10N = new LocalizationHelper(NET_STRINGS_URI);
diff --git a/devtools/client/netmonitor/src/utils/moz.build b/devtools/client/netmonitor/src/utils/moz.build
new file mode 100644
index 0000000000..2eb6aec69c
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -0,0 +1,27 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "firefox",
+]
+
+DevToolsModules(
+ "context-menu-utils.js",
+ "doc-utils.js",
+ "filter-autocomplete-provider.js",
+ "filter-predicates.js",
+ "filter-text-utils.js",
+ "format-utils.js",
+ "headers-provider.js",
+ "l10n.js",
+ "open-request-in-tab.js",
+ "powershell.js",
+ "prefs.js",
+ "request-blocking.js",
+ "request-utils.js",
+ "sort-predicates.js",
+ "sort-utils.js",
+ "tooltips.js",
+)
diff --git a/devtools/client/netmonitor/src/utils/open-request-in-tab.js b/devtools/client/netmonitor/src/utils/open-request-in-tab.js
new file mode 100644
index 0000000000..cb63da61ec
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/open-request-in-tab.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is a chrome-API-free version of the module
+// devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js, so that
+// it can be used in Chrome-API-free applications, such as the Launchpad. But
+// because of this, it cannot take advantage of utilizing chrome APIs and should
+// implement the similar functionalities on its own.
+//
+// Please keep in mind that if the feature in this file has changed, don't
+// forget to also change that accordingly in
+// devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js.
+
+"use strict";
+
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+
+/**
+ * Opens given request in a new tab.
+ *
+ * For POST request supports application/x-www-form-urlencoded content-type only.
+ */
+function openRequestInTab(url, requestHeaders, requestPostData) {
+ if (!requestPostData) {
+ openContentLink(url, { relatedToCurrent: true });
+ } else {
+ openPostRequestInTabHelper({
+ url,
+ data: requestPostData.postData,
+ });
+ }
+}
+
+function openPostRequestInTabHelper({ url, data }) {
+ const form = document.createElement("form");
+ form.target = "_blank";
+ form.action = url;
+ form.method = "post";
+
+ if (data) {
+ for (const key in data) {
+ const input = document.createElement("input");
+ input.name = key;
+ input.value = data[key];
+ form.appendChild(input);
+ }
+ }
+
+ form.hidden = true;
+ document.body.appendChild(form);
+ form.submit();
+ form.remove();
+}
+
+module.exports = {
+ openRequestInTab,
+};
diff --git a/devtools/client/netmonitor/src/utils/powershell.js b/devtools/client/netmonitor/src/utils/powershell.js
new file mode 100644
index 0000000000..2efcfd8faa
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/powershell.js
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2022 Mozilla Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Utility to generate commands to invoke a request for powershell
+"use strict";
+
+// Some of these headers are passed in as seperate `Invoke-WebRequest` parameters so ignore
+// when building the headers list, others are not to neccesarily restrict the request.
+const IGNORED_HEADERS = [
+ "connection",
+ "proxy-connection",
+ "content-length",
+ "expect",
+ "range",
+ "host",
+ "content-type",
+ "user-agent",
+ "cookie",
+];
+/**
+ * This escapes strings for the powershell command
+ *
+ * 1. Escape the backtick, dollar sign and the double quotes See https://www.rlmueller.net/PowerShellEscape.htm
+ * 2. Convert any non printing ASCII characters found, using the ASCII code.
+ */
+function escapeStr(str) {
+ return `"${str
+ .replace(/[`\$"]/g, "`$&")
+ .replace(/[^\x20-\x7E]/g, char => "$([char]" + char.charCodeAt(0) + ")")}"`;
+}
+
+const PowerShell = {
+ generateCommand(url, method, headers, postData, cookies) {
+ const parameters = [];
+
+ // Create a WebSession to pass the information about cookies
+ // See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.2#-websession
+ const session = [];
+ for (const { name, value, domain } of cookies) {
+ if (!session.length) {
+ session.push(
+ "$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession"
+ );
+ }
+ session.push(
+ `$session.Cookies.Add((New-Object System.Net.Cookie(${escapeStr(
+ name
+ )}, ${escapeStr(value)}, "/", ${escapeStr(
+ domain || new URL(url).host
+ )})))`
+ );
+ }
+
+ parameters.push(`-Uri ${escapeStr(url)}`);
+
+ if (method !== "GET") {
+ parameters.push(`-Method ${method}`);
+ }
+
+ if (session.length) {
+ parameters.push("-WebSession $session");
+ }
+
+ const userAgent = headers.find(
+ ({ name }) => name.toLowerCase() === "user-agent"
+ );
+ if (userAgent) {
+ parameters.push("-UserAgent " + escapeStr(userAgent.value));
+ }
+
+ const headersStr = [];
+ for (let { name, value } of headers) {
+ // Translate any HTTP2 pseudo headers to HTTP headers
+ name = name.replace(/^:/, "");
+
+ if (IGNORED_HEADERS.includes(name.toLowerCase())) {
+ continue;
+ }
+ headersStr.push(`${escapeStr(name)} = ${escapeStr(value)}`);
+ }
+ if (headersStr.length) {
+ parameters.push(`-Headers @{\n${headersStr.join("\n ")}\n}`);
+ }
+
+ const contentType = headers.find(
+ header => header.name.toLowerCase() === "content-type"
+ );
+ if (contentType) {
+ parameters.push("-ContentType " + escapeStr(contentType.value));
+ }
+
+ const formData = postData.text;
+ if (formData) {
+ // Encode bytes if any of the characters is not an ASCII printing character (not between Space character and ~ character)
+ // a-zA-Z0-9 etc. See http://www.asciitable.com/
+ const body = /[^\x20-\x7E]/.test(formData)
+ ? "([System.Text.Encoding]::UTF8.GetBytes(" + escapeStr(formData) + "))"
+ : escapeStr(formData);
+ parameters.push("-Body " + body);
+ }
+
+ return `${
+ session.length ? session.join("\n").concat("\n") : ""
+ // -UseBasicParsing is added for backward compatibility.
+ // See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.2#-usebasicparsing
+ }Invoke-WebRequest -UseBasicParsing ${parameters.join(" `\n")}`;
+ },
+};
+
+exports.PowerShell = PowerShell;
diff --git a/devtools/client/netmonitor/src/utils/prefs.js b/devtools/client/netmonitor/src/utils/prefs.js
new file mode 100644
index 0000000000..3c988f4294
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/prefs.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PrefsHelper } = require("resource://devtools/client/shared/prefs.js");
+
+/**
+ * Shortcuts for accessing various network monitor preferences.
+ */
+exports.Prefs = new PrefsHelper("devtools.netmonitor", {
+ networkDetailsWidth: ["Int", "panes-network-details-width"],
+ networkDetailsHeight: ["Int", "panes-network-details-height"],
+ visibleColumns: ["Json", "visibleColumns"],
+ columnsData: ["Json", "columnsData"],
+ filters: ["Json", "filters"],
+});
diff --git a/devtools/client/netmonitor/src/utils/request-blocking.js b/devtools/client/netmonitor/src/utils/request-blocking.js
new file mode 100644
index 0000000000..67b217afe0
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/request-blocking.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function hasMatchingBlockingRequestPattern(blockedUrls, url) {
+ return blockedUrls.some(blockedUrl => url.includes(blockedUrl));
+}
+
+module.exports = {
+ hasMatchingBlockingRequestPattern,
+};
diff --git a/devtools/client/netmonitor/src/utils/request-utils.js b/devtools/client/netmonitor/src/utils/request-utils.js
new file mode 100644
index 0000000000..11273016be
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/request-utils.js
@@ -0,0 +1,769 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ getUnicodeUrl,
+ getUnicodeUrlPath,
+ getUnicodeHostname,
+} = require("resource://devtools/client/shared/unicode-url.js");
+
+const {
+ UPDATE_PROPS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+ ecmascript: "js",
+ javascript: "js",
+ "x-javascript": "js",
+};
+
+/**
+ * Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
+ * POST request.
+ *
+ * @param {object} headers - the "requestHeaders".
+ * @param {object} uploadHeaders - the "requestHeadersFromUploadStream".
+ * @param {object} postData - the "requestPostData".
+ * @return {array} a promise list that is resolved with the extracted form data.
+ */
+async function getFormDataSections(
+ headers,
+ uploadHeaders,
+ postData,
+ getLongString
+) {
+ const formDataSections = [];
+
+ const requestHeaders = headers.headers;
+ const payloadHeaders = uploadHeaders ? uploadHeaders.headers : [];
+ const allHeaders = [...payloadHeaders, ...requestHeaders];
+
+ const contentTypeHeader = allHeaders.find(e => {
+ return e.name.toLowerCase() == "content-type";
+ });
+
+ const contentTypeLongString = contentTypeHeader
+ ? contentTypeHeader.value
+ : "";
+
+ const contentType = await getLongString(contentTypeLongString);
+
+ if (contentType && contentType.includes("x-www-form-urlencoded")) {
+ const postDataLongString = postData.postData.text;
+ const text = await getLongString(postDataLongString);
+
+ for (const section of text.trim().split(/\r\n|\r|\n/)) {
+ // Before displaying it, make sure this section of the POST data
+ // isn't a line containing upload stream headers.
+ if (payloadHeaders.every(header => !section.startsWith(header.name))) {
+ formDataSections.push(section);
+ }
+ }
+ }
+
+ return formDataSections;
+}
+
+/**
+ * Fetch headers full content from actor server
+ *
+ * @param {object} headers - a object presents headers data
+ * @return {object} a headers object with updated content payload
+ */
+async function fetchHeaders(headers, getLongString) {
+ for (const { value } of headers.headers) {
+ headers.headers.value = await getLongString(value);
+ }
+ return headers;
+}
+
+/**
+ * Fetch network event update packets from actor server
+ * Expect to fetch a couple of network update packets from a given request.
+ *
+ * @param {function} requestData - requestData function for lazily fetch data
+ * @param {object} request - request object
+ * @param {array} updateTypes - a list of network event update types
+ */
+function fetchNetworkUpdatePacket(requestData, request, updateTypes) {
+ const promises = [];
+ if (request) {
+ updateTypes.forEach(updateType => {
+ // Only stackTrace will be handled differently
+ if (updateType === "stackTrace") {
+ if (request.cause.stacktraceAvailable && !request.stacktrace) {
+ promises.push(requestData(request.id, updateType));
+ }
+ return;
+ }
+
+ if (request[`${updateType}Available`] && !request[updateType]) {
+ promises.push(requestData(request.id, updateType));
+ }
+ });
+ }
+
+ return Promise.all(promises);
+}
+
+/**
+ * Form a data: URI given a mime type, encoding, and some text.
+ *
+ * @param {string} mimeType - mime type
+ * @param {string} encoding - encoding to use; if not set, the
+ * text will be base64-encoded.
+ * @param {string} text - text of the URI.
+ * @return {string} a data URI
+ */
+function formDataURI(mimeType, encoding, text) {
+ if (!encoding) {
+ encoding = "base64";
+ text = btoa(unescape(encodeURIComponent(text)));
+ }
+ return "data:" + mimeType + ";" + encoding + "," + text;
+}
+
+/**
+ * Write out a list of headers into a chunk of text
+ *
+ * @param {array} headers - array of headers info { name, value }
+ * @param {string} preHeaderText - first line of the headers request/response
+ * @return {string} list of headers in text format
+ */
+function writeHeaderText(headers, preHeaderText) {
+ let result = "";
+ if (preHeaderText) {
+ result += preHeaderText + "\r\n";
+ }
+ result += headers.map(({ name, value }) => name + ": " + value).join("\r\n");
+ result += "\r\n\r\n";
+ return result;
+}
+
+/**
+ * Decode base64 string.
+ *
+ * @param {string} url - a string
+ * @return {string} decoded string
+ */
+function decodeUnicodeBase64(string) {
+ try {
+ return decodeURIComponent(atob(string));
+ } catch (err) {
+ // Ignore error and return input string directly.
+ }
+ return string;
+}
+
+/**
+ * Helper for getting an abbreviated string for a mime type.
+ *
+ * @param {string} mimeType - mime type
+ * @return {string} abbreviated mime type
+ */
+function getAbbreviatedMimeType(mimeType) {
+ if (!mimeType) {
+ return "";
+ }
+ const abbrevType = (mimeType.split(";")[0].split("/")[1] || "").split("+")[0];
+ return CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
+}
+
+/**
+ * Helpers for getting a filename from a mime type.
+ *
+ * @param {string} baseNameWithQuery - unicode basename and query of a url
+ * @return {string} unicode filename portion of a url
+ */
+function getFileName(baseNameWithQuery) {
+ const basename = baseNameWithQuery && baseNameWithQuery.split("?")[0];
+ return basename && basename.includes(".") ? basename : null;
+}
+
+/**
+ * Helpers for retrieving a URL object from a string
+ *
+ * @param {string} url - unvalidated url string
+ * @return {URL} The URL object
+ */
+function getUrl(url) {
+ try {
+ return new URL(url);
+ } catch (err) {
+ return null;
+ }
+}
+
+/**
+ * Helpers for retrieving the value of a URL object property
+ *
+ * @param {string} input - unvalidated url string
+ * @param {string} string - desired property in the URL object
+ * @return {string} unicode query of a url
+ */
+function getUrlProperty(input, property) {
+ const url = getUrl(input);
+ return url?.[property] ? url[property] : "";
+}
+
+/**
+ * Helpers for getting the last portion of a url.
+ * For example helper returns "basename" from http://domain.com/path/basename
+ * If basename portion is empty, it returns the url pathname.
+ *
+ * @param {string} input - unvalidated url string
+ * @return {string} unicode basename of a url
+ */
+function getUrlBaseName(url) {
+ const pathname = getUrlProperty(url, "pathname");
+ return getUnicodeUrlPath(pathname.replace(/\S*\//, "") || pathname || "/");
+}
+
+/**
+ * Helpers for getting the query portion of a url.
+ *
+ * @param {string} url - unvalidated url string
+ * @return {string} unicode query of a url
+ */
+function getUrlQuery(url) {
+ return getUrlProperty(url, "search").replace(/^\?/, "");
+}
+
+/**
+ * Helpers for getting unicode name and query portions of a url.
+ *
+ * @param {string} url - unvalidated url string
+ * @return {string} unicode basename and query portions of a url
+ */
+function getUrlBaseNameWithQuery(url) {
+ const basename = getUrlBaseName(url);
+ const search = getUrlProperty(url, "search");
+ return basename + getUnicodeUrlPath(search);
+}
+
+/**
+ * Helpers for getting hostname portion of an URL.
+ *
+ * @param {string} url - unvalidated url string
+ * @return {string} unicode hostname of a url
+ */
+function getUrlHostName(url) {
+ return getUrlProperty(url, "hostname");
+}
+
+/**
+ * Helpers for getting host portion of an URL.
+ *
+ * @param {string} url - unvalidated url string
+ * @return {string} unicode host of a url
+ */
+function getUrlHost(url) {
+ return getUrlProperty(url, "host");
+}
+
+/**
+ * Helpers for getting the shceme portion of a url.
+ * For example helper returns "http" from http://domain.com/path/basename
+ *
+ * @param {string} url - unvalidated url string
+ * @return {string} string scheme of a url
+ */
+function getUrlScheme(url) {
+ const protocol = getUrlProperty(url, "protocol");
+ return protocol.replace(":", "").toLowerCase();
+}
+
+/**
+ * Extract several details fields from a URL at once.
+ */
+function getUrlDetails(url) {
+ const baseNameWithQuery = getUrlBaseNameWithQuery(url);
+ let host = getUrlHost(url);
+ const hostname = getUrlHostName(url);
+ const unicodeUrl = getUnicodeUrl(url);
+ const scheme = getUrlScheme(url);
+
+ // If the hostname contains unreadable ASCII characters, we need to do the
+ // following two steps:
+ // 1. Converting the unreadable hostname to a readable Unicode domain name.
+ // For example, converting xn--g6w.xn--8pv into a Unicode domain name.
+ // 2. Replacing the unreadable hostname portion in the `host` with the
+ // readable hostname.
+ // For example, replacing xn--g6w.xn--8pv:8000 with [Unicode domain]:8000
+ // After finishing the two steps, we get a readable `host`.
+ const unicodeHostname = getUnicodeHostname(hostname);
+ if (unicodeHostname !== hostname) {
+ host = host.replace(hostname, unicodeHostname);
+ }
+
+ // Mark local hosts specially, where "local" is as defined in the W3C
+ // spec for secure contexts.
+ // http://www.w3.org/TR/powerful-features/
+ //
+ // * If the name falls under 'localhost'
+ // * If the name is an IPv4 address within 127.0.0.0/8
+ // * If the name is an IPv6 address within ::1/128
+ //
+ // IPv6 parsing is a little sloppy; it assumes that the address has
+ // been validated before it gets here.
+ const isLocal =
+ hostname.match(/(.+\.)?localhost$/) ||
+ hostname.match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/) ||
+ hostname.match(/\[[0:]+1\]/);
+
+ return {
+ baseNameWithQuery,
+ host,
+ scheme,
+ unicodeUrl,
+ isLocal,
+ url,
+ };
+}
+
+/**
+ * Parse a url's query string into its components
+ *
+ * @param {string} query - query string of a url portion
+ * @return {array} array of query params { name, value }
+ */
+function parseQueryString(query) {
+ if (!query) {
+ return null;
+ }
+ return query
+ .replace(/^[?&]/, "")
+ .split("&")
+ .map(e => {
+ const param = e.split("=");
+ return {
+ name: param[0] ? getUnicodeUrlPath(param[0].replace(/\+/g, " ")) : "",
+ value: param[1]
+ ? getUnicodeUrlPath(param.slice(1).join("=").replace(/\+/g, " "))
+ : "",
+ };
+ });
+}
+
+/**
+ * Parse a string of formdata sections into its components
+ *
+ * @param {string} sections - sections of formdata joined by &
+ * @return {array} array of formdata params { name, value }
+ */
+function parseFormData(sections) {
+ if (!sections) {
+ return [];
+ }
+
+ return sections
+ .replace(/^&/, "")
+ .split("&")
+ .map(e => {
+ const param = e.split("=");
+ return {
+ name: param[0] ? getUnicodeUrlPath(param[0]) : "",
+ value: param[1] ? getUnicodeUrlPath(param[1]) : "",
+ };
+ });
+}
+
+/**
+ * Reduces an IP address into a number for easier sorting
+ *
+ * @param {string} ip - IP address to reduce
+ * @return {number} the number representing the IP address
+ */
+function ipToLong(ip) {
+ if (!ip) {
+ // Invalid IP
+ return -1;
+ }
+
+ let base;
+ let octets = ip.split(".");
+
+ if (octets.length === 4) {
+ // IPv4
+ base = 10;
+ } else if (ip.includes(":")) {
+ // IPv6
+ const numberOfZeroSections =
+ 8 - ip.replace(/^:+|:+$/g, "").split(/:+/g).length;
+ octets = ip
+ .replace("::", `:${"0:".repeat(numberOfZeroSections)}`)
+ .replace(/^:|:$/g, "")
+ .split(":");
+ base = 16;
+ } else {
+ // Invalid IP
+ return -1;
+ }
+ return octets
+ .map((val, ix, arr) => {
+ return parseInt(val, base) * Math.pow(256, arr.length - 1 - ix);
+ })
+ .reduce((sum, val) => {
+ return sum + val;
+ }, 0);
+}
+
+/**
+ * Compare two objects on a subset of their properties
+ */
+function propertiesEqual(props, item1, item2) {
+ return item1 === item2 || props.every(p => item1[p] === item2[p]);
+}
+
+/**
+ * Calculate the start time of a request, which is the time from start
+ * of 1st request until the start of this request.
+ *
+ * Without a firstRequestStartedMs argument the wrong time will be returned.
+ * However, it can be omitted when comparing two start times and neither supplies
+ * a firstRequestStartedMs.
+ */
+function getStartTime(item, firstRequestStartedMs = 0) {
+ return item.startedMs - firstRequestStartedMs;
+}
+
+/**
+ * Calculate the end time of a request, which is the time from start
+ * of 1st request until the end of this response.
+ *
+ * Without a firstRequestStartedMs argument the wrong time will be returned.
+ * However, it can be omitted when comparing two end times and neither supplies
+ * a firstRequestStartedMs.
+ */
+function getEndTime(item, firstRequestStartedMs = 0) {
+ const { startedMs, totalTime } = item;
+ return startedMs + totalTime - firstRequestStartedMs;
+}
+
+/**
+ * Calculate the response time of a request, which is the time from start
+ * of 1st request until the beginning of download of this response.
+ *
+ * Without a firstRequestStartedMs argument the wrong time will be returned.
+ * However, it can be omitted when comparing two response times and neither supplies
+ * a firstRequestStartedMs.
+ */
+function getResponseTime(item, firstRequestStartedMs = 0) {
+ const { startedMs, totalTime, eventTimings = { timings: {} } } = item;
+ return (
+ startedMs + totalTime - firstRequestStartedMs - eventTimings.timings.receive
+ );
+}
+
+/**
+ * Format the protocols used by the request.
+ */
+function getFormattedProtocol(item) {
+ const { httpVersion = "", responseHeaders = { headers: [] } } = item;
+ const protocol = [httpVersion];
+ responseHeaders.headers.some(h => {
+ if (h.hasOwnProperty("name") && h.name.toLowerCase() === "x-firefox-spdy") {
+ /**
+ * First we make sure h.value is defined and not an empty string.
+ * Then check that HTTP version and x-firefox-spdy == "http/1.1".
+ * If not, check that HTTP version and x-firefox-spdy have the same
+ * numeric value when of the forms "http/<x>" and "h<x>" respectively.
+ * If not, will push to protocol the non-standard x-firefox-spdy value.
+ *
+ * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1501357
+ */
+ if (h.value !== undefined && h.value.length) {
+ if (
+ h.value.toLowerCase() !== "http/1.1" ||
+ protocol[0].toLowerCase() !== "http/1.1"
+ ) {
+ if (
+ parseFloat(h.value.toLowerCase().split("")[1]) !==
+ parseFloat(protocol[0].toLowerCase().split("/")[1])
+ ) {
+ protocol.push(h.value);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ });
+ return protocol.join("+");
+}
+
+/**
+ * Get the value of a particular response header, or null if not
+ * present.
+ */
+function getResponseHeader(item, header) {
+ const { responseHeaders } = item;
+ if (!responseHeaders || !responseHeaders.headers.length) {
+ return null;
+ }
+ header = header.toLowerCase();
+ for (const responseHeader of responseHeaders.headers) {
+ if (responseHeader.name.toLowerCase() == header) {
+ return responseHeader.value;
+ }
+ }
+ return null;
+}
+
+/**
+ * Get the value of a particular request header, or null if not
+ * present.
+ */
+function getRequestHeader(item, header) {
+ const { requestHeaders } = item;
+ if (!requestHeaders || !requestHeaders.headers.length) {
+ return null;
+ }
+ header = header.toLowerCase();
+ for (const requestHeader of requestHeaders.headers) {
+ if (requestHeader.name.toLowerCase() == header) {
+ return requestHeader.value;
+ }
+ }
+ return null;
+}
+
+/**
+ * Extracts any urlencoded form data sections from a POST request.
+ */
+async function updateFormDataSections(props) {
+ const { connector, request = {}, updateRequest } = props;
+ let {
+ id,
+ formDataSections,
+ requestHeaders,
+ requestHeadersAvailable,
+ requestHeadersFromUploadStream,
+ requestPostData,
+ requestPostDataAvailable,
+ } = request;
+
+ if (requestHeadersAvailable && !requestHeaders) {
+ requestHeaders = await connector.requestData(id, "requestHeaders");
+ }
+
+ if (requestPostDataAvailable && !requestPostData) {
+ requestPostData = await connector.requestData(id, "requestPostData");
+ }
+
+ if (
+ !formDataSections &&
+ requestHeaders &&
+ requestPostData &&
+ requestHeadersFromUploadStream
+ ) {
+ formDataSections = await getFormDataSections(
+ requestHeaders,
+ requestHeadersFromUploadStream,
+ requestPostData,
+ connector.getLongString
+ );
+
+ updateRequest(request.id, { formDataSections }, true);
+ }
+}
+
+/**
+ * This helper function helps to resolve the full payload of a message
+ * that is wrapped in a LongStringActor object.
+ */
+async function getMessagePayload(payload, getLongString) {
+ const result = await getLongString(payload);
+ return result;
+}
+
+/**
+ * This helper function is used for additional processing of
+ * incoming network update packets. It makes sure the only valid
+ * update properties and the values are correct.
+ * It's used by Network and Console panel reducers.
+ * @param {object} update
+ * The new update payload
+ * @param {object} request
+ * The current request in the state
+ */
+function processNetworkUpdates(update) {
+ const newRequest = {};
+ for (const [key, value] of Object.entries(update)) {
+ if (UPDATE_PROPS.includes(key)) {
+ newRequest[key] = value;
+ if (key == "requestPostData") {
+ newRequest.requestHeadersFromUploadStream = value.uploadHeaders;
+ }
+ }
+ }
+ return newRequest;
+}
+
+/**
+ * This method checks that the response is base64 encoded by
+ * comparing these 2 values:
+ * 1. The original response
+ * 2. The value of doing a base64 decode on the
+ * response and then base64 encoding the result.
+ * If the values are different or an error is thrown,
+ * the method will return false.
+ */
+function isBase64(payload) {
+ try {
+ return btoa(atob(payload)) == payload;
+ } catch (err) {
+ return false;
+ }
+}
+
+/**
+ * Checks if the payload is of JSON type.
+ * This function also handles JSON with XSSI-escaping characters by stripping them
+ * and returning the stripped chars in the strippedChars property
+ * This function also handles Base64 encoded JSON.
+ * @returns {Object} shape:
+ * {Object} json: parsed JSON object
+ * {Error} error: JSON parsing error
+ * {string} strippedChars: XSSI stripped chars removed from JSON payload
+ */
+function parseJSON(payloadUnclean) {
+ let json;
+ const jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+ const [, jsonpCallback, jsonp] = payloadUnclean.match(jsonpRegex) || [];
+ if (jsonpCallback && jsonp) {
+ let error;
+ try {
+ json = parseJSON(jsonp).json;
+ } catch (err) {
+ error = err;
+ }
+ return { json, error, jsonpCallback };
+ }
+
+ let { payload, strippedChars, error } = removeXSSIString(payloadUnclean);
+
+ try {
+ json = JSON.parse(payload);
+ } catch (err) {
+ if (isBase64(payload)) {
+ try {
+ json = JSON.parse(atob(payload));
+ } catch (err64) {
+ error = err64;
+ }
+ } else {
+ error = err;
+ }
+ }
+
+ // Do not present JSON primitives (e.g. boolean, strings in quotes, numbers)
+ // as JSON expandable tree.
+ if (!error) {
+ if (typeof json !== "object") {
+ return {};
+ }
+ }
+ return {
+ json,
+ error,
+ strippedChars,
+ };
+}
+
+/**
+ * Removes XSSI prevention sequences from JSON payloads
+ * @param {string} payloadUnclean: JSON payload that may or may have a
+ * XSSI prevention sequence
+ * @returns {Object} Shape:
+ * {string} payload: the JSON witht the XSSI prevention sequence removed
+ * {string} strippedChars: XSSI string that was removed, null if no XSSI
+ * prevention sequence was found
+ * {Error} error: error attempting to strip XSSI prevention sequence
+ */
+function removeXSSIString(payloadUnclean) {
+ // Regex that finds the XSSI protection sequences )]}'\n for(;;); and while(1);
+ const xssiRegex = /(^\)\]\}',?\n)|(^for ?\(;;\);?)|(^while ?\(1\);?)/;
+ let payload, strippedChars, error;
+ const xssiRegexMatch = payloadUnclean.match(xssiRegex);
+
+ // Remove XSSI string if there was one found
+ if (xssiRegexMatch?.length > 0) {
+ const xssiLen = xssiRegexMatch[0].length;
+ try {
+ // substring the payload by the length of the XSSI match to remove it
+ // and save the match to report
+ payload = payloadUnclean.substring(xssiLen);
+ strippedChars = xssiRegexMatch[0];
+ } catch (err) {
+ error = err;
+ payload = payloadUnclean;
+ }
+ } else {
+ // if there was no XSSI match just return the raw payload
+ payload = payloadUnclean;
+ }
+ return {
+ payload,
+ strippedChars,
+ error,
+ };
+}
+
+/**
+ * Computes the request headers of an HTTP request
+ *
+ * @param {string} method: request method
+ * @param {string} httpVersion: request http version
+ * @param {object} requestHeaders: request headers
+ * @param {object} urlDetails: request url details
+ *
+ * @return {string} the request headers
+ */
+function getRequestHeadersRawText(
+ method,
+ httpVersion,
+ requestHeaders,
+ urlDetails
+) {
+ const url = new URL(urlDetails.url);
+ const path = url ? `${url.pathname}${url.search}` : "<unknown>";
+ const preHeaderText = `${method} ${path} ${httpVersion}`;
+ return writeHeaderText(requestHeaders.headers, preHeaderText).trim();
+}
+
+module.exports = {
+ decodeUnicodeBase64,
+ getFormDataSections,
+ fetchHeaders,
+ fetchNetworkUpdatePacket,
+ formDataURI,
+ writeHeaderText,
+ getAbbreviatedMimeType,
+ getFileName,
+ getEndTime,
+ getFormattedProtocol,
+ getMessagePayload,
+ getRequestHeader,
+ getResponseHeader,
+ getResponseTime,
+ getStartTime,
+ getUrlBaseName,
+ getUrlBaseNameWithQuery,
+ getUrlDetails,
+ getUrlHost,
+ getUrlHostName,
+ getUrlQuery,
+ getUrlScheme,
+ parseQueryString,
+ parseFormData,
+ updateFormDataSections,
+ processNetworkUpdates,
+ propertiesEqual,
+ ipToLong,
+ parseJSON,
+ getRequestHeadersRawText,
+};
diff --git a/devtools/client/netmonitor/src/utils/sort-predicates.js b/devtools/client/netmonitor/src/utils/sort-predicates.js
new file mode 100644
index 0000000000..839a37d00e
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/sort-predicates.js
@@ -0,0 +1,319 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ getAbbreviatedMimeType,
+ getEndTime,
+ getResponseTime,
+ getResponseHeader,
+ getStartTime,
+ ipToLong,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+const {
+ RESPONSE_HEADERS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const {
+ getUrlBaseName,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+/**
+ * Predicates used when sorting items.
+ *
+ * @param object first
+ * The first item used in the comparison.
+ * @param object second
+ * The second item used in the comparison.
+ * @return number
+ * <0 to sort first to a lower index than second
+ * =0 to leave first and second unchanged with respect to each other
+ * >0 to sort second to a lower index than first
+ */
+
+function compareValues(first, second) {
+ if (first === second) {
+ return 0;
+ }
+ return first > second ? 1 : -1;
+}
+
+function waterfall(first, second) {
+ const result = compareValues(first.startedMs, second.startedMs);
+ return result || compareValues(first.id, second.id);
+}
+
+function priority(first, second) {
+ const result = compareValues(first.priority, second.priority);
+ return result || waterfall(first, second);
+}
+
+function status(first, second) {
+ const result = compareValues(getStatusValue(first), getStatusValue(second));
+ return result || waterfall(first, second);
+}
+
+function method(first, second) {
+ const result = compareValues(first.method, second.method);
+ return result || waterfall(first, second);
+}
+
+function file(first, second) {
+ const firstUrl = first.urlDetails.baseNameWithQuery.toLowerCase();
+ const secondUrl = second.urlDetails.baseNameWithQuery.toLowerCase();
+ const result = compareValues(firstUrl, secondUrl);
+ return result || waterfall(first, second);
+}
+
+function url(first, second) {
+ const firstUrl = first.url.toLowerCase();
+ const secondUrl = second.url.toLowerCase();
+ const result = compareValues(firstUrl, secondUrl);
+ return result || waterfall(first, second);
+}
+
+function protocol(first, second) {
+ const result = compareValues(first.httpVersion, second.httpVersion);
+ return result || waterfall(first, second);
+}
+
+function scheme(first, second) {
+ const result = compareValues(
+ first.urlDetails.scheme,
+ second.urlDetails.scheme
+ );
+ return result || waterfall(first, second);
+}
+
+function startTime(first, second) {
+ const result = compareValues(getStartTime(first), getStartTime(second));
+ return result || waterfall(first, second);
+}
+
+function endTime(first, second) {
+ const result = compareValues(getEndTime(first), getEndTime(second));
+ return result || waterfall(first, second);
+}
+
+function responseTime(first, second) {
+ const result = compareValues(getResponseTime(first), getResponseTime(second));
+ return result || waterfall(first, second);
+}
+
+function duration(first, second) {
+ const result = compareValues(first.totalTime, second.totalTime);
+ return result || waterfall(first, second);
+}
+
+function latency(first, second) {
+ const { eventTimings: firstEventTimings = { timings: {} } } = first;
+ const { eventTimings: secondEventTimings = { timings: {} } } = second;
+ const result = compareValues(
+ firstEventTimings.timings.wait,
+ secondEventTimings.timings.wait
+ );
+ return result || waterfall(first, second);
+}
+
+function compareHeader(header, first, second) {
+ const firstValue = getResponseHeader(first, header) || "";
+ const secondValue = getResponseHeader(second, header) || "";
+
+ let result;
+
+ switch (header) {
+ case "Content-Length": {
+ result = compareValues(
+ parseInt(firstValue, 10) || 0,
+ parseInt(secondValue, 10) || 0
+ );
+ break;
+ }
+ case "Last-Modified": {
+ result = compareValues(
+ new Date(firstValue).valueOf() || -1,
+ new Date(secondValue).valueOf() || -1
+ );
+ break;
+ }
+ default: {
+ result = compareValues(firstValue, secondValue);
+ break;
+ }
+ }
+
+ return result || waterfall(first, second);
+}
+
+const responseHeaders = RESPONSE_HEADERS.reduce(
+ (acc, header) =>
+ Object.assign(acc, {
+ [header]: (first, second) => compareHeader(header, first, second),
+ }),
+ {}
+);
+
+function domain(first, second) {
+ const firstDomain = first.urlDetails.host.toLowerCase();
+ const secondDomain = second.urlDetails.host.toLowerCase();
+ const result = compareValues(firstDomain, secondDomain);
+ return result || waterfall(first, second);
+}
+
+function remoteip(first, second) {
+ const firstIP = ipToLong(first.remoteAddress);
+ const secondIP = ipToLong(second.remoteAddress);
+ const result = compareValues(firstIP, secondIP);
+ return result || waterfall(first, second);
+}
+
+function cause(first, second) {
+ const firstCause = first.cause.type;
+ const secondCause = second.cause.type;
+ const result = compareValues(firstCause, secondCause);
+ return result || waterfall(first, second);
+}
+
+function initiator(first, second) {
+ const firstCause = first.cause.type;
+ const secondCause = second.cause.type;
+
+ let firstInitiator = "";
+ let firstInitiatorLineNumber = 0;
+
+ if (first.cause.lastFrame) {
+ firstInitiator = getUrlBaseName(first.cause.lastFrame.filename);
+ firstInitiatorLineNumber = first.cause.lastFrame.lineNumber;
+ }
+
+ let secondInitiator = "";
+ let secondInitiatorLineNumber = 0;
+
+ if (second.cause.lastFrame) {
+ secondInitiator = getUrlBaseName(second.cause.lastFrame.filename);
+ secondInitiatorLineNumber = second.cause.lastFrame.lineNumber;
+ }
+
+ let result;
+ // if both initiators don't have a stack trace, compare their causes
+ if (!firstInitiator && !secondInitiator) {
+ result = compareValues(firstCause, secondCause);
+ } else if (!firstInitiator || !secondInitiator) {
+ // if one initiator doesn't have a stack trace but the other does, former should precede the latter
+ result = compareValues(firstInitiatorLineNumber, secondInitiatorLineNumber);
+ } else {
+ result = compareValues(firstInitiator, secondInitiator);
+ if (result === 0) {
+ result = compareValues(
+ firstInitiatorLineNumber,
+ secondInitiatorLineNumber
+ );
+ }
+ }
+
+ return result || waterfall(first, second);
+}
+
+function setCookies(first, second) {
+ let { responseCookies: firstResponseCookies = { cookies: [] } } = first;
+ let { responseCookies: secondResponseCookies = { cookies: [] } } = second;
+ firstResponseCookies = firstResponseCookies.cookies || firstResponseCookies;
+ secondResponseCookies =
+ secondResponseCookies.cookies || secondResponseCookies;
+ const result = compareValues(
+ firstResponseCookies.length,
+ secondResponseCookies.length
+ );
+ return result || waterfall(first, second);
+}
+
+function cookies(first, second) {
+ let { requestCookies: firstRequestCookies = { cookies: [] } } = first;
+ let { requestCookies: secondRequestCookies = { cookies: [] } } = second;
+ firstRequestCookies = firstRequestCookies.cookies || firstRequestCookies;
+ secondRequestCookies = secondRequestCookies.cookies || secondRequestCookies;
+ const result = compareValues(
+ firstRequestCookies.length,
+ secondRequestCookies.length
+ );
+ return result || waterfall(first, second);
+}
+
+function type(first, second) {
+ const firstType = getAbbreviatedMimeType(first.mimeType).toLowerCase();
+ const secondType = getAbbreviatedMimeType(second.mimeType).toLowerCase();
+ const result = compareValues(firstType, secondType);
+ return result || waterfall(first, second);
+}
+
+function getStatusValue(item) {
+ let value;
+ if (item.blockedReason) {
+ value = typeof item.blockedReason == "number" ? -item.blockedReason : -1000;
+ } else if (item.status == null) {
+ value = -2;
+ } else {
+ value = item.status;
+ }
+ return value;
+}
+
+function getTransferedSizeValue(item) {
+ let value;
+ if (item.blockedReason) {
+ // Also properly group/sort various blocked reasons.
+ value = typeof item.blockedReason == "number" ? -item.blockedReason : -1000;
+ } else if (item.fromCache || item.status === "304") {
+ value = -2;
+ } else if (item.fromServiceWorker) {
+ value = -3;
+ } else if (typeof item.transferredSize == "number") {
+ value = item.transferredSize;
+ if (item.isRacing && typeof item.isRacing == "boolean") {
+ value = -4;
+ }
+ } else if (item.transferredSize === null) {
+ value = -5;
+ }
+ return value;
+}
+
+function transferred(first, second) {
+ const result = compareValues(
+ getTransferedSizeValue(first),
+ getTransferedSizeValue(second)
+ );
+ return result || waterfall(first, second);
+}
+
+function contentSize(first, second) {
+ const result = compareValues(first.contentSize, second.contentSize);
+ return result || waterfall(first, second);
+}
+
+const sorters = {
+ status,
+ method,
+ domain,
+ file,
+ protocol,
+ scheme,
+ cookies,
+ setCookies,
+ remoteip,
+ cause,
+ initiator,
+ type,
+ transferred,
+ contentSize,
+ startTime,
+ endTime,
+ responseTime,
+ duration,
+ latency,
+ waterfall,
+ url,
+ priority,
+};
+exports.Sorters = Object.assign(sorters, responseHeaders);
diff --git a/devtools/client/netmonitor/src/utils/sort-utils.js b/devtools/client/netmonitor/src/utils/sort-utils.js
new file mode 100644
index 0000000000..a7f49417e8
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/sort-utils.js
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Sorts object by keys in alphabetical order
+ * If object has nested children, it sorts the child-elements also by keys
+ * @param {object} which should be sorted by keys in alphabetical order
+ */
+function sortObjectKeys(object) {
+ if (object == null) {
+ return null;
+ }
+
+ if (Array.isArray(object)) {
+ for (let i = 0; i < object.length; i++) {
+ if (typeof object[i] === "object") {
+ object[i] = sortObjectKeys(object[i]);
+ }
+ }
+ return object;
+ }
+
+ return Object.keys(object)
+ .sort(function (left, right) {
+ return left.toLowerCase().localeCompare(right.toLowerCase());
+ })
+ .reduce((acc, key) => {
+ if (typeof object[key] === "object") {
+ acc[key] = sortObjectKeys(object[key]);
+ } else {
+ acc[key] = object[key];
+ }
+ return acc;
+ }, Object.create(null));
+}
+
+module.exports = {
+ sortObjectKeys,
+};
diff --git a/devtools/client/netmonitor/src/utils/tooltips.js b/devtools/client/netmonitor/src/utils/tooltips.js
new file mode 100644
index 0000000000..1f35346353
--- /dev/null
+++ b/devtools/client/netmonitor/src/utils/tooltips.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Returns first 128 characters of value for use as a tooltip.
+ * @param object
+ * @returns {*}
+ */
+function limitTooltipLength(object) {
+ return object.length > 128 ? object.substring(0, 128) + "…" : object;
+}
+
+module.exports = {
+ limitTooltipLength,
+};