Source code
Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
NetworkHelper:
"resource://devtools/shared/network-observer/NetworkHelper.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_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;
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;
}
/**
* 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
);
}
/**
* 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<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) {
// 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 };
}
/**
* For a given channel, fetch the response's headers and cookies.
*
* @param {nsIChannel} 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 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 &&
(isChannelFromSystemPrincipal(channel) ||
channel.loadInfo.isInDevToolsContext)
) {
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 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
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",
];
// 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;
}
export const NetworkUtils = {
causeTypeToString,
fetchRequestHeadersAndCookies,
fetchResponseHeadersAndCookies,
getCauseDetails,
getChannelBrowsingContextID,
getChannelInnerWindowId,
getChannelPriority,
getHttpVersion,
getProtocol,
getReferrerPolicy,
getWebSocketChannel,
isChannelFromSystemPrincipal,
isChannelPrivate,
isFromCache,
isNavigationRequest,
isPreloadRequest,
isRedirectedChannel,
isThirdPartyTrackingResource,
matchRequest,
stringToCauseType,
getBlockedReason,
getCharset,
};