/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters( lazy, { NetworkHelper: "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs", NetworkTimings: "resource://devtools/shared/network-observer/NetworkTimings.sys.mjs", }, { global: "contextual" } ); ChromeUtils.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_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; if (channel.isDocument) { // The loadingPrincipal is the principal where the request will be used. principal = channel.loadInfo.loadingPrincipal; } else { // The triggeringPrincipal is the principal of the resource which triggered // the request. Except for document loads, this is normally the best way // to know if a request is done on behalf of a chrome resource. // For instance if a chrome stylesheet loads a resource which is used in a // content page, the loadingPrincipal will be a content principal, but the // triggeringPrincipal will be the system principal. principal = channel.loadInfo.triggeringPrincipal; } return !!principal?.isSystemPrincipal; } function isChromeFileChannel(channel) { if (!(channel instanceof Ci.nsIFileChannel)) { return false; } return ( channel.originalURI.spec.startsWith("chrome://") || channel.originalURI.spec.startsWith("resource://") ); } function isPrivilegedChannel(channel) { return ( isChannelFromSystemPrincipal(channel) || isChromeFileChannel(channel) || channel.loadInfo.isInDevToolsContext ); } /** * Get the browsing context id for the channel. * * @param {*} channel * @returns {number} */ function getChannelBrowsingContextID(channel) { // `frameBrowsingContextID` is non-0 if the channel is loading an iframe. // If available, use it instead of `browsingContextID` which is exceptionally // set to the parent's BrowsingContext id for such channels. if (channel.loadInfo.frameBrowsingContextID) { return channel.loadInfo.frameBrowsingContextID; } 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 || type == Ci.nsIContentPolicy.TYPE_INTERNAL_JSON_PRELOAD ); } /** * Get the channel cause details. * * @param {nsIChannel} channel * @returns {Object} * - loadingDocumentUri {string} uri of the document which created the * channel * - type {string} cause type as string */ function getCauseDetails(channel) { // 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; } } return { loadingDocumentUri: causeUri, type: causeTypeToString( causeType, channel.loadFlags, channel.loadInfo.internalContentPolicyType ), }; } /** * Get the channel priority. Priority is a number which typically ranges from * -20 (lowest priority) to 20 (highest priority). Can be null if the channel * does not implement nsISupportsPriority. * * @param {nsIChannel} channel * @returns {number|undefined} */ function getChannelPriority(channel) { if (channel instanceof Ci.nsISupportsPriority) { return channel.priority; } return null; } /** * Get the channel HTTP version as an uppercase string starting with "HTTP/" * (eg "HTTP/2"). * * @param {nsIChannel} channel * @returns {string} */ function getHttpVersion(channel) { if (!(channel instanceof Ci.nsIHttpChannelInternal)) { return null; } // Determine the HTTP version. const httpVersionMaj = {}; const httpVersionMin = {}; channel.QueryInterface(Ci.nsIHttpChannelInternal); channel.getResponseVersion(httpVersionMaj, httpVersionMin); // The official name HTTP version 2.0 and 3.0 are HTTP/2 and HTTP/3, omit the // trailing `.0`. if (httpVersionMin.value == 0) { return "HTTP/" + httpVersionMaj.value; } return "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value; } const UNKNOWN_PROTOCOL_STRINGS = ["", "unknown"]; const HTTP_PROTOCOL_STRINGS = ["http", "https"]; /** * Get the protocol for the provided httpActivity. Either the ALPN negotiated * protocol or as a fallback a protocol computed from the scheme and the * response status. * * TODO: The `protocol` is similar to another response property called * `httpVersion`. `httpVersion` is uppercase and purely computed from the * response status, whereas `protocol` uses nsIHttpChannel.protocolVersion by * default and otherwise falls back on `httpVersion`. Ideally we should merge * the two properties. * * @param {Object} httpActivity * The httpActivity object for which we need to get the protocol. * * @returns {string} * The protocol as a string. */ function getProtocol(channel) { let protocol = ""; try { const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); // protocolVersion corresponds to ALPN negotiated protocol. protocol = httpChannel.protocolVersion; } catch (e) { // Ignore errors reading protocolVersion. } if (UNKNOWN_PROTOCOL_STRINGS.includes(protocol)) { protocol = channel.URI.scheme; const httpVersion = getHttpVersion(channel); if ( typeof httpVersion == "string" && HTTP_PROTOCOL_STRINGS.includes(protocol) ) { protocol = httpVersion.toLowerCase(); } } return protocol; } /** * Get the channel referrer policy as a string * (eg "strict-origin-when-cross-origin"). * * @param {nsIChannel} channel * @returns {string} */ function getReferrerPolicy(channel) { return channel.referrerInfo ? channel.referrerInfo.getReferrerPolicyString() : ""; } /** * Check if the channel is private. * * @param {nsIChannel} channel * @returns {boolean} */ function isChannelPrivate(channel) { channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); return channel.isChannelPrivate; } /** * Check if the channel data is loaded from the cache or not. * * @param {nsIChannel} channel * The channel for which we need to check the cache status. * * @returns {boolean} * True if the channel data is loaded from the cache, false otherwise. */ function isFromCache(channel) { if (channel instanceof Ci.nsICacheInfoChannel) { return channel.isFromCache(); } return false; } const REDIRECT_STATES = [ 301, // HTTP Moved Permanently 302, // HTTP Found 303, // HTTP See Other 307, // HTTP Temporary Redirect ]; /** * Check if the channel's status corresponds to a known redirect status. * * @param {nsIChannel} channel * The channel for which we need to check the redirect status. * * @returns {boolean} * True if the channel data is a redirect, false otherwise. */ function isRedirectedChannel(channel) { try { return REDIRECT_STATES.includes(channel.responseStatus); } catch (e) { // Throws NS_ERROR_NOT_AVAILABLE if the request was not sent yet. } return false; } /** * 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. * * @param {nsIChannel} channel * @return {boolean} */ function isNavigationRequest(channel) { return channel.isMainDocumentChannel && channel.loadInfo.isTopLevelLoad; } /** * Returns true if the channel has been processed by URL-Classifier features * and is considered third-party with the top window URI, and if it has loaded * a resource that is classified as a tracker. * * @param {nsIChannel} channel * @return {boolean} */ function isThirdPartyTrackingResource(channel) { // 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. return !!( channel instanceof Ci.nsIClassifiedChannel && channel.isThirdPartyTrackingResource() && (channel.thirdPartyClassificationFlags & lazy.tpFlagsMask) == 0 ); } /** * Retrieve the websocket channel for the provided channel, if available. * Returns null otherwise. * * @param {nsIChannel} channel * @returns {nsIWebSocketChannel|null} */ function getWebSocketChannel(channel) { let wsChannel = null; if (channel.notificationCallbacks) { try { wsChannel = channel.notificationCallbacks.QueryInterface( Ci.nsIWebSocketChannel ); } catch (e) { // Not all channels implement nsIWebSocketChannel. } } return wsChannel; } /** * For a given channel, fetch the request's headers and cookies. * * @param {nsIChannel} channel * @return {Object} * An object with two properties: * @property {Array} cookies * Array of { name, value } objects. * @property {Array} 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) { // The `Proxy-Authorization` header even though it appears on the channel is not // actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel // is setup by the proxy. if (name == "Proxy-Authorization") { return; } if (name == "Cookie") { cookieHeader = value; } headers.push({ name, value }); }, }); if (cookieHeader) { cookies = lazy.NetworkHelper.parseCookieHeader(cookieHeader); } return { cookies, headers }; } /** * Parse the early hint raw headers string to an * array of name/value object header pairs * * @param {String} rawHeaders * @returns {Array} */ function parseEarlyHintsResponseHeaders(rawHeaders) { const headers = rawHeaders.split("\r\n"); // Remove the line with the HTTP version and the status headers.shift(); return headers .map(header => { const [name, value] = header.split(":"); return { name, value }; }) .filter(header => header.name.length); } /** * For a given channel, fetch the response's headers and cookies. * * @param {nsIChannel} channel * @return {Object} * An object with two properties: * @property {Array} cookies * Array of { name, value } objects. * @property {Array} headers * Array of { name, value } objects. */ function fetchResponseHeadersAndCookies(channel) { // Read response headers and cookies. const headers = []; const setCookieHeaders = []; const SET_COOKIE_REGEXP = /set-cookie/i; channel.visitOriginalResponseHeaders({ visitHeader(name, value) { if (SET_COOKIE_REGEXP.test(name)) { setCookieHeaders.push(value); } headers.push({ name, value }); }, }); return { cookies: lazy.NetworkHelper.parseSetCookieHeaders(setCookieHeaders), headers, }; } /** * Check if a given network request should be logged by a network monitor * based on the specified filters. * * @param {(nsIHttpChannel|nsIFileChannel)} 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 && isPrivilegedChannel(channel) ) { return false; } // When a page fails loading in top level or in iframe, an error page is shown // which will trigger a request to about:neterror (which is translated into a file:// URI request). // Ignore this request in regular toolbox (but not in the browser toolbox). if (channel.loadInfo?.loadErrorPage) { 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) { // Ignore requests from chrome or add-on code when we don't monitor the whole browser if ( filters.targetActor.sessionContext?.type !== "all" && isPrivilegedChannel(channel) ) { return false; } // 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?.loadingDocument === null && (isChannelFromSystemPrincipal(channel) || 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?.browsingContext?.browserId == filters.browserId) { return true; } } if ( filters.addonId && channel.loadInfo?.loadingPrincipal?.addonId === filters.addonId ) { return true; } return false; } function getBlockedReason(channel, fromCache = false) { let blockingExtension, blockedReason; const { status } = channel; try { const request = channel.QueryInterface(Ci.nsIHttpChannel); const properties = request.QueryInterface(Ci.nsIPropertyBag); blockedReason = request.loadInfo.requestBlockingReason; blockingExtension = properties.getProperty("cancelledByExtension"); // WebExtensionPolicy is not available for workers if (typeof WebExtensionPolicy !== "undefined") { blockingExtension = WebExtensionPolicy.getByID(blockingExtension).name; } } catch (err) { // "cancelledByExtension" doesn't have to be available. } // These are platform errors which are not exposed to the users, // usually the requests (with these errors) might be displayed with various // other status codes. const ignoreList = [ // These are emited when the request is already in the cache. "NS_ERROR_PARSED_DATA_CACHED", // This is emited when there is some issues around images e.g When the img.src // links to a non existent url. This is typically shown as a 404 request. "NS_IMAGELIB_ERROR_FAILURE", // This is emited when there is a redirect. They are shown as 301 requests. "NS_BINDING_REDIRECTED", // E.g Emited by send beacon requests. "NS_ERROR_ABORT", // This is emmited when browser.http.blank_page_with_error_response.enabled // is set to false, and a 404 or 500 request has no content. // They are shown as 404 or 500 requests. "NS_ERROR_NET_EMPTY_RESPONSE", ]; // NS_BINDING_ABORTED are emmited when request are abruptly halted, these are valid and should not be ignored. // They can also be emmited for requests already cache which have the `cached` status, these should be ignored. if (fromCache) { ignoreList.push("NS_BINDING_ABORTED"); } // If the request has not failed or is not blocked by a web extension, check for // any errors not on the ignore list. e.g When a host is not found (NS_ERROR_UNKNOWN_HOST). if ( blockedReason == 0 && !Components.isSuccessCode(status) && !ignoreList.includes(ChromeUtils.getXPCOMErrorName(status)) ) { blockedReason = ChromeUtils.getXPCOMErrorName(status); } return { blockingExtension, blockedReason }; } function getCharset(channel) { const win = lazy.NetworkHelper.getWindowForRequest(channel); return win ? win.document.characterSet : null; } /** * Data channels are either handled in the parent process NetworkObserver for * navigation requests, or in content processes for any other request. * * This function allows to apply the same logic to build the network event actor * in both cases. * * @param {nsIDataChannel} channel * The data channel for which we are creating a network event actor. * @param {object} networkEventActor * The network event actor owning this resource. */ function handleDataChannel(channel, networkEventActor) { networkEventActor.addResponseStart({ channel, fromCache: false, // According to the fetch spec for data URLs we can just hardcode // "Content-Type" header. rawHeaders: "content-type: " + channel.contentType, }); // For data URLs we can not set up a stream listener as for http, // so we have to create a response manually and complete it. const response = { // TODO: Bug 1903807. Re-evaluate if it's correct to just return // zero for `bodySize` and `decodedBodySize`. bodySize: 0, decodedBodySize: 0, contentCharset: channel.contentCharset, contentLength: channel.contentLength, contentType: channel.contentType, mimeType: lazy.NetworkHelper.addCharsetToMimeType( channel.contentType, channel.contentCharset ), transferredSize: 0, }; // For data URIs all timings can be set to zero. const result = lazy.NetworkTimings.getEmptyHARTimings(); networkEventActor.addEventTimings( result.total, result.timings, result.offsets ); const url = channel.URI.spec; response.text = url.substring(url.indexOf(",") + 1); if ( !response.mimeType || !lazy.NetworkHelper.isTextMimeType(response.mimeType) ) { response.encoding = "base64"; try { response.text = btoa(response.text); } catch (err) { // Ignore. } } // Note: `size`` is only used by DevTools, WebDriverBiDi relies on // `bodySize` and `decodedBodySize`. Waiting on Bug 1903807 to decide // if those fields should have non-0 values as well. response.size = response.text.length; // Security information is not relevant for data channel, but it should // not be considered as insecure either. Set empty string as security // state. networkEventActor.addSecurityInfo({ state: "" }); networkEventActor.addResponseContent(response, {}); } export const NetworkUtils = { causeTypeToString, fetchRequestHeadersAndCookies, fetchResponseHeadersAndCookies, getBlockedReason, getCauseDetails, getChannelBrowsingContextID, getChannelInnerWindowId, getChannelPriority, getCharset, getHttpVersion, getProtocol, getReferrerPolicy, getWebSocketChannel, handleDataChannel, isChannelFromSystemPrincipal, isChannelPrivate, isFromCache, isNavigationRequest, isPreloadRequest, isRedirectedChannel, isThirdPartyTrackingResource, matchRequest, parseEarlyHintsResponseHeaders, stringToCauseType, };