Bug 1848156 - [bidi] Suspend requests which match an intercept on BeforeRequestSent or ResponseStarted r=webdriver-reviewers,whimboo

Depends on D185997

Differential Revision: https://phabricator.services.mozilla.com/D185456
This commit is contained in:
Julian Descottes 2023-09-07 18:44:16 +00:00
parent 40f8df623f
commit 69f0469916
2 changed files with 216 additions and 47 deletions

View file

@ -18,14 +18,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
* NetworkListener instance which created it.
*/
export class NetworkEventRecord {
#channel;
#contextId;
#fromCache;
#isMainDocumentChannel;
#networkListener;
#redirectCount;
#requestChannel;
#requestData;
#requestId;
#responseChannel;
#responseData;
#wrappedChannel;
@ -40,7 +41,9 @@ export class NetworkEventRecord {
* The NetworkListener which created this NetworkEventRecord.
*/
constructor(networkEvent, channel, networkListener) {
this.#channel = channel;
this.#requestChannel = channel;
this.#responseChannel = null;
this.#fromCache = networkEvent.fromCache;
this.#isMainDocumentChannel = channel.isMainDocumentChannel;
@ -122,6 +125,8 @@ export class NetworkEventRecord {
*/
addResponseStart(options) {
const { channel, fromCache, rawHeaders = "" } = options;
this.#responseChannel = channel;
const { headers } =
lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel);
@ -232,6 +237,7 @@ export class NetworkEventRecord {
this.#networkListener.emit("before-request-sent", {
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
requestChannel: this.#requestChannel,
redirectCount: this.#redirectCount,
requestData: this.#requestData,
timestamp: Date.now(),
@ -245,7 +251,9 @@ export class NetworkEventRecord {
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
responseChannel: this.#responseChannel,
responseData: this.#responseData,
timestamp: Date.now(),
});
@ -258,7 +266,9 @@ export class NetworkEventRecord {
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
responseChannel: this.#responseChannel,
responseData: this.#responseData,
timestamp: Date.now(),
});
@ -288,7 +298,9 @@ export class NetworkEventRecord {
}
#getBrowsingContext() {
const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel);
const id = lazy.NetworkUtils.getChannelBrowsingContextID(
this.#requestChannel
);
return BrowsingContext.get(id);
}
@ -314,7 +326,7 @@ export class NetworkEventRecord {
try {
mimeType = this.#wrappedChannel.contentType;
const contentCharset = this.#channel.contentCharset;
const contentCharset = this.#requestChannel.contentCharset;
if (contentCharset) {
mimeType += `;charset=${contentCharset}`;
}
@ -379,7 +391,9 @@ export class NetworkEventRecord {
* any event from this class.
*/
#updateDataFromTimedChannel() {
const timedChannel = this.#channel.QueryInterface(Ci.nsITimedChannel);
const timedChannel = this.#requestChannel.QueryInterface(
Ci.nsITimedChannel
);
this.#redirectCount = timedChannel.redirectCount;
this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel);
}

View file

@ -10,6 +10,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
matchURLPattern:
"chrome://remote/content/shared/webdriver/URLPattern.sys.mjs",
notifyNavigationStarted:
"chrome://remote/content/shared/NavigationManager.sys.mjs",
NetworkListener:
@ -24,12 +26,20 @@ ChromeUtils.defineESModuleGetters(lazy, {
/**
* @typedef {object} BaseParameters
* @property {string=} context
* @property {Array<string>?} intercepts
* @property {boolean} isBlocked
* @property {Navigation=} navigation
* @property {number} redirectCount
* @property {RequestData} request
* @property {number} timestamp
*/
/**
* @typedef {object} BlockedRequest
* @property {NetworkEventRecord} networkEventRecord
* @property {InterceptPhase} phase
*/
/**
* Enum of possible BytesValue types.
*
@ -223,6 +233,7 @@ const InterceptPhase = {
/* eslint-enable jsdoc/valid-types */
class NetworkModule extends Module {
#blockedRequests;
#interceptMap;
#networkListener;
#subscribedEvents;
@ -230,6 +241,9 @@ class NetworkModule extends Module {
constructor(messageHandler) {
super(messageHandler);
// Map of request id to BlockedRequest
this.#blockedRequests = new Map();
// Map of intercept id to InterceptProperties
this.#interceptMap = new Map();
@ -248,6 +262,7 @@ class NetworkModule extends Module {
this.#networkListener.off("response-started", this.#onResponseEvent);
this.#networkListener.destroy();
this.#blockedRequests = null;
this.#interceptMap = null;
this.#subscribedEvents = null;
}
@ -359,24 +374,67 @@ class NetworkModule extends Module {
};
}
#getOrCreateNavigationId(browsingContext, url) {
const navigation =
#getNetworkIntercepts(event, requestData) {
const intercepts = [];
let phase;
switch (event) {
case "network.beforeRequestSent":
phase = InterceptPhase.BeforeRequestSent;
break;
case "network.responseStarted":
phase = InterceptPhase.ResponseStarted;
break;
case "network.authRequired":
phase = InterceptPhase.AuthRequired;
break;
case "network.responseCompleted":
// The network.responseCompleted event does not match any interception
// phase. Return immediately.
return intercepts;
}
const url = requestData.url;
for (const [interceptId, intercept] of this.#interceptMap) {
if (intercept.phases.includes(phase)) {
const urlPatterns = intercept.urlPatterns;
if (
!urlPatterns.length ||
urlPatterns.some(pattern => lazy.matchURLPattern(pattern, url))
) {
intercepts.push(interceptId);
}
}
}
return intercepts;
}
#getNavigationId(eventName, isNavigationRequest, browsingContext, url) {
if (!isNavigationRequest) {
// Not a navigation request return null.
return null;
}
let navigation =
this.messageHandler.navigationManager.getNavigationForBrowsingContext(
browsingContext
);
// Check if an ongoing navigation is available for this browsing context.
// onBeforeRequestSent might be too early for the NavigationManager.
// `onBeforeRequestSent` might be too early for the NavigationManager.
// If there is no ongoing navigation, create one ourselves.
// TODO: Bug 1835704 to detect navigations earlier and avoid this.
if (navigation && !navigation.finished) {
return navigation.navigationId;
if (
eventName === "network.beforeRequestSent" &&
(!navigation || navigation.finished)
) {
navigation = lazy.notifyNavigationStarted({
contextDetails: { context: browsingContext },
url,
});
}
// No ongoing navigation for this browsing context, create a new one.
return lazy.notifyNavigationStarted({
contextDetails: { context: browsingContext },
url,
}).navigationId;
return navigation ? navigation.navigationId : null;
}
#onBeforeRequestSent = (name, data) => {
@ -384,39 +442,61 @@ class NetworkModule extends Module {
contextId,
isNavigationRequest,
redirectCount,
requestChannel,
requestData,
timestamp,
} = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const protocolEventName = "network.beforeRequestSent";
const isListening = this.messageHandler.eventsDispatcher.hasListener(
protocolEventName,
{ contextId }
);
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
// bail out.
return;
}
const baseParameters = this.#processNetworkEvent(protocolEventName, {
contextId,
isNavigationRequest,
redirectCount,
requestData,
timestamp,
});
// Bug 1805479: Handle the initiator, including stacktrace details.
const initiator = {
type: InitiatorType.Other,
};
const navigationId = isNavigationRequest
? this.#getOrCreateNavigationId(browsingContext, requestData.url)
: null;
const baseParameters = {
context: contextId,
navigation: navigationId,
redirectCount,
request: requestData,
timestamp,
};
const beforeRequestSentEvent = this.#serializeNetworkEvent({
...baseParameters,
initiator,
});
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
this.emitEvent(
"network.beforeRequestSent",
protocolEventName,
beforeRequestSentEvent,
this.#getContextInfo(browsingContext)
);
if (beforeRequestSentEvent.isBlocked) {
// TODO: Requests suspended in beforeRequestSent still reach the server at
// the moment. https://bugzilla.mozilla.org/show_bug.cgi?id=1849686
requestChannel.suspend();
this.#blockedRequests.set(beforeRequestSentEvent.request.request, {
request: requestChannel,
phase: InterceptPhase.BeforeRequestSent,
});
// TODO: Once we implement network.continueRequest, we should create a
// promise here which will wait until the request is resumed and removes
// the request from the blockedRequests. See Bug 1850680.
}
};
#onResponseEvent = (name, data) => {
@ -424,44 +504,119 @@ class NetworkModule extends Module {
contextId,
isNavigationRequest,
redirectCount,
requestChannel,
requestData,
responseChannel,
responseData,
timestamp,
} = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const protocolEventName =
name === "response-started"
? "network.responseStarted"
: "network.responseCompleted";
const isListening = this.messageHandler.eventsDispatcher.hasListener(
protocolEventName,
{ contextId }
);
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
// bail out.
return;
}
const navigation = isNavigationRequest
? this.messageHandler.navigationManager.getNavigationForBrowsingContext(
browsingContext
)
: null;
const baseParameters = {
context: contextId,
navigation: navigation ? navigation.navigationId : null,
const baseParameters = this.#processNetworkEvent(protocolEventName, {
contextId,
isNavigationRequest,
redirectCount,
request: requestData,
requestData,
timestamp,
};
});
const responseEvent = this.#serializeNetworkEvent({
...baseParameters,
response: responseData,
});
const protocolEventName =
name === "response-started"
? "network.responseStarted"
: "network.responseCompleted";
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
this.emitEvent(
protocolEventName,
responseEvent,
this.#getContextInfo(browsingContext)
);
if (
protocolEventName === "network.responseStarted" &&
responseEvent.isBlocked
) {
requestChannel.suspend();
this.#blockedRequests.set(responseEvent.request.request, {
request: requestChannel,
response: responseChannel,
phase: InterceptPhase.ResponseStarted,
});
// TODO: Once we implement network.continueRequest, we should create a
// promise here which will wait until the request is resumed and removes
// the request from the blockedRequests. See Bug 1850680.
}
};
/**
* Process the network event data for a given network event name and create
* the corresponding base parameters.
*
* @param {string} eventName
* One of the supported network event names.
* @param {object} data
* @param {string} data.contextId
* The browsing context id for the network event.
* @param {boolean} data.isNavigationRequest
* True if the network event is related to a navigation request.
* @param {number} data.redirectCount
* The redirect count for the network event.
* @param {RequestData} data.requestData
* The network.RequestData information for the network event.
* @param {number} data.timestamp
* The timestamp when the network event was created.
*/
#processNetworkEvent(eventName, data) {
const {
contextId,
isNavigationRequest,
redirectCount,
requestData,
timestamp,
} = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const navigation = this.#getNavigationId(
eventName,
isNavigationRequest,
browsingContext,
requestData.url
);
const intercepts = this.#getNetworkIntercepts(eventName, requestData);
const isBlocked = !!intercepts.length;
const baseParameters = {
context: contextId,
isBlocked,
navigation,
redirectCount,
request: requestData,
timestamp,
};
if (isBlocked) {
baseParameters.intercepts = intercepts;
}
return baseParameters;
}
#serializeHeadersOrCookies(headersOrCookies) {
return headersOrCookies.map(item => ({
name: item.name,