summaryrefslogtreecommitdiffstats
path: root/devtools/shared/network-observer/NetworkUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/network-observer/NetworkUtils.sys.mjs')
-rw-r--r--devtools/shared/network-observer/NetworkUtils.sys.mjs481
1 files changed, 481 insertions, 0 deletions
diff --git a/devtools/shared/network-observer/NetworkUtils.sys.mjs b/devtools/shared/network-observer/NetworkUtils.sys.mjs
new file mode 100644
index 0000000000..2ecb760718
--- /dev/null
+++ b/devtools/shared/network-observer/NetworkUtils.sys.mjs
@@ -0,0 +1,481 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "tpFlagsMask", () => {
+ const trackingProtectionLevel2Enabled = Services.prefs
+ .getStringPref("urlclassifier.trackingTable")
+ .includes("content-track-digest256");
+
+ return trackingProtectionLevel2Enabled
+ ? ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
+ ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING
+ : ~Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING &
+ Ci.nsIClassifiedChannel.CLASSIFIED_ANY_STRICT_TRACKING;
+});
+
+/**
+ * Convert a nsIContentPolicy constant to a display string
+ */
+const LOAD_CAUSE_STRINGS = {
+ [Ci.nsIContentPolicy.TYPE_INVALID]: "invalid",
+ [Ci.nsIContentPolicy.TYPE_OTHER]: "other",
+ [Ci.nsIContentPolicy.TYPE_SCRIPT]: "script",
+ [Ci.nsIContentPolicy.TYPE_IMAGE]: "img",
+ [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "stylesheet",
+ [Ci.nsIContentPolicy.TYPE_OBJECT]: "object",
+ [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "document",
+ [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "subdocument",
+ [Ci.nsIContentPolicy.TYPE_PING]: "ping",
+ [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "xhr",
+ [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "objectSubdoc",
+ [Ci.nsIContentPolicy.TYPE_DTD]: "dtd",
+ [Ci.nsIContentPolicy.TYPE_FONT]: "font",
+ [Ci.nsIContentPolicy.TYPE_MEDIA]: "media",
+ [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "websocket",
+ [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "csp",
+ [Ci.nsIContentPolicy.TYPE_XSLT]: "xslt",
+ [Ci.nsIContentPolicy.TYPE_BEACON]: "beacon",
+ [Ci.nsIContentPolicy.TYPE_FETCH]: "fetch",
+ [Ci.nsIContentPolicy.TYPE_IMAGESET]: "imageset",
+ [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "webManifest",
+ [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "webidentity",
+};
+
+function causeTypeToString(causeType, loadFlags, internalContentPolicyType) {
+ let prefix = "";
+ if (
+ (causeType == Ci.nsIContentPolicy.TYPE_IMAGESET ||
+ internalContentPolicyType == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE) &&
+ loadFlags & Ci.nsIRequest.LOAD_BACKGROUND
+ ) {
+ prefix = "lazy-";
+ }
+
+ return prefix + LOAD_CAUSE_STRINGS[causeType] || "unknown";
+}
+
+function stringToCauseType(value) {
+ return Object.keys(LOAD_CAUSE_STRINGS).find(
+ key => LOAD_CAUSE_STRINGS[key] === value
+ );
+}
+
+function isChannelFromSystemPrincipal(channel) {
+ let principal = null;
+ let browsingContext = channel.loadInfo.browsingContext;
+ if (!browsingContext) {
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ if (topFrame) {
+ browsingContext = topFrame.browsingContext;
+ } else {
+ // Fallback to the triggering principal when browsingContext and topFrame is null
+ // e.g some chrome requests
+ principal = channel.loadInfo.triggeringPrincipal;
+ }
+ }
+
+ // When in the parent process, we can get the documentPrincipal from the
+ // WindowGlobal which is available on the BrowsingContext
+ if (!principal) {
+ principal = CanonicalBrowsingContext.isInstance(browsingContext)
+ ? browsingContext.currentWindowGlobal.documentPrincipal
+ : browsingContext.window.document.nodePrincipal;
+ }
+ return principal.isSystemPrincipal;
+}
+
+/**
+ * Get the browsing context id for the channel.
+ *
+ * @param {*} channel
+ * @returns {number}
+ */
+function getChannelBrowsingContextID(channel) {
+ if (channel.loadInfo.browsingContextID) {
+ return channel.loadInfo.browsingContextID;
+ }
+ // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
+ // We fallback on top frame element, which works, but will be wrong for WebSocket
+ // in same-process iframes...
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // topFrame is typically null for some chrome requests like favicons
+ if (topFrame && topFrame.browsingContext) {
+ return topFrame.browsingContext.id;
+ }
+ return null;
+}
+
+/**
+ * Get the innerWindowId for the channel.
+ *
+ * @param {*} channel
+ * @returns {number}
+ */
+function getChannelInnerWindowId(channel) {
+ if (channel.loadInfo.innerWindowID) {
+ return channel.loadInfo.innerWindowID;
+ }
+ // At least WebSocket channel aren't having a browsingContextID set on their loadInfo
+ // We fallback on top frame element, which works, but will be wrong for WebSocket
+ // in same-process iframes...
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // topFrame is typically null for some chrome requests like favicons
+ if (topFrame?.browsingContext?.currentWindowGlobal) {
+ return topFrame.browsingContext.currentWindowGlobal.innerWindowId;
+ }
+ return null;
+}
+
+/**
+ * Does this channel represent a Preload request.
+ *
+ * @param {*} channel
+ * @returns {boolean}
+ */
+function isPreloadRequest(channel) {
+ const type = channel.loadInfo.internalContentPolicyType;
+ return (
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_MODULE_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET_PRELOAD ||
+ type == Ci.nsIContentPolicy.TYPE_INTERNAL_FONT_PRELOAD
+ );
+}
+
+/**
+ * Creates a network event based on the channel.
+ *
+ * @param {*} channel
+ * @return {Object} event - The network event
+ */
+function createNetworkEvent(
+ channel,
+ {
+ timestamp,
+ fromCache,
+ fromServiceWorker,
+ extraStringData,
+ blockedReason,
+ blockingExtension = null,
+ saveRequestAndResponseBodies = false,
+ }
+) {
+ channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+
+ const event = {};
+ event.method = channel.requestMethod;
+ event.channelId = channel.channelId;
+ event.isFromSystemPrincipal = isChannelFromSystemPrincipal(channel);
+ event.browsingContextID = this.getChannelBrowsingContextID(channel);
+ event.innerWindowId = this.getChannelInnerWindowId(channel);
+ event.url = channel.URI.spec;
+ event.private = channel.isChannelPrivate;
+ event.headersSize = extraStringData ? extraStringData.length : 0;
+ event.startedDateTime = (timestamp
+ ? new Date(Math.round(timestamp / 1000))
+ : new Date()
+ ).toISOString();
+ event.fromCache = fromCache;
+ event.fromServiceWorker = fromServiceWorker;
+ // Only consider channels classified as level-1 to be trackers if our preferences
+ // would not cause such channels to be blocked in strict content blocking mode.
+ // Make sure the value produced here is a boolean.
+ if (channel instanceof Ci.nsIClassifiedChannel) {
+ event.isThirdPartyTrackingResource = !!(
+ channel.isThirdPartyTrackingResource() &&
+ (channel.thirdPartyClassificationFlags & lazy.tpFlagsMask) == 0
+ );
+ }
+ const referrerInfo = channel.referrerInfo;
+ event.referrerPolicy = referrerInfo
+ ? referrerInfo.getReferrerPolicyString()
+ : "";
+
+ if (channel instanceof Ci.nsISupportsPriority) {
+ event.priority = channel.priority;
+ }
+
+ // Determine the cause and if this is an XHR request.
+ let causeType = Ci.nsIContentPolicy.TYPE_OTHER;
+ let causeUri = null;
+
+ if (channel.loadInfo) {
+ causeType = channel.loadInfo.externalContentPolicyType;
+ const { loadingPrincipal } = channel.loadInfo;
+ if (loadingPrincipal) {
+ causeUri = loadingPrincipal.spec;
+ }
+ }
+
+ // Show the right WebSocket URL in case of WS channel.
+ if (channel.notificationCallbacks) {
+ let wsChannel = null;
+ try {
+ wsChannel = channel.notificationCallbacks.QueryInterface(
+ Ci.nsIWebSocketChannel
+ );
+ } catch (e) {
+ // Not all channels implement nsIWebSocketChannel.
+ }
+ if (wsChannel) {
+ event.url = wsChannel.URI.spec;
+ event.serial = wsChannel.serial;
+ }
+ }
+
+ event.cause = {
+ type: this.causeTypeToString(
+ causeType,
+ channel.loadFlags,
+ channel.loadInfo.internalContentPolicyType
+ ),
+ loadingDocumentUri: causeUri,
+ stacktrace: undefined,
+ };
+
+ event.isXHR =
+ causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
+ causeType === Ci.nsIContentPolicy.TYPE_FETCH;
+
+ // Determine the HTTP version.
+ const httpVersionMaj = {};
+ const httpVersionMin = {};
+
+ channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ channel.getRequestVersion(httpVersionMaj, httpVersionMin);
+
+ event.httpVersion =
+ "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value;
+
+ event.discardRequestBody = !saveRequestAndResponseBodies;
+ event.discardResponseBody = !saveRequestAndResponseBodies;
+
+ if (blockedReason) {
+ event.blockedReason = blockedReason;
+ if (blockingExtension) {
+ event.blockingExtension = blockingExtension;
+ }
+ } else if (blockedReason !== undefined) {
+ // We were definitely blocked, but the blocker didn't say why.
+ event.blockedReason = "unknown";
+ }
+
+ // isNavigationRequest is true for the one request used to load a new top level document
+ // of a given tab, or top level window. It will typically be false for navigation requests
+ // of iframes, i.e. the request loading another document in an iframe.
+ event.isNavigationRequest =
+ channel.isMainDocumentChannel && channel.loadInfo.isTopLevelLoad;
+
+ return event;
+}
+
+/**
+ * For a given channel, with its associated http activity object,
+ * fetch the request's headers and cookies.
+ * This data is passed to the owner, i.e. the NetworkEventActor,
+ * so that the frontend can later fetch it via getRequestHeaders/getRequestCookies.
+ *
+ * @param {*} channel
+ * @return {Object}
+ * An object with two properties:
+ * @property {Array<Object>} cookies
+ * Array of { name, value } objects.
+ * @property {Array<Object>} headers
+ * Array of { name, value } objects.
+ */
+function fetchRequestHeadersAndCookies(channel) {
+ const headers = [];
+ let cookies = [];
+ let cookieHeader = null;
+
+ // Copy the request header data.
+ channel.visitRequestHeaders({
+ visitHeader(name, value) {
+ if (name == "Cookie") {
+ cookieHeader = value;
+ }
+ headers.push({ name, value });
+ },
+ });
+
+ if (cookieHeader) {
+ cookies = lazy.NetworkHelper.parseCookieHeader(cookieHeader);
+ }
+
+ return { cookies, headers };
+}
+
+/**
+ * Check if a given network request should be logged by a network monitor
+ * based on the specified filters.
+ *
+ * @param nsIHttpChannel channel
+ * Request to check.
+ * @param filters
+ * NetworkObserver filters to match against. An object with one of the following attributes:
+ * - sessionContext: When inspecting requests from the parent process, pass the WatcherActor's session context.
+ * This helps know what is the overall debugged scope.
+ * See watcher actor constructor for more info.
+ * - targetActor: When inspecting requests from the content process, pass the WindowGlobalTargetActor.
+ * This helps know what exact subset of request we should accept.
+ * This is especially useful to behave correctly regarding EFT, where we should include or not
+ * iframes requests.
+ * - browserId, addonId, window: All these attributes are legacy.
+ * Only browserId attribute is still used by the legacy WebConsoleActor startListener API.
+ * @return boolean
+ * True if the network request should be logged, false otherwise.
+ */
+function matchRequest(channel, filters) {
+ // NetworkEventWatcher should now pass a session context for the parent process codepath
+ if (filters.sessionContext) {
+ const { type } = filters.sessionContext;
+ if (type == "all") {
+ return true;
+ }
+
+ // Ignore requests from chrome or add-on code when we don't monitor the whole browser
+ if (
+ channel.loadInfo?.loadingDocument === null &&
+ (channel.loadInfo.loadingPrincipal ===
+ Services.scriptSecurityManager.getSystemPrincipal() ||
+ channel.loadInfo.isInDevToolsContext)
+ ) {
+ return false;
+ }
+
+ if (type == "browser-element") {
+ if (!channel.loadInfo.browsingContext) {
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // `topFrame` is typically null for some chrome requests like favicons
+ // And its `browsingContext` attribute might be null if the request happened
+ // while the tab is being closed.
+ return (
+ topFrame?.browsingContext?.browserId ==
+ filters.sessionContext.browserId
+ );
+ }
+ return (
+ channel.loadInfo.browsingContext.browserId ==
+ filters.sessionContext.browserId
+ );
+ }
+ if (type == "webextension") {
+ return (
+ channel?.loadInfo.loadingPrincipal.addonId ===
+ filters.sessionContext.addonId
+ );
+ }
+ throw new Error("Unsupported session context type: " + type);
+ }
+
+ // NetworkEventContentWatcher and NetworkEventStackTraces pass a target actor instead, from the content processes
+ // Because of EFT, we can't use session context as we have to know what exact windows the target actor covers.
+ if (filters.targetActor) {
+ // Bug 1769982 the target actor might be destroying and accessing windows will throw.
+ // Ignore all further request when this happens.
+ let windows;
+ try {
+ windows = filters.targetActor.windows;
+ } catch (e) {
+ return false;
+ }
+ const win = lazy.NetworkHelper.getWindowForRequest(channel);
+ return windows.includes(win);
+ }
+
+ // This is fallback code for the legacy WebConsole.startListeners codepath,
+ // which may still pass individual browserId/window/addonId attributes.
+ // This should be removable once we drop the WebConsole codepath for network events
+ // (bug 1721592 and followups)
+ return legacyMatchRequest(channel, filters);
+}
+
+function legacyMatchRequest(channel, filters) {
+ // Log everything if no filter is specified
+ if (!filters.browserId && !filters.window && !filters.addonId) {
+ return true;
+ }
+
+ // Ignore requests from chrome or add-on code when we are monitoring
+ // content.
+ if (
+ channel.loadInfo &&
+ channel.loadInfo.loadingDocument === null &&
+ (channel.loadInfo.loadingPrincipal ===
+ Services.scriptSecurityManager.getSystemPrincipal() ||
+ channel.loadInfo.isInDevToolsContext)
+ ) {
+ return false;
+ }
+
+ if (filters.window) {
+ let win = lazy.NetworkHelper.getWindowForRequest(channel);
+ if (filters.matchExactWindow) {
+ return win == filters.window;
+ }
+
+ // Since frames support, this.window may not be the top level content
+ // frame, so that we can't only compare with win.top.
+ while (win) {
+ if (win == filters.window) {
+ return true;
+ }
+ if (win.parent == win) {
+ break;
+ }
+ win = win.parent;
+ }
+ return false;
+ }
+
+ if (filters.browserId) {
+ const topFrame = lazy.NetworkHelper.getTopFrameForRequest(channel);
+ // `topFrame` is typically null for some chrome requests like favicons
+ // And its `browsingContext` attribute might be null if the request happened
+ // while the tab is being closed.
+ if (topFrame?.browsingContext?.browserId == filters.browserId) {
+ return true;
+ }
+
+ // If we couldn't get the top frame BrowsingContext from the loadContext,
+ // look for it on channel.loadInfo instead.
+ if (
+ channel.loadInfo &&
+ channel.loadInfo.browsingContext &&
+ channel.loadInfo.browsingContext.browserId == filters.browserId
+ ) {
+ return true;
+ }
+ }
+
+ if (
+ filters.addonId &&
+ channel?.loadInfo.loadingPrincipal.addonId === filters.addonId
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+export const NetworkUtils = {
+ causeTypeToString,
+ createNetworkEvent,
+ fetchRequestHeadersAndCookies,
+ getChannelBrowsingContextID,
+ getChannelInnerWindowId,
+ isPreloadRequest,
+ matchRequest,
+ stringToCauseType,
+};