diff options
Diffstat (limited to 'devtools/shared/network-observer/NetworkUtils.sys.mjs')
-rw-r--r-- | devtools/shared/network-observer/NetworkUtils.sys.mjs | 481 |
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, +}; |