diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/resources/network-events.js | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/devtools/server/actors/resources/network-events.js b/devtools/server/actors/resources/network-events.js new file mode 100644 index 0000000000..c1440f2c8d --- /dev/null +++ b/devtools/server/actors/resources/network-events.js @@ -0,0 +1,420 @@ +/* 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 { isWindowGlobalPartOfContext } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs" +); +const { WatcherRegistry } = ChromeUtils.importESModule( + "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs", + { + // WatcherRegistry needs to be a true singleton and loads ActorManagerParent + // which also has to be a true singleton. + loadInDevToolsLoader: false, + } +); +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", +}); + +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; + this.listener = new lazy.NetworkObserver({ + ignoreChannelFunction: this.shouldIgnoreChannel.bind(this), + onNetworkEvent: this.onNetworkEvent.bind(this), + }); + + Services.obs.addObserver(this, "window-global-destroyed"); + } + + /** + * 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); + } + + /** + * Watch for previous document being unloaded in order to clear + * all related network events, in case persist is disabled. + * (which is the default behavior) + */ + observe(windowGlobal, topic) { + if (topic !== "window-global-destroyed") { + return; + } + // If we persist, we will keep all requests allocated. + // For now, consider that the Browser console and toolbox persist all the requests. + if (this.persist || this.watcherActor.sessionContext.type == "all") { + return; + } + // Only process WindowGlobals which are related to the debugged scope. + if ( + !isWindowGlobalPartOfContext( + windowGlobal, + this.watcherActor.sessionContext + ) + ) { + return; + } + const { innerWindowId } = windowGlobal; + + for (const child of this.pool.poolChildren()) { + // Destroy all network events matching the destroyed WindowGlobal + if (!child.isNavigationRequest()) { + if (child.getInnerWindowId() == innerWindowId) { + child.destroy(); + } + // Avoid destroying the navigation request, which is flagged with previous document's innerWindowId. + // When navigating, the WindowGlobal we navigate *from* will be destroyed and notified here. + // We should explicitly avoid destroying it here. + // But, we still want to eventually destroy them. + // So do this when navigating a second time, we will navigate from a distinct WindowGlobal + // and check that this is the top level window global and not an iframe one. + // So that we avoid clearing the top navigation when an iframe navigates + // + // Avoid destroying the request if innerWindowId isn't set. This happens when we reload many times in a row. + // The previous navigation request will be cancelled and because of that its innerWindowId will be null. + // But the frontend will receive it after the navigation begins (after will-navigate) and will display it + // and try to fetch extra data about it. So, avoid destroying its NetworkEventActor. + } else if ( + child.getInnerWindowId() && + child.getInnerWindowId() != innerWindowId && + windowGlobal.browsingContext == + this.watcherActor.browserElement?.browsingContext + ) { + 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" && + !WatcherRegistry.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, + resourceType: resource.resourceType, + isBlocked, + isFileRequest: resource.isFileRequest, + 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. + requestCookiesAvailable: true, + requestHeadersAvailable: true, + }, + }; + 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) { + this._emitUpdate(networkEvent); + } + + return actor; + } + + onNetworkEventUpdate(updateResource) { + const networkEvent = this.networkEvents.get(updateResource.resourceId); + + if (!networkEvent) { + return; + } + + const { resourceUpdates, receivedUpdates } = networkEvent; + + switch (updateResource.updateType) { + case "responseStart": + resourceUpdates.httpVersion = updateResource.httpVersion; + resourceUpdates.status = updateResource.status; + resourceUpdates.statusText = updateResource.statusText; + 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; + resourceUpdates.proxyHttpVersion = updateResource.proxyHttpVersion; + resourceUpdates.proxyStatus = updateResource.proxyStatus; + resourceUpdates.proxyStatusText = updateResource.proxyStatusText; + + resourceUpdates.responseHeadersAvailable = true; + resourceUpdates.responseCookiesAvailable = true; + break; + case "responseContent": + resourceUpdates.contentSize = updateResource.contentSize; + resourceUpdates.transferredSize = updateResource.transferredSize; + resourceUpdates.mimeType = updateResource.mimeType; + resourceUpdates.blockingExtension = updateResource.blockingExtension; + resourceUpdates.blockedReason = updateResource.blockedReason; + break; + case "eventTimings": + resourceUpdates.totalTime = updateResource.totalTime; + break; + case "securityInfo": + resourceUpdates.securityState = updateResource.state; + resourceUpdates.isRacing = updateResource.isRacing; + break; + } + + resourceUpdates[`${updateResource.updateType}Available`] = true; + receivedUpdates.push(updateResource.updateType); + + const isComplete = networkEvent.isFileRequest + ? receivedUpdates.includes("responseStart") + : receivedUpdates.includes("eventTimings") && + receivedUpdates.includes("responseContent") && + receivedUpdates.includes("securityInfo"); + + if (isComplete) { + this._emitUpdate(networkEvent); + } + } + + _emitUpdate(networkEvent) { + this.onNetworkEventUpdated([ + { + resourceType: networkEvent.resourceType, + 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(); + Services.obs.removeObserver(this, "window-global-destroyed"); + } + } +} + +module.exports = NetworkEventWatcher; |