Bug 1790368 - [bidi] Implement basic support for network.beforeRequestSent event r=webdriver-reviewers,Sasha,whimboo

Depends on D164147

Differential Revision: https://phabricator.services.mozilla.com/D162037
This commit is contained in:
Julian Descottes 2022-12-16 10:53:50 +00:00
Родитель 51f558b426
Коммит 7d99823762
6 изменённых файлов: 528 добавлений и 0 удалений

Просмотреть файл

@ -31,6 +31,8 @@ remote.jar:
content/shared/listeners/ConsoleAPIListener.sys.mjs (shared/listeners/ConsoleAPIListener.sys.mjs)
content/shared/listeners/ConsoleListener.sys.mjs (shared/listeners/ConsoleListener.sys.mjs)
content/shared/listeners/LoadListener.sys.mjs (shared/listeners/LoadListener.sys.mjs)
content/shared/listeners/NetworkEventRecord.sys.mjs (shared/listeners/NetworkEventRecord.sys.mjs)
content/shared/listeners/NetworkListener.sys.mjs (shared/listeners/NetworkListener.sys.mjs)
# shared modules (messagehandler architecture)
content/shared/messagehandler/Errors.sys.mjs (shared/messagehandler/Errors.sys.mjs)

Просмотреть файл

@ -0,0 +1,190 @@
/* 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, {
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
/**
* 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 {
#contextId;
#channel;
#networkListener;
#requestData;
#requestId;
#responseData;
/**
*
* @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.
*/
constructor(networkEvent, channel, networkListener) {
const browsingContext = BrowsingContext.get(networkEvent.browsingContextID);
this.#contextId = lazy.TabManager.getIdForBrowsingContext(browsingContext);
this.#channel = channel;
this.#networkListener = networkListener;
// The wrappedChannel id remains identical across redirects, whereas
// nsIChannel.channelId is different for each and every request.
const wrappedChannel = ChannelWrapper.get(channel);
this.#requestId = wrappedChannel.id.toString();
this.#requestData = {
bodySize: 0,
cookies: [],
headers: [],
headersSize: networkEvent.headersSize || null,
method: networkEvent.method || null,
request: this.#requestId,
timings: {},
url: networkEvent.url || null,
};
}
/**
* This method has to be defined to match the event owner API of the
* NetworkObserver. It will only be called once per network event record, so
* despite the name we will simply store the headers and rawHeaders.
*
* Set network request headers.
*
* @param {Array} headers
* The request headers array.
* @param {string=} rawHeaders
* The raw headers source.
*/
addRequestHeaders(headers, rawHeaders) {
this.#requestData.headers = headers;
this.#requestData.rawHeaders = rawHeaders;
}
/**
* Set network request cookies.
*
* This method has to be defined to match the event owner API of the
* NetworkObserver. It will only be called once per network event record, so
* despite the name we will simply store the cookies.
*
* @param {Array} cookies
* The request cookies array.
*/
addRequestCookies(cookies) {
this.#requestData.cookies = cookies;
// By design, the NetworkObserver will synchronously create a "network event"
// then call addRequestHeaders and finally addRequestCookies.
// 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();
}
// Expected network event owner API from the NetworkObserver.
addEventTimings() {}
addRequestPostData() {}
addResponseCache() {}
addResponseContent() {}
addResponseCookies() {}
addResponseHeaders() {}
addResponseStart() {}
addSecurityInfo() {}
addServerTimings() {}
#emitBeforeRequestSent() {
const timedChannel = this.#channel.QueryInterface(Ci.nsITimedChannel);
this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel);
this.#networkListener.emit("before-request-sent", {
contextId: this.#contextId,
redirectCount: timedChannel.redirectCount,
requestData: this.#requestData,
requestId: this.#requestId,
timestamp: Date.now(),
});
}
/**
* Convert the provided request timing to a timing relative to the beginning
* of the request. All timings are numbers representing high definition
* timestamps.
*
* @param {number} timing
* High definition timestamp for a request timing relative from the time
* origin.
* @param {number} requestTime
* High definition timestamp for the request start time relative from the
* time origin.
* @returns {number} High definition timestamp for the request timing relative
* to the start time of the request, or 0 if the provided timing was 0.
*/
#convertTimestamp(timing, requestTime) {
if (timing == 0) {
return 0;
}
return timing - requestTime;
}
#getTimingsFromTimedChannel(timedChannel) {
const {
channelCreationTime,
redirectStartTime,
redirectEndTime,
dispatchFetchEventStartTime,
cacheReadStartTime,
domainLookupStartTime,
domainLookupEndTime,
connectStartTime,
connectEndTime,
secureConnectionStartTime,
requestStartTime,
responseStartTime,
responseEndTime,
} = timedChannel;
// fetchStart should be the post-redirect start time, which should be the
// first non-zero timing from: dispatchFetchEventStart, cacheReadStart and
// domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model
const fetchStartTime =
dispatchFetchEventStartTime ||
cacheReadStartTime ||
domainLookupStartTime;
// Bug 1805478: Per spec, the origin time should match Performance API's
// originTime for the global which initiated the request. This is not
// available in the parent process, so for now we will use 0.
const originTime = 0;
return {
originTime,
requestTime: this.#convertTimestamp(channelCreationTime, originTime),
redirectStart: this.#convertTimestamp(redirectStartTime, originTime),
redirectEnd: this.#convertTimestamp(redirectEndTime, originTime),
fetchStart: this.#convertTimestamp(fetchStartTime, originTime),
dnsStart: this.#convertTimestamp(domainLookupStartTime, originTime),
dnsEnd: this.#convertTimestamp(domainLookupEndTime, originTime),
connectStart: this.#convertTimestamp(connectStartTime, originTime),
connectEnd: this.#convertTimestamp(connectEndTime, originTime),
tlsStart: this.#convertTimestamp(secureConnectionStartTime, originTime),
tlsEnd: this.#convertTimestamp(connectEndTime, originTime),
requestStart: this.#convertTimestamp(requestStartTime, originTime),
responseStart: this.#convertTimestamp(responseStartTime, originTime),
responseEnd: this.#convertTimestamp(responseEndTime, originTime),
};
}
}

Просмотреть файл

@ -0,0 +1,104 @@
/* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NetworkEventRecord:
"chrome://remote/content/shared/listeners/NetworkEventRecord.sys.mjs",
NetworkObserver:
"resource://devtools/shared/network-observer/NetworkObserver.sys.mjs",
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.jsm",
});
/**
* The NetworkListener listens to all network activity from the parent
* process.
*
* Example:
* ```
* const listener = new NetworkListener();
* listener.on("before-request-sent", onBeforeRequestSent);
* listener.startListening();
*
* const onBeforeRequestSent = (eventName, data = {}) => {
* const { cntextId, redirectCount, requestData, requestId, timestamp } = data;
* ...
* };
* ```
*
* @emits before-request-sent
* The NetworkListener emits "before-request-sent" events, with the
* following object as payload:
* - {number} browsingContextId - The browsing context id of the browsing
* context where this request was performed.
* - {number} redirectCount - The request's redirect count.
* - {RequestData} requestData - The request's data as expected by
* WebDriver BiDi.
* - {string} requestId - The id of the request, consistent across
* redirects.
* - {number} timestamp - Timestamp when the event was generated.
*/
export class NetworkListener {
#devtoolsNetworkObserver;
#listening;
constructor() {
lazy.EventEmitter.decorate(this);
this.#listening = false;
}
destroy() {
this.stopListening();
this.#devtoolsNetworkObserver.destroy();
}
startListening() {
if (this.#listening) {
return;
}
this.#devtoolsNetworkObserver = new lazy.NetworkObserver({
ignoreChannelFunction: this.#ignoreChannelFunction,
onNetworkEvent: this.#onNetworkEvent,
});
this.#listening = true;
}
stopListening() {
if (!this.#listening) {
return;
}
this.#devtoolsNetworkObserver.destroy();
this.#devtoolsNetworkObserver = null;
this.#listening = false;
}
#ignoreChannelFunction = channel => {
// Ignore chrome-privileged or DevTools-initiated requests
if (
channel.loadInfo?.loadingDocument === null &&
(channel.loadInfo.loadingPrincipal ===
Services.scriptSecurityManager.getSystemPrincipal() ||
channel.loadInfo.isInDevToolsContext)
) {
return true;
}
return false;
};
#onNetworkEvent = (networkEvent, httpActivity) => {
return new lazy.NetworkEventRecord(networkEvent, httpActivity, this);
};
}

Просмотреть файл

@ -17,6 +17,7 @@ remote.jar:
# WebDriver BiDi root modules
content/webdriver-bidi/modules/root/browsingContext.sys.mjs (modules/root/browsingContext.sys.mjs)
content/webdriver-bidi/modules/root/log.sys.mjs (modules/root/log.sys.mjs)
content/webdriver-bidi/modules/root/network.sys.mjs (modules/root/network.sys.mjs)
content/webdriver-bidi/modules/root/script.sys.mjs (modules/root/script.sys.mjs)
content/webdriver-bidi/modules/root/session.sys.mjs (modules/root/session.sys.mjs)

Просмотреть файл

@ -13,6 +13,8 @@ ChromeUtils.defineESModuleGetters(modules.root, {
browsingContext:
"chrome://remote/content/webdriver-bidi/modules/root/browsingContext.sys.mjs",
log: "chrome://remote/content/webdriver-bidi/modules/root/log.sys.mjs",
network:
"chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
script: "chrome://remote/content/webdriver-bidi/modules/root/script.sys.mjs",
session:
"chrome://remote/content/webdriver-bidi/modules/root/session.sys.mjs",

Просмотреть файл

@ -0,0 +1,229 @@
/* 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/. */
import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NetworkListener:
"chrome://remote/content/shared/listeners/NetworkListener.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
WindowGlobalMessageHandler:
"chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
});
/**
* @typedef {Object} BaseParameters
* @property {string=} context
* @property {boolean} isRedirect
* @property {Navigation=} navigation
* @property {RequestData} request
* @property {number} timestamp
*/
/**
* @typedef {Object} Cookie
* @property {Array<number>=} binaryValue
* @property {string} domain
* @property {number=} expires
* @property {boolean} httpOnly
* @property {string} name
* @property {string} path
* @property {('lax' | 'none' | 'strict')} sameSite
* @property {boolean} secure
* @property {number} size
* @property {string=} value
*/
/**
* @typedef {Object} FetchTimingInfo
* @property {number} originTime
* @property {number} requestTime
* @property {number} redirectStart
* @property {number} redirectEnd
* @property {number} fetchStart
* @property {number} dnsStart
* @property {number} dnsEnd
* @property {number} connectStart
* @property {number} connectEnd
* @property {number} tlsStart
* @property {number} requestStart
* @property {number} responseStart
* @property {number} responseEnd
*/
/**
* @typedef {Object} Header
* @property {Array<number>=} binaryValue
* @property {string} name
* @property {string=} value
*/
/**
* @typedef {string} InitiatorType
**/
/**
* Enum of possible initiator types.
*
* @readonly
* @enum {InitiatorType}
**/
const InitiatorType = {
Other: "other",
Parser: "parser",
Preflight: "preflight",
Script: "script",
};
/**
* @typedef {Object} Initiator
* @property {InitiatorType} type
* @property {number=} columnNumber
* @property {number=} lineNumber
* @property {string=} request
* @property {StackTrace=} stackTrace
*/
/**
* @typedef {Object} RequestData
* @property {number} bodySize
* @property {Array<Cookie>} cookies
* @property {Array<Header>} headers
* @property {number} headersSize
* @property {string} method
* @property {string} request
* @property {FetchTimingInfo} timings
* @property {string} url
*/
/**
* @typedef {Object} BeforeRequestSentParametersProperties
* @property {Initiator} initiator
*/
/**
* Parameters for the BeforeRequestSent event
*
* @typedef {BaseParameters & BeforeRequestSentParametersProperties} BeforeRequestSentParameters
*/
class NetworkModule extends Module {
#beforeRequestSentMap;
#networkListener;
#subscribedEvents;
constructor(messageHandler) {
super(messageHandler);
// Map of request ids to redirect counts. A WebDriver BiDi request id is
// identical for redirects of a given request, this map allows to know if we
// already emitted a beforeRequestSent event for a given request with a
// specific redirectCount.
this.#beforeRequestSentMap = new Map();
this.#networkListener = new lazy.NetworkListener(messageHandler);
// Set of event names which have active subscriptions
this.#subscribedEvents = new Set();
this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent);
}
destroy() {
this.#networkListener.off("before-request-sent", this.#onBeforeRequestSent);
this.#beforeRequestSentMap = null;
this.#subscribedEvents = null;
}
#getContextInfo(browsingContext) {
return {
contextId: browsingContext.id,
type: lazy.WindowGlobalMessageHandler.type,
};
}
#onBeforeRequestSent = (name, data) => {
const { contextId, requestData, timestamp, redirectCount } = data;
const isRedirect = redirectCount > 0;
this.#beforeRequestSentMap.set(requestData.requestId, redirectCount);
// Bug 1805479: Handle the initiator, including stacktrace details.
const initiator = {
type: InitiatorType.Other,
};
const baseParameters = {
context: contextId,
isRedirect,
redirectCount,
// Bug 1805405: Handle the navigation id.
navigation: null,
request: requestData,
timestamp,
};
const beforeRequestSentEvent = {
...baseParameters,
initiator,
};
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
this.emitEvent(
"network.beforeRequestSent",
beforeRequestSentEvent,
this.#getContextInfo(browsingContext)
);
};
#startListening(event) {
if (this.#subscribedEvents.size == 0) {
this.#networkListener.startListening();
}
this.#subscribedEvents.add(event);
}
#stopListening(event) {
this.#subscribedEvents.delete(event);
if (this.#subscribedEvents.size == 0) {
this.#networkListener.stopListening();
}
}
#subscribeEvent(event) {
if (this.constructor.supportedEvents.includes(event)) {
this.#startListening(event);
}
}
#unsubscribeEvent(event) {
if (this.constructor.supportedEvents.includes(event)) {
this.#stopListening(event);
}
}
/**
* Internal commands
*/
_applySessionData(params) {
const { category, added = [], removed = [] } = params;
if (category === "event") {
for (const event of added) {
this.#subscribeEvent(event);
}
for (const event of removed) {
this.#unsubscribeEvent(event);
}
}
}
static get supportedEvents() {
return ["network.beforeRequestSent"];
}
}
export const network = NetworkModule;