tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 53654d78bd6fb7fbc11490a226139db47d042e85
parent 532a5614499d6677045daf216b94b29b4f5a2117
Author: Julian Descottes <jdescottes@mozilla.com>
Date:   Wed,  5 Nov 2025 16:17:32 +0000

Bug 1988955 - [bidi] Add support for dataType=request to network data collection r=whimboo

Differential Revision: https://phabricator.services.mozilla.com/D267227

Diffstat:
Mremote/jar.mn | 1+
Aremote/shared/NetworkDataBytes.sys.mjs | 35+++++++++++++++++++++++++++++++++++
Mremote/shared/NetworkRequest.sys.mjs | 65++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mremote/shared/NetworkResponse.sys.mjs | 57+++++++++++++++++++++++++++++++--------------------------
Mremote/webdriver-bidi/modules/root/network.sys.mjs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
5 files changed, 256 insertions(+), 72 deletions(-)

diff --git a/remote/jar.mn b/remote/jar.mn @@ -27,6 +27,7 @@ remote.jar: content/shared/Navigate.sys.mjs (shared/Navigate.sys.mjs) content/shared/NavigationManager.sys.mjs (shared/NavigationManager.sys.mjs) content/shared/NetworkCacheManager.sys.mjs (shared/NetworkCacheManager.sys.mjs) + content/shared/NetworkDataBytes.sys.mjs (shared/NetworkDataBytes.sys.mjs) content/shared/NetworkDecodedBodySizeMap.sys.mjs (shared/NetworkDecodedBodySizeMap.sys.mjs) content/shared/NetworkRequest.sys.mjs (shared/NetworkRequest.sys.mjs) content/shared/NetworkResponse.sys.mjs (shared/NetworkResponse.sys.mjs) diff --git a/remote/shared/NetworkDataBytes.sys.mjs b/remote/shared/NetworkDataBytes.sys.mjs @@ -0,0 +1,35 @@ +/* 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/. */ + +export class NetworkDataBytes { + #getBytesValue; + #isBase64; + + /** + * Common interface used to handle network BytesValue for collected data which + * might be encoded and require an additional async step in order to retrieve + * the actual bytes. + * + * This is a simple wrapper mostly designed to ensure a common interface in + * case this is used for request or response bodies. + * + * @param {object} options + * @param {Function} options.getBytesValue + * A -potentially async- callable which returns the bytes as a string. + * @param {boolean} options.isBase64 + * Whether this represents a base64-encoded binary data. + */ + constructor(options) { + this.#getBytesValue = options.getBytesValue; + this.#isBase64 = options.isBase64; + } + + get isBase64() { + return this.#isBase64; + } + + async getBytesValue() { + return this.#getBytesValue(); + } +} diff --git a/remote/shared/NetworkRequest.sys.mjs b/remote/shared/NetworkRequest.sys.mjs @@ -12,6 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, { generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", NavigationState: "chrome://remote/content/shared/NavigationManager.sys.mjs", + NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs", notifyNavigationStarted: "chrome://remote/content/shared/NavigationManager.sys.mjs", }); @@ -29,6 +30,7 @@ export class NetworkRequest { #isDataURL; #navigationId; #navigationManager; + #postData; #postDataSize; #rawHeaders; #redirectCount; @@ -97,9 +99,10 @@ export class NetworkRequest { this.#contextId = this.#getContextId(); this.#navigationId = this.#getNavigationId(); - // The postDataSize will no longer be available after the channel is closed. - // Compute and cache the value, to be updated when `setRequestBody` is used. - this.#postDataSize = this.#computePostDataSize(); + // The postData will no longer be available after the channel is closed. + // Compute the postData and postDataSize properties, to be updated later if + // `setRequestBody` is used. + this.#updatePostData(); } get alreadyCompleted() { @@ -159,6 +162,10 @@ export class NetworkRequest { return this.#navigationId; } + get postData() { + return this.#postData; + } + get postDataSize() { return this.#postDataSize; } @@ -217,6 +224,19 @@ export class NetworkRequest { } /** + * Returns the NetworkDataBytes instance representing the request body for + * this request. + * + * @returns {NetworkDataBytes} + */ + readAndProcessRequestBody = () => { + return new lazy.NetworkDataBytes({ + getBytesValue: () => this.#postData.text, + isBase64: this.#postData.isBase64, + }); + }; + + /** * Redirect the request to another provided URL. * * @param {string} url @@ -253,7 +273,7 @@ export class NetworkRequest { } finally { // Make sure to reset the flag once the modification was attempted. this.#channel.requestObserversCalled = true; - this.#postDataSize = this.#computePostDataSize(); + this.#updatePostData(); } } @@ -337,6 +357,7 @@ export class NetworkRequest { initiatorType: this.initiatorType, method: this.method, navigationId: this.navigationId, + postData: this.postData, postDataSize: this.postDataSize, redirectCount: this.redirectCount, requestId: this.requestId, @@ -348,15 +369,6 @@ export class NetworkRequest { }; } - #computePostDataSize() { - const charset = lazy.NetworkUtils.getCharset(this.#channel); - const sentBody = lazy.NetworkHelper.readPostTextFromRequest( - this.#channel, - charset - ); - return sentBody ? sentBody.length : 0; - } - /** * Convert the provided request timing to a timing relative to the beginning * of the request. Note that https://w3c.github.io/resource-timing/#dfn-convert-fetch-timestamp @@ -517,4 +529,31 @@ export class NetworkRequest { ); return !browsingContext.parent; } + + #readPostDataFromRequestAsUTF8() { + const postData = lazy.NetworkHelper.readPostDataFromRequest( + this.#channel, + "UTF-8" + ); + + if (postData === null || postData.data === null) { + return null; + } + + return { + text: postData.isDecodedAsText ? postData.data : btoa(postData.data), + isBase64: !postData.isDecodedAsText, + }; + } + + #updatePostData() { + const sentBody = this.#readPostDataFromRequestAsUTF8(); + if (sentBody) { + this.#postData = sentBody; + this.#postDataSize = sentBody.text.length; + } else { + this.#postData = null; + this.#postDataSize = 0; + } + } } diff --git a/remote/shared/NetworkResponse.sys.mjs b/remote/shared/NetworkResponse.sys.mjs @@ -6,6 +6,8 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { NetworkUtils: "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", + + NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs", }); /** @@ -157,34 +159,37 @@ export class NetworkResponse { ); } - async readResponseBody() { - return this.#responseBodyReady.promise; - } + /** + * Returns the NetworkDataBytes instance representing the response body for + * this response. + * + * @returns {NetworkDataBytes} + */ + readAndProcessResponseBody = async () => { + const responseContent = await this.#responseBodyReady.promise; + + return new lazy.NetworkDataBytes({ + getBytesValue: async () => { + if (responseContent.isContentEncoded) { + return lazy.NetworkUtils.decodeResponseChunks( + responseContent.encodedData, + { + // Should always attempt to decode as UTF-8. + charset: "UTF-8", + compressionEncodings: responseContent.compressionEncodings, + encodedBodySize: responseContent.encodedBodySize, + encoding: responseContent.encoding, + } + ); + } + return responseContent.text; + }, + isBase64: responseContent.encoding === "base64", + }); + }; setResponseContent(responseContent) { - // Extract the properties necessary to decode the response body later on. - let encodedResponseBody; - - if (responseContent.isContentEncoded) { - encodedResponseBody = { - encoding: responseContent.encoding, - getDecodedResponseBody: async () => - lazy.NetworkUtils.decodeResponseChunks(responseContent.encodedData, { - // Should always attempt to decode as UTF-8. - charset: "UTF-8", - compressionEncodings: responseContent.compressionEncodings, - encodedBodySize: responseContent.encodedBodySize, - encoding: responseContent.encoding, - }), - }; - } else { - encodedResponseBody = { - encoding: responseContent.encoding, - getDecodedResponseBody: () => responseContent.text, - }; - } - - this.#responseBodyReady.resolve(encodedResponseBody); + this.#responseBodyReady.resolve(responseContent); } /** diff --git a/remote/webdriver-bidi/modules/root/network.sys.mjs b/remote/webdriver-bidi/modules/root/network.sys.mjs @@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, { matchURLPattern: "chrome://remote/content/shared/webdriver/URLPattern.sys.mjs", NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", + NetworkDataBytes: "chrome://remote/content/shared/NetworkDataBytes.sys.mjs", NetworkDecodedBodySizeMap: "chrome://remote/content/shared/NetworkDecodedBodySizeMap.sys.mjs", NetworkListener: @@ -161,6 +162,7 @@ const ContinueWithAuthAction = { * @enum {DataType} */ const DataType = { + Request: "request", Response: "response", }; @@ -1283,11 +1285,10 @@ class NetworkModule extends RootBiDiModule { ); } - const value = await collectedData.bytes.getDecodedResponseBody(); - const type = - collectedData.bytes.encoding === "base64" - ? BytesValueType.Base64 - : BytesValueType.String; + const value = await collectedData.bytes.getBytesValue(); + const type = collectedData.bytes.isBase64 + ? BytesValueType.Base64 + : BytesValueType.String; if (disown) { this.#removeCollectorFromData(collectedData, collector); @@ -1918,6 +1919,37 @@ class NetworkModule extends RootBiDiModule { } } + #cloneNetworkRequestBody(request) { + if (!this.#networkCollectors.size) { + return; + } + + // If request body is missing or null, do not store any collected data. + if (!request.postData || request.postData === null) { + return; + } + + const collectedData = { + bytes: null, + collectors: new Set(), + pending: true, + // This allows to implement the await/resume on "network data collected" + // described in the specification. + networkDataCollected: Promise.withResolvers(), + request: request.requestId, + size: null, + type: DataType.Request, + }; + + // The actual cloning is already handled by the DevTools + // NetworkResponseListener, here we just have to prepare the networkData and + // add it to the array. + this.#collectedNetworkData.set( + `${request.requestId}-${DataType.Request}`, + collectedData + ); + } + #cloneNetworkResponseBody(request) { if (!this.#networkCollectors.size) { return; @@ -1925,7 +1957,6 @@ class NetworkModule extends RootBiDiModule { const collectedData = { bytes: null, - // Note: The specification expects a `clonedBody` property on // The cloned body is fully handled by DevTools' NetworkResponseListener // so it will not explicitly be stored here. collectors: new Set(), @@ -2287,7 +2318,32 @@ class NetworkModule extends RootBiDiModule { } /** - * Implements https://w3c.github.io/webdriver-bidi/#maybe-collect-network-response-body + * Implements https://w3c.github.io/webdriver-bidi/#maybe-collect-network-request-body + * + * @param {NetworkRequest} request + * The request object for which we want to collect the body. + */ + async #maybeCollectNetworkRequestBody(request) { + const collectedData = this.#getCollectedData( + request.requestId, + DataType.Request + ); + + if (collectedData === null) { + return; + } + + this.#maybeCollectNetworkData({ + collectedData, + dataType: DataType.Request, + request, + readAndProcessBodyFn: request.readAndProcessRequestBody, + size: request.postDataSize, + }); + } + + /** + * Implements https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-response-body * * @param {NetworkRequest} request * The request object for which we want to collect the body. @@ -2322,12 +2378,69 @@ class NetworkModule extends RootBiDiModule { return; } + let readAndProcessBodyFn, size; + if (response.isDataURL) { + // Handle data URLs as a special case since the response is not provided + // by the DevTools ResponseListener in this case. + const url = request.serializedURL; + const body = url.substring(url.indexOf(",") + 1); + const isText = + response.mimeType && + lazy.NetworkHelper.isTextMimeType(response.mimeType); + + readAndProcessBodyFn = () => + new lazy.NetworkDataBytes({ + getBytesValue: () => body, + isBase64: !isText, + }); + size = body.length; + } else { + readAndProcessBodyFn = response.readAndProcessResponseBody; + size = response.encodedBodySize; + } + + this.#maybeCollectNetworkData({ + collectedData, + dataType: DataType.Response, + request, + readAndProcessBodyFn, + size, + }); + } + + /** + * Implements https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-data + * + * @param {object} options + * @param {Data} options.collectedData + * @param {DataType} options.dataType + * @param {NetworkRequest} options.request + * @param {Function} options.readAndProcessBodyFn + * @param {number} options.size + */ + async #maybeCollectNetworkData(options) { + const { + collectedData, + dataType, + request, + // Note: this parameter is not present in + // https://www.w3.org/TR/webdriver-bidi/#maybe-collect-network-data + // Each caller is responsible for providing a callable which will return + // a NetworkDataBytes instance corresponding to the collected data. + readAndProcessBodyFn, + // Note: the spec assumes that in some cases the size can be computed + // dynamically. But in practice we might be storing encoding data in a + // format which makes it hard to get the size. So here we always expect + // callers to provide a size. + size, + } = options; + const browsingContext = lazy.NavigableManager.getBrowsingContextById( request.contextId ); if (!browsingContext) { lazy.logger.trace( - `Network data not collected for request "${request.requestId}" and data type "${DataType.Response}"` + + `Network data not collected for request "${request.requestId}" and data type "${dataType}"` + `: navigable no longer available` ); collectedData.pending = false; @@ -2342,7 +2455,7 @@ class NetworkModule extends RootBiDiModule { let collectors = []; for (const [, collector] of this.#networkCollectors) { if ( - collector.dataTypes.includes(DataType.Response) && + collector.dataTypes.includes(dataType) && this.#matchCollectorForNavigable(collector, topNavigable) ) { collectors.push(collector); @@ -2351,7 +2464,7 @@ class NetworkModule extends RootBiDiModule { if (!collectors.length) { lazy.logger.trace( - `Network data not collected for request "${request.requestId}" and data type "${DataType.Response}"` + + `Network data not collected for request "${request.requestId}" and data type "${dataType}"` + `: no matching collector` ); collectedData.pending = false; @@ -2363,32 +2476,16 @@ class NetworkModule extends RootBiDiModule { } let bytes = null; - let size = null; // At this point, the specification expects to processBody for the cloned - // body. Since this is handled by the DevTools NetworkResponseListener, so - // here we wait until the response content is set. + // body. Here we do not explicitly clone the bodies. + // For responses, DevTools' NetworkResponseListener clones the stream. + // For requests, NetworkHelper.readPostTextFromRequest clones the stream on + // the fly to read it as text. try { - if (response.isDataURL) { - // Handle data URLs as a special case since the response is not provided - // by the DevTools ResponseListener in this case. - const url = request.serializedURL; - const body = url.substring(url.indexOf(",") + 1); - const isText = - response.mimeType && - lazy.NetworkHelper.isTextMimeType(response.mimeType); - // TODO: Reuse a common interface being introduced in Bug 1988955. - bytes = { - getDecodedResponseBody: () => body, - encoding: isText ? null : "base64", - }; - size = body.length; - } else { - const bytesOrNull = await response.readResponseBody(); - if (bytesOrNull !== null) { - bytes = bytesOrNull; - size = response.encodedBodySize; - } + const bytesOrNull = await readAndProcessBodyFn(); + if (bytesOrNull !== null) { + bytes = bytesOrNull; } } catch { // Let processBodyError be this step: Do nothing. @@ -2515,6 +2612,11 @@ class NetworkModule extends RootBiDiModule { return; } + // Make sure a collected data is created for the request. + // Note: this is supposed to be triggered from fetch and doesn't depend on + // whether network events are used or not. + this.#cloneNetworkRequestBody(request); + const relatedNavigables = [browsingContext]; this.#updateRequestHeaders(request, relatedNavigables); @@ -2529,6 +2631,8 @@ class NetworkModule extends RootBiDiModule { return; } + this.#maybeCollectNetworkRequestBody(request); + const baseParameters = this.#processNetworkEvent( protocolEventName, request