/* 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, { NetworkRequest: "chrome://remote/content/shared/NetworkRequest.sys.mjs", NetworkResponse: "chrome://remote/content/shared/NetworkResponse.sys.mjs", NetworkUtils: "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", Log: "chrome://remote/content/shared/Log.sys.mjs", truncate: "chrome://remote/content/shared/Format.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); /** * The NetworkEventRecord implements the interface expected from network event * owners for consumers of the DevTools NetworkObserver. * * The NetworkEventRecord emits the before-request-sent event on behalf of the * NetworkListener instance which created it. */ export class NetworkEventRecord { #decodedBodySizeMap; #devtoolsResponseContent; #devtoolsResponseSizes; #fromCache; #networkEventsMap; #networkListener; #request; #response; #responseStartOverride; #wrappedChannel; /** * * @param {object} networkEvent * The initial network event information (see createNetworkEvent() in * NetworkUtils.sys.mjs). * @param {nsIChannel} channel * The nsIChannel behind this network event. * @param {NetworkListener} networkListener * The NetworkListener which created this NetworkEventRecord. * @param {NetworkDecodedBodySizeMap} decodedBodySizeMap * Map from channelId to decoded body sizes. This information is read * from all processes and aggregated in the parent process. * @param {NavigationManager} navigationManager * The NavigationManager which belongs to the same session as this * NetworkEventRecord. * @param {Map} networkEventsMap * The map between request id and NetworkEventRecord instance to complete * the previous event in case of redirect. */ constructor( networkEvent, channel, networkListener, decodedBodySizeMap, navigationManager, networkEventsMap ) { this.#request = new lazy.NetworkRequest(channel, { eventRecord: this, navigationManager, rawHeaders: networkEvent.rawHeaders, }); this.#response = null; this.#devtoolsResponseContent = null; this.#devtoolsResponseSizes = { decodedBodySize: 0, encodedBodySize: 0, totalTransmittedSize: 0, }; if (channel instanceof Ci.nsIChannel) { this.#wrappedChannel = ChannelWrapper.get(channel); this.#wrappedChannel.addEventListener("error", this.#onChannelCompleted); this.#wrappedChannel.addEventListener("stop", this.#onChannelCompleted); } this.#fromCache = networkEvent.fromCache; this.#decodedBodySizeMap = decodedBodySizeMap; this.#networkListener = networkListener; this.#networkEventsMap = networkEventsMap; if (this.#networkEventsMap.has(this.#requestId)) { const previousEvent = this.#networkEventsMap.get(this.#requestId); if (this.redirectCount != previousEvent.redirectCount) { // If redirect count is set, this is a redirect from the previous request. // notifyRedirect will complete the previous request. previousEvent.notifyRedirect(); } else { // Otherwise if there is no redirect count or if it is identical to the // previously detected request, this is an authentication attempt. previousEvent.notifyAuthenticationAttempt( this.#request.channel.channelId ); } } this.#networkEventsMap.set(this.#requestId, this); // NetworkObserver creates a network event when request headers have been // parsed. // According to the BiDi spec, we should emit beforeRequestSent when adding // request headers, see https://whatpr.org/fetch/1540.html#http-network-or-cache-fetch // step 8.17 // Bug 1802181: switch the NetworkObserver to an event-based API. this.#emitBeforeRequestSent(); // If the request is already blocked, we will not receive further updates, // emit a network.fetchError event immediately. if (networkEvent.blockedReason) { this.#emitFetchError(); } } get #requestId() { return this.#request.requestId; } get channelId() { return this.#request.channel.channelId; } get redirectCount() { return this.#request.redirectCount; } /** * Add network request cache details. * * Required API for a NetworkObserver event owner. * * @param {object} options * @param {boolean} options.fromCache */ addCacheDetails(options) { const { fromCache } = options; this.#fromCache = fromCache; } /** * Add network request raw headers. * * Required API for a NetworkObserver event owner. * * @param {object} options * @param {string} options.rawHeaders */ addRawHeaders(options) { const { rawHeaders } = options; this.#request.addRawHeaders(rawHeaders); } /** * Add network request POST data. * * Required API for a NetworkObserver event owner. */ addRequestPostData() {} /** * Add the initial network response information. * * Required API for a NetworkObserver event owner. * * @param {object} options * @param {nsIChannel} options.channel * The channel. * @param {boolean} options.fromCache * @param {boolean} options.fromServiceWorker * @param {string} options.rawHeaders */ addResponseStart(options) { const { channel, fromCache, fromServiceWorker, rawHeaders } = options; this.#response = new lazy.NetworkResponse(channel, { fromCache: this.#fromCache || !!fromCache, fromServiceWorker, rawHeaders, }); // This should be triggered when all headers have been received, matching // the WebDriverBiDi response started trigger in `4.6. HTTP-network fetch` // from the fetch specification, based on the PR visible at // https://github.com/whatwg/fetch/pull/1540 this.#emitResponseStarted(); } /** * Add connection security information. * * Required API for a NetworkObserver event owner. * * Not used for RemoteAgent. */ addSecurityInfo() {} /** * Add network event timings. * * Required API for a NetworkObserver event owner. * * Not used for RemoteAgent. */ addEventTimings() {} /** * Add response cache entry. * * Required API for a NetworkObserver event owner. * * Not used for RemoteAgent. */ addResponseCache() {} /** * Add response content. * * Required API for a NetworkObserver event owner. * * @param {object} responseContent * An object which represents the response content. */ addResponseContent(responseContent) { // Bug 1982252: at the moment we have no way to know which // addResponseContent call corresponds to the last chunk, and therefore we // hold on the responseContent and will forward it to the NetworkResponse // class in addResponseContentComplete. this.#devtoolsResponseContent = responseContent; // Bug 1979111: In Bug 1971778 the DevTools NetworkObserver is configured // to no longer decode response sizes. // Consequently `responseContent` no longer exposes the decodedBodySize. // Until we can monitor decoded body size in all processes, ServiceWorker // initiated requests will report the encodedBodySize here, which is at // least non-zero. this.#devtoolsResponseSizes.decodedBodySize = responseContent.isContentEncoded ? responseContent.encodedBodySize : responseContent.decodedBodySize; // Note: response's bodySize is normally equal to encodedBodySize, but // encodedBodySize is only available on responses with isContentEncoded // set to true, which is not the case for data or file URIs, regardless // of the `decodeResponseBodies` configuration of the NetworkObserver. this.#devtoolsResponseSizes.encodedBodySize = responseContent.bodySize; this.#devtoolsResponseSizes.totalTransmittedSize = responseContent.transferredSize; } /** * Add response content complete * * Required API for a NetworkObserver event owner. * * @param {object} responseInfo * An object with info for when response content is complete */ addResponseContentComplete(responseInfo) { // addResponseContentComplete is called when all chunks have been received, // we can now set the final response content in the response object. if (this.#response && this.#devtoolsResponseContent) { this.#response.setResponseContent(this.#devtoolsResponseContent); } if ( // Ignore already completed requests. this.#request.alreadyCompleted || // Ignore HTTP channels which are not service worker requests, they will // be handled via "error" and "stop" events, see #onChannelCompleted. (this.#request.isHttpChannel && !this.#response?.fromServiceWorker) ) { return; } this.#handleRequestEnd( responseInfo.blockedReason, this.#devtoolsResponseSizes ); } /** * Add server timings. * * Required API for a NetworkObserver event owner. * * Not used for RemoteAgent. */ addServerTimings() {} /** * Add service worker timings. * * Required API for a NetworkObserver event owner. * * Not used for RemoteAgent. */ addServiceWorkerTimings() {} /** * Complete response in case of an authentication attempt. * * This method is required to be called on the previous event. * * @param {number} nextChannelId * The channelId of the next authentication attempt in the authentication * chain. */ notifyAuthenticationAttempt(nextChannelId) { // TODO: Bug 1899604, behavior might change based on spec issue // https://github.com/w3c/webdriver-bidi/issues/722 // For now, in case of authentication attempts, we mark the current event as // completed and skip its responseCompleted event. // This way, only the last successful/failed authentication attempt will // emit a response completed event. this.#markRequestComplete(); // Notify the decodedBodySizeMap about the authentication chain as well. this.#decodedBodySizeMap.setAuthenticationAttemptMapping( this.#request.channel.channelId, nextChannelId ); } /** * Complete response in case of redirect. * * This method is required to be called on the previous event. */ notifyRedirect() { this.#emitResponseCompleted(); this.#markRequestComplete(); } onAuthPrompt(authDetails, authCallbacks) { this.#emitAuthRequired(authCallbacks); } prepareResponseStart(options) { this.#responseStartOverride = options; } #emitAuthRequired(authCallbacks) { this.#networkListener.emit("auth-required", { authCallbacks, request: this.#request, response: this.#response, }); } #emitBeforeRequestSent() { this.#networkListener.emit("before-request-sent", { request: this.#request, }); } #emitFetchError() { this.#networkListener.emit("fetch-error", { request: this.#request, }); } #emitResponseCompleted() { this.#networkListener.emit("response-completed", { request: this.#request, response: this.#response, }); } #emitResponseStarted() { this.#networkListener.emit("response-started", { request: this.#request, response: this.#response, }); } #handleRequestEnd(blockedReason, sizes) { if (this.#responseStartOverride) { this.addResponseStart(this.#responseStartOverride); } if (blockedReason) { this.#emitFetchError(); } else { // In the meantime, if the request was already completed, bail out here. if (this.#request.alreadyCompleted) { return; } if (!this.#response) { lazy.logger.warn( lazy.truncate`Missing response info, network.responseCompleted will be skipped for URL: ${this.#request.serializedURL}` ); } else { this.#response.setResponseSizes(sizes); this.#emitResponseCompleted(); } } this.#markRequestComplete(); } #markRequestComplete() { this.#request.alreadyCompleted = true; this.#networkEventsMap.delete(this.#requestId); this.#decodedBodySizeMap.delete(this.#request.channel.channelId); if (this.#wrappedChannel) { this.#wrappedChannel.removeEventListener( "error", this.#onChannelCompleted ); this.#wrappedChannel.removeEventListener( "stop", this.#onChannelCompleted ); } } #onChannelCompleted = () => { if (this.#request.alreadyCompleted) { return; } const { blockedReason } = lazy.NetworkUtils.getBlockedReason( this.#request.channel, this.#response ? this.#response.fromCache : false ); // TODO: Figure out a good default value for the decoded body size for non // http channels. // Blocked channels will emit a fetchError event which does not contain // sizes. const sizes = {}; if (this.#request.isHttpChannel && !blockedReason) { sizes.decodedBodySize = this.#decodedBodySizeMap.getDecodedBodySize( this.#request.channel.channelId ); sizes.encodedBodySize = this.#request.channel.encodedBodySize; sizes.totalTransmittedSize = this.#request.channel.transferSize; } this.#handleRequestEnd(blockedReason, sizes); }; }