Body limit configuration may truncate encoded response to no body at all, in which case, we weren't showing any warning. Differential Revision: https://phabricator.services.mozilla.com/D301570
478 lines
15 KiB
JavaScript
478 lines
15 KiB
JavaScript
/* 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 { Pool } = require("resource://devtools/shared/protocol/Pool.js");
|
|
const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule(
|
|
"resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs",
|
|
// ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent
|
|
// which also has to be a true singleton.
|
|
{ global: "shared" }
|
|
);
|
|
const Targets = require("resource://devtools/server/actors/targets/index.js");
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(
|
|
lazy,
|
|
{
|
|
NetworkObserver:
|
|
"resource://devtools/shared/network-observer/NetworkObserver.sys.mjs",
|
|
NetworkUtils:
|
|
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
|
|
},
|
|
{ global: "contextual" }
|
|
);
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"NetworkEventActor",
|
|
"resource://devtools/server/actors/network-monitor/network-event-actor.js",
|
|
true
|
|
);
|
|
|
|
/**
|
|
* Handles network events from the parent process
|
|
*/
|
|
class NetworkEventWatcher {
|
|
/**
|
|
* Start watching for all network events related to a given Watcher Actor.
|
|
*
|
|
* @param WatcherActor watcherActor
|
|
* The watcher actor in the parent process from which we should
|
|
* observe network events.
|
|
* @param Object options
|
|
* Dictionary object with following attributes:
|
|
* - onAvailable: mandatory function
|
|
* This will be called for each resource.
|
|
* - onUpdated: optional function
|
|
* This would be called multiple times for each resource.
|
|
*/
|
|
async watch(watcherActor, { onAvailable, onUpdated }) {
|
|
this.networkEvents = new Map();
|
|
|
|
this.watcherActor = watcherActor;
|
|
this.onNetworkEventAvailable = onAvailable;
|
|
this.onNetworkEventUpdated = onUpdated;
|
|
// Boolean to know if we keep previous document network events or not.
|
|
this.persist = false;
|
|
|
|
// Retrive instantiation value for body limit from target configurations
|
|
const targetConfigurationActor =
|
|
this.watcherActor.getTargetConfigurationActor();
|
|
const bodyLimit = targetConfigurationActor.getNetworkBodyLimit();
|
|
|
|
this.listener = new lazy.NetworkObserver({
|
|
// The responses will be decoded lazily when the Response details are
|
|
// requested by the UI.
|
|
decodeResponseBodies: false,
|
|
bodyLimit,
|
|
ignoreChannelFunction: this.shouldIgnoreChannel.bind(this),
|
|
onNetworkEvent: this.onNetworkEvent.bind(this),
|
|
});
|
|
|
|
this.watcherActor.on(
|
|
"top-browsing-context-will-navigate",
|
|
this.#onTopBrowsingContextWillNavigate
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear all the network events and the related actors.
|
|
*
|
|
* This is called on actor destroy, but also from WatcherActor.clearResources(NETWORK_EVENT)
|
|
*/
|
|
clear() {
|
|
this.networkEvents.clear();
|
|
this.listener.clear();
|
|
if (this._pool) {
|
|
this._pool.destroy();
|
|
this._pool = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A protocol.js Pool to store all NetworkEventActor's which may be destroyed on navigations.
|
|
*/
|
|
get pool() {
|
|
if (this._pool) {
|
|
return this._pool;
|
|
}
|
|
this._pool = new Pool(this.watcherActor.conn, "network-events");
|
|
this.watcherActor.manage(this._pool);
|
|
return this._pool;
|
|
}
|
|
|
|
/**
|
|
* Instruct to keep reference to previous document requests or not.
|
|
* If persist is disabled, we will clear all informations about previous document
|
|
* on each navigation.
|
|
* If persist is enabled, we will keep all informations for all documents, leading
|
|
* to lots of allocations!
|
|
*
|
|
* @param {boolean} enabled
|
|
*/
|
|
setPersist(enabled) {
|
|
this.persist = enabled;
|
|
}
|
|
|
|
/**
|
|
* Gets the throttle settings
|
|
*
|
|
* @return {*} data
|
|
*/
|
|
getThrottleData() {
|
|
return this.listener.getThrottleData();
|
|
}
|
|
|
|
/**
|
|
* Sets the throttle data
|
|
*
|
|
* @param {*} data
|
|
*/
|
|
setThrottleData(data) {
|
|
this.listener.setThrottleData(data);
|
|
}
|
|
|
|
/**
|
|
* Instruct to save or ignore request and response bodies
|
|
*
|
|
* @param {boolean} save
|
|
*/
|
|
setSaveRequestAndResponseBodies(save) {
|
|
this.listener.setSaveRequestAndResponseBodies(save);
|
|
}
|
|
|
|
/**
|
|
* Block requests based on the filters
|
|
*
|
|
* @param {object} filters
|
|
*/
|
|
blockRequest(filters) {
|
|
this.listener.blockRequest(filters);
|
|
}
|
|
|
|
/**
|
|
* Unblock requests based on the fitlers
|
|
*
|
|
* @param {object} filters
|
|
*/
|
|
unblockRequest(filters) {
|
|
this.listener.unblockRequest(filters);
|
|
}
|
|
|
|
/**
|
|
* Calls the listener to set blocked urls
|
|
*
|
|
* @param {Array} urls
|
|
* The urls to block
|
|
*/
|
|
|
|
setBlockedUrls(urls) {
|
|
this.listener.setBlockedUrls(urls);
|
|
}
|
|
|
|
/**
|
|
* Calls the listener to get the blocked urls
|
|
*
|
|
* @return {Array} urls
|
|
* The blocked urls
|
|
*/
|
|
|
|
getBlockedUrls() {
|
|
return this.listener.getBlockedUrls();
|
|
}
|
|
|
|
override(url, path) {
|
|
this.listener.override(url, path);
|
|
}
|
|
|
|
removeOverride(url) {
|
|
this.listener.removeOverride(url);
|
|
}
|
|
|
|
setLocalModeMappings(mappings) {
|
|
this.listener.setLocalModeMappings(mappings);
|
|
}
|
|
|
|
setBodyLimit(bodyLimit) {
|
|
this.listener.setBodyLimit(bodyLimit);
|
|
}
|
|
|
|
/**
|
|
* Watch for previous document being unloaded in order to clear
|
|
* all related network events, in case persist is disabled.
|
|
* (which is the default behavior)
|
|
*
|
|
* This "will-navigate" event should only be fired when debugging tabs
|
|
* (not for web extensions or browser toolbox).
|
|
*/
|
|
#onTopBrowsingContextWillNavigate = () => {
|
|
// If we persist, we will keep all requests allocated.
|
|
if (this.persist) {
|
|
return;
|
|
}
|
|
|
|
const { innerWindowId } =
|
|
this.watcherActor.browsingContext.currentWindowGlobal;
|
|
|
|
// When a navigation starts, destroy all network request actors as the UI should not longer show them.
|
|
// We can easily destroy all requests which aren't navigation request.
|
|
// But navigation requests should be preserved as they started just before the navigation
|
|
// (and the will-navigate" event fired).
|
|
// The current WindowGloball is still for the document we navigate **from**,
|
|
// so destroy navigation requests from iframes or the WindowGlobal from the previous navigation
|
|
// with the `innerWindowId` comparison.
|
|
for (const child of this.pool.poolChildren()) {
|
|
if (
|
|
!child.isNavigationRequest() ||
|
|
(child.getInnerWindowId() && child.getInnerWindowId() != innerWindowId)
|
|
) {
|
|
child.destroy();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called by NetworkObserver in order to know if the channel should be ignored
|
|
*/
|
|
shouldIgnoreChannel(channel) {
|
|
// First of all, check if the channel matches the watcherActor's session.
|
|
const filters = { sessionContext: this.watcherActor.sessionContext };
|
|
if (!lazy.NetworkUtils.matchRequest(channel, filters)) {
|
|
return true;
|
|
}
|
|
|
|
// When we are in the browser toolbox in parent process scope,
|
|
// the session context is still "all", but we are no longer watching frame and process targets.
|
|
// In this case, we should ignore all requests belonging to a BrowsingContext that isn't in the parent process
|
|
// (i.e. the process where this Watcher runs)
|
|
const isParentProcessOnlyBrowserToolbox =
|
|
this.watcherActor.sessionContext.type == "all" &&
|
|
!ParentProcessWatcherRegistry.isWatchingTargets(
|
|
this.watcherActor,
|
|
Targets.TYPES.FRAME
|
|
);
|
|
if (isParentProcessOnlyBrowserToolbox) {
|
|
// We should ignore all requests coming from BrowsingContext running in another process
|
|
const browsingContextID =
|
|
lazy.NetworkUtils.getChannelBrowsingContextID(channel);
|
|
const browsingContext = BrowsingContext.get(browsingContextID);
|
|
// We accept any request that isn't bound to any BrowsingContext.
|
|
// This is most likely a privileged request done from a JSM/C++.
|
|
// `isInProcess` will be true, when the document executes in the parent process.
|
|
//
|
|
// Note that we will still accept all requests that aren't bound to any BrowsingContext
|
|
// See browser_resources_network_events_parent_process.js test with privileged request
|
|
// made from the content processes.
|
|
// We miss some attribute on channel/loadInfo to know that it comes from the content process.
|
|
if (browsingContext?.currentWindowGlobal.isInProcess === false) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
onNetworkEvent(networkEventOptions, channel) {
|
|
if (channel.channelId && this.networkEvents.has(channel.channelId)) {
|
|
throw new Error(
|
|
`Got notified about channel ${channel.channelId} more than once.`
|
|
);
|
|
}
|
|
|
|
const actor = new NetworkEventActor(
|
|
this.watcherActor.conn,
|
|
this.watcherActor.sessionContext,
|
|
{
|
|
onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
|
|
onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this),
|
|
},
|
|
networkEventOptions,
|
|
channel
|
|
);
|
|
this.pool.manage(actor);
|
|
|
|
const resource = actor.asResource();
|
|
const isBlocked = !!resource.blockedReason;
|
|
const networkEvent = {
|
|
browsingContextID: resource.browsingContextID,
|
|
innerWindowId: resource.innerWindowId,
|
|
resourceId: resource.resourceId,
|
|
isBlocked,
|
|
receivedUpdates: [],
|
|
resourceUpdates: {},
|
|
};
|
|
|
|
// Requests already come with request cookies and headers, so those
|
|
// should always be considered as available. But the client still
|
|
// heavily relies on those `Available` flags to fetch additional data,
|
|
// so it is better to keep them for consistency.
|
|
|
|
// Set the flags on the resource so that the front-end can fetch
|
|
// and display request headers and cookies details asap.
|
|
lazy.NetworkUtils.setEventAsAvailable(resource, [
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_COOKIES,
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.REQUEST_HEADERS,
|
|
]);
|
|
|
|
this.networkEvents.set(resource.resourceId, networkEvent);
|
|
|
|
this.onNetworkEventAvailable([resource]);
|
|
|
|
// Blocked requests will not receive further updates and should emit an
|
|
// update packet immediately.
|
|
// The frontend expects to receive a dedicated update to consider the
|
|
// request as completed. TODO: lift this restriction so that we can only
|
|
// emit a resource available notification if no update is needed.
|
|
if (isBlocked) {
|
|
lazy.NetworkUtils.setEventAsAvailable(networkEvent.resourceUpdates, [
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
|
|
]);
|
|
this._emitUpdate(networkEvent);
|
|
}
|
|
|
|
return actor;
|
|
}
|
|
|
|
onNetworkEventUpdate(updateResource) {
|
|
const networkEvent = this.networkEvents.get(updateResource.resourceId);
|
|
|
|
if (!networkEvent) {
|
|
return;
|
|
}
|
|
const { NETWORK_EVENT_TYPES } = lazy.NetworkUtils;
|
|
const { resourceUpdates, receivedUpdates } = networkEvent;
|
|
|
|
const networkEventTypes = [
|
|
NETWORK_EVENT_TYPES.RESPONSE_COOKIES,
|
|
NETWORK_EVENT_TYPES.RESPONSE_HEADERS,
|
|
];
|
|
|
|
switch (updateResource.updateType) {
|
|
case NETWORK_EVENT_TYPES.CACHE_DETAILS:
|
|
resourceUpdates.fromCache = updateResource.fromCache;
|
|
resourceUpdates.fromServiceWorker = updateResource.fromServiceWorker;
|
|
break;
|
|
case NETWORK_EVENT_TYPES.RESPONSE_START:
|
|
resourceUpdates.httpVersion = updateResource.httpVersion;
|
|
resourceUpdates.status = updateResource.status;
|
|
resourceUpdates.statusText = updateResource.statusText;
|
|
resourceUpdates.earlyHintsStatus = updateResource.earlyHintsStatus;
|
|
resourceUpdates.remoteAddress = updateResource.remoteAddress;
|
|
resourceUpdates.remotePort = updateResource.remotePort;
|
|
// The mimetype is only set when then the contentType is available
|
|
// in the _onResponseHeader and not for cached/service worker requests
|
|
// in _httpResponseExaminer.
|
|
resourceUpdates.mimeType = updateResource.mimeType;
|
|
resourceUpdates.waitingTime = updateResource.waitingTime;
|
|
resourceUpdates.isResolvedByTRR = updateResource.isResolvedByTRR;
|
|
if (Number.isInteger(updateResource.priority)) {
|
|
resourceUpdates.priority = updateResource.priority;
|
|
}
|
|
resourceUpdates.proxyHttpVersion = updateResource.proxyHttpVersion;
|
|
resourceUpdates.proxyStatus = updateResource.proxyStatus;
|
|
resourceUpdates.proxyStatusText = updateResource.proxyStatusText;
|
|
resourceUpdates.isRedirect = updateResource.isRedirect;
|
|
|
|
if (resourceUpdates.earlyHintsStatus.length) {
|
|
networkEventTypes.push(
|
|
NETWORK_EVENT_TYPES.EARLY_HINT_RESPONSE_HEADERS
|
|
);
|
|
}
|
|
|
|
lazy.NetworkUtils.setEventAsAvailable(
|
|
resourceUpdates,
|
|
networkEventTypes
|
|
);
|
|
|
|
break;
|
|
case NETWORK_EVENT_TYPES.RESPONSE_CONTENT:
|
|
if (updateResource.contentSize !== undefined) {
|
|
resourceUpdates.contentSize = updateResource.contentSize;
|
|
}
|
|
resourceUpdates.transferredSize = updateResource.transferredSize;
|
|
resourceUpdates.mimeType = updateResource.mimeType;
|
|
break;
|
|
case NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE:
|
|
resourceUpdates.extension = updateResource.extension;
|
|
resourceUpdates.blockedReason = updateResource.blockedReason;
|
|
if (Number.isInteger(updateResource.priority)) {
|
|
resourceUpdates.priority = updateResource.priority;
|
|
}
|
|
resourceUpdates.truncated = updateResource.truncated;
|
|
break;
|
|
case NETWORK_EVENT_TYPES.EVENT_TIMINGS:
|
|
resourceUpdates.totalTime = updateResource.totalTime;
|
|
break;
|
|
case NETWORK_EVENT_TYPES.SECURITY_INFO:
|
|
resourceUpdates.securityState = updateResource.state;
|
|
break;
|
|
}
|
|
|
|
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
|
|
updateResource.updateType,
|
|
]);
|
|
|
|
receivedUpdates.push(updateResource.updateType);
|
|
|
|
const isResponseComplete =
|
|
receivedUpdates.includes(NETWORK_EVENT_TYPES.EVENT_TIMINGS) &&
|
|
receivedUpdates.includes(NETWORK_EVENT_TYPES.RESPONSE_CONTENT_COMPLETE) &&
|
|
receivedUpdates.includes(NETWORK_EVENT_TYPES.SECURITY_INFO);
|
|
|
|
if (isResponseComplete) {
|
|
// Lets add an event to clearly define the last update expected to be
|
|
// emitted. There will be no more updates after this.
|
|
lazy.NetworkUtils.setEventAsAvailable(resourceUpdates, [
|
|
lazy.NetworkUtils.NETWORK_EVENT_TYPES.RESPONSE_END,
|
|
]);
|
|
}
|
|
|
|
if (
|
|
updateResource.updateType == NETWORK_EVENT_TYPES.REQUEST_POSTDATA ||
|
|
updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_START ||
|
|
updateResource.updateType == NETWORK_EVENT_TYPES.RESPONSE_CONTENT ||
|
|
isResponseComplete
|
|
) {
|
|
this._emitUpdate(networkEvent);
|
|
// clean up already sent updates
|
|
networkEvent.resourceUpdates = {};
|
|
}
|
|
}
|
|
|
|
_emitUpdate(networkEvent) {
|
|
this.onNetworkEventUpdated([
|
|
{
|
|
resourceId: networkEvent.resourceId,
|
|
resourceUpdates: networkEvent.resourceUpdates,
|
|
browsingContextID: networkEvent.browsingContextID,
|
|
innerWindowId: networkEvent.innerWindowId,
|
|
},
|
|
]);
|
|
}
|
|
|
|
onNetworkEventDestroy(channelId) {
|
|
if (this.networkEvents.has(channelId)) {
|
|
this.networkEvents.delete(channelId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop watching for network event related to a given Watcher Actor.
|
|
*/
|
|
destroy() {
|
|
if (this.listener) {
|
|
this.clear();
|
|
this.listener.destroy();
|
|
this.watcherActor.off(
|
|
"top-browsing-context-will-navigate",
|
|
this.#onTopBrowsingContextWillNavigate
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = NetworkEventWatcher;
|