Bug 1893117 - [remote] Introduce NetworkRequest and NetworkResponse classes r=webdriver-reviewers,Sasha

Differential Revision: https://phabricator.services.mozilla.com/D208450
This commit is contained in:
Julian Descottes 2024-04-25 16:20:42 +00:00
parent 8a79370a86
commit f324714930
7 changed files with 641 additions and 535 deletions

View File

@ -23,6 +23,8 @@ remote.jar:
content/shared/MobileTabBrowser.sys.mjs (shared/MobileTabBrowser.sys.mjs)
content/shared/Navigate.sys.mjs (shared/Navigate.sys.mjs)
content/shared/NavigationManager.sys.mjs (shared/NavigationManager.sys.mjs)
content/shared/NetworkRequest.sys.mjs (shared/NetworkRequest.sys.mjs)
content/shared/NetworkResponse.sys.mjs (shared/NetworkResponse.sys.mjs)
content/shared/PDF.sys.mjs (shared/PDF.sys.mjs)
content/shared/Prompt.sys.mjs (shared/Prompt.sys.mjs)
content/shared/Realm.sys.mjs (shared/Realm.sys.mjs)

View File

@ -0,0 +1,254 @@
/* 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, {
NetworkUtils:
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
notifyNavigationStarted:
"chrome://remote/content/shared/NavigationManager.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});
/**
* The NetworkRequest class is a wrapper around the internal channel which
* provides getters and methods closer to fetch's response concept
* (https://fetch.spec.whatwg.org/#concept-response).
*/
export class NetworkRequest {
#channel;
#contextId;
#navigationId;
#navigationManager;
#postData;
#rawHeaders;
#redirectCount;
#requestId;
#timedChannel;
#wrappedChannel;
/**
*
* @param {nsIChannel} channel
* The channel for the request.
* @param {object} params
* @param {NavigationManager} params.navigationManager
* The NavigationManager where navigations for the current session are
* monitored.
* @param {string=} params.rawHeaders
* The request's raw (ie potentially compressed) headers
*/
constructor(channel, params) {
const { navigationManager, rawHeaders = "" } = params;
this.#channel = channel;
this.#navigationManager = navigationManager;
this.#rawHeaders = rawHeaders;
this.#timedChannel = this.#channel.QueryInterface(Ci.nsITimedChannel);
this.#wrappedChannel = ChannelWrapper.get(channel);
this.#redirectCount = this.#timedChannel.redirectCount;
// The wrappedChannel id remains identical across redirects, whereas
// nsIChannel.channelId is different for each and every request.
this.#requestId = this.#wrappedChannel.id.toString();
this.#contextId = this.#getContextId();
this.#navigationId = this.#getNavigationId();
}
get contextId() {
return this.#contextId;
}
get errorText() {
// TODO: Update with a proper error text. Bug 1873037.
return ChromeUtils.getXPCOMErrorName(this.#channel.status);
}
get headersSize() {
// TODO: rawHeaders will not be updated after modifying the headers via
// request interception. Need to find another way to retrieve the
// information dynamically.
return this.#rawHeaders.length;
}
get method() {
return this.#channel.requestMethod;
}
get navigationId() {
return this.#navigationId;
}
get postDataSize() {
return this.#postData ? this.postData.size : 0;
}
get redirectCount() {
return this.#redirectCount;
}
get requestId() {
return this.#requestId;
}
get serializedURL() {
return this.#channel.URI.spec;
}
get wrappedChannel() {
return this.#wrappedChannel;
}
/**
* Retrieve the Fetch timings for the NetworkRequest.
*
* @returns {object}
* Object with keys corresponding to fetch timing names, and their
* corresponding values.
*/
getFetchTimings() {
const {
channelCreationTime,
redirectStartTime,
redirectEndTime,
dispatchFetchEventStartTime,
cacheReadStartTime,
domainLookupStartTime,
domainLookupEndTime,
connectStartTime,
connectEndTime,
secureConnectionStartTime,
requestStartTime,
responseStartTime,
responseEndTime,
} = this.#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
// timeOrigin for the global which initiated the request. This is not
// available in the parent process, so for now we will use 0.
const timeOrigin = 0;
return {
timeOrigin,
requestTime: this.#convertTimestamp(channelCreationTime, timeOrigin),
redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin),
redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin),
fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin),
dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin),
dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin),
connectStart: this.#convertTimestamp(connectStartTime, timeOrigin),
connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin),
tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
requestStart: this.#convertTimestamp(requestStartTime, timeOrigin),
responseStart: this.#convertTimestamp(responseStartTime, timeOrigin),
responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin),
};
}
/**
* Retrieve the list of headers for the NetworkRequest.
*
* @returns {Array.Array}
* Array of (name, value) tuples.
*/
getHeadersList() {
const headers = [];
this.#channel.visitRequestHeaders({
visitHeader(name, value) {
// The `Proxy-Authorization` header even though it appears on the channel is not
// actually sent to the server for non CONNECT requests after the HTTP/HTTPS tunnel
// is setup by the proxy.
if (name == "Proxy-Authorization") {
return;
}
headers.push([name, value]);
},
});
return headers;
}
/**
* Update the postData for this NetworkRequest. This is currently forwarded
* by the DevTools' NetworkObserver.
*
* TODO: We should read this information dynamically from the channel so that
* we can get updated information in case it was modified via network
* interception.
*
* @param {object} postData
* The request POST data.
*/
setPostData(postData) {
this.#postData = postData;
}
/**
* 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;
}
#getContextId() {
const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel);
const browsingContext = BrowsingContext.get(id);
return lazy.TabManager.getIdForBrowsingContext(browsingContext);
}
#getNavigationId() {
if (!this.#channel.isMainDocumentChannel) {
return null;
}
const browsingContext = lazy.TabManager.getBrowsingContextById(
this.#contextId
);
let navigation =
this.#navigationManager.getNavigationForBrowsingContext(browsingContext);
// `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) {
navigation = lazy.notifyNavigationStarted({
contextDetails: { context: browsingContext },
url: this.serializedURL,
});
}
return navigation ? navigation.navigationId : null;
}
}

View File

@ -0,0 +1,131 @@
/* 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, {
NetworkUtils:
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
});
/**
* The NetworkResponse class is a wrapper around the internal channel which
* provides getters and methods closer to fetch's response concept
* (https://fetch.spec.whatwg.org/#concept-response).
*/
export class NetworkResponse {
#channel;
#decodedBodySize;
#encodedBodySize;
#fromCache;
#headersTransmittedSize;
#status;
#statusMessage;
#totalTransmittedSize;
#wrappedChannel;
/**
*
* @param {nsIChannel} channel
* The channel for the response.
* @param {object} params
* @param {boolean} params.fromCache
* Whether the response was read from the cache or not.
* @param {string=} params.rawHeaders
* The response's raw (ie potentially compressed) headers
*/
constructor(channel, params) {
this.#channel = channel;
const { fromCache, rawHeaders = "" } = params;
this.#fromCache = fromCache;
this.#wrappedChannel = ChannelWrapper.get(channel);
this.#decodedBodySize = 0;
this.#encodedBodySize = 0;
this.#headersTransmittedSize = rawHeaders.length;
this.#totalTransmittedSize = rawHeaders.length;
// TODO: responseStatus and responseStatusText are sometimes inconsistent.
// For instance, they might be (304, Not Modified) when retrieved during the
// responseStarted event, and then (200, OK) during the responseCompleted
// event.
// For now consider them as immutable and store them on startup.
this.#status = this.#channel.responseStatus;
this.#statusMessage = this.#channel.responseStatusText;
}
get decodedBodySize() {
return this.#decodedBodySize;
}
get encodedBodySize() {
return this.#encodedBodySize;
}
get headersTransmittedSize() {
return this.#headersTransmittedSize;
}
get fromCache() {
return this.#fromCache;
}
get protocol() {
return lazy.NetworkUtils.getProtocol(this.#channel);
}
get serializedURL() {
return this.#channel.URI.spec;
}
get status() {
return this.#status;
}
get statusMessage() {
return this.#statusMessage;
}
get totalTransmittedSize() {
return this.#totalTransmittedSize;
}
addResponseContent(responseContent) {
this.#decodedBodySize = responseContent.decodedBodySize;
this.#encodedBodySize = responseContent.bodySize;
this.#totalTransmittedSize = responseContent.transferredSize;
}
getComputedMimeType() {
// TODO: DevTools NetworkObserver is computing a similar value in
// addResponseContent, but uses an inconsistent implementation in
// addResponseStart. This approach can only be used as early as in
// addResponseHeaders. We should move this logic to the NetworkObserver and
// expose mimeType in addResponseStart. Bug 1809670.
let mimeType = "";
try {
mimeType = this.#wrappedChannel.contentType;
const contentCharset = this.#channel.contentCharset;
if (contentCharset) {
mimeType += `;charset=${contentCharset}`;
}
} catch (e) {
// Ignore exceptions when reading contentType/contentCharset
}
return mimeType;
}
getHeadersList() {
const headers = [];
this.#channel.visitOriginalResponseHeaders({
visitHeader(name, value) {
headers.push([name, value]);
},
});
return headers;
}
}

View File

@ -4,10 +4,8 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NetworkUtils:
"resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
NetworkRequest: "chrome://remote/content/shared/NetworkRequest.sys.mjs",
NetworkResponse: "chrome://remote/content/shared/NetworkResponse.sys.mjs",
});
/**
@ -18,17 +16,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
* NetworkListener instance which created it.
*/
export class NetworkEventRecord {
#contextId;
#fromCache;
#isMainDocumentChannel;
#networkListener;
#redirectCount;
#requestChannel;
#requestData;
#requestId;
#responseChannel;
#responseData;
#wrappedChannel;
#request;
#response;
/**
*
@ -39,56 +30,21 @@ export class NetworkEventRecord {
* The nsIChannel behind this network event.
* @param {NetworkListener} networkListener
* The NetworkListener which created this NetworkEventRecord.
* @param {NavigationManager} navigationManager
* The NavigationManager which belongs to the same session as this
* NetworkEventRecord.
*/
constructor(networkEvent, channel, networkListener) {
this.#requestChannel = channel;
this.#responseChannel = null;
constructor(networkEvent, channel, networkListener, navigationManager) {
this.#request = new lazy.NetworkRequest(channel, {
navigationManager,
rawHeaders: networkEvent.rawHeaders,
});
this.#response = null;
this.#fromCache = networkEvent.fromCache;
this.#isMainDocumentChannel = channel.isMainDocumentChannel;
this.#wrappedChannel = ChannelWrapper.get(channel);
this.#networkListener = networkListener;
// The context ids computed by TabManager have the lifecycle of a navigable
// and can be reused for all the events emitted from this record.
this.#contextId = this.#getContextId();
// The wrappedChannel id remains identical across redirects, whereas
// nsIChannel.channelId is different for each and every request.
this.#requestId = this.#wrappedChannel.id.toString();
const { cookies, headers } =
lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel);
// See the RequestData type definition for the full list of properties that
// should be set on this object.
this.#requestData = {
bodySize: null,
cookies,
headers,
headersSize: networkEvent.rawHeaders ? networkEvent.rawHeaders.length : 0,
method: channel.requestMethod,
request: this.#requestId,
timings: {},
url: channel.URI.spec,
};
// See the ResponseData type definition for the full list of properties that
// should be set on this object.
this.#responseData = {
// encoded size (body)
bodySize: null,
content: {
// decoded size
size: null,
},
// encoded size (headers)
headersSize: null,
url: channel.URI.spec,
};
// NetworkObserver creates a network event when request headers have been
// parsed.
// According to the BiDi spec, we should emit beforeRequestSent when adding
@ -113,8 +69,7 @@ export class NetworkEventRecord {
* The request POST data.
*/
addRequestPostData(postData) {
// Only the postData size is needed for RemoteAgent consumers.
this.#requestData.bodySize = postData.size;
this.#request.setPostData(postData);
}
/**
@ -130,25 +85,11 @@ export class NetworkEventRecord {
* @param {string} options.rawHeaders
*/
addResponseStart(options) {
const { channel, fromCache, rawHeaders = "" } = options;
this.#responseChannel = channel;
const { headers } =
lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel);
const headersSize = rawHeaders.length;
this.#responseData = {
...this.#responseData,
bodySize: 0,
bytesReceived: headersSize,
const { channel, fromCache, rawHeaders } = options;
this.#response = new lazy.NetworkResponse(channel, {
rawHeaders,
fromCache: this.#fromCache || !!fromCache,
headers,
headersSize,
mimeType: this.#getMimeType(),
protocol: lazy.NetworkUtils.getProtocol(channel),
status: channel.responseStatus,
statusText: channel.responseStatusText,
};
});
// This should be triggered when all headers have been received, matching
// the WebDriverBiDi response started trigger in `4.6. HTTP-network fetch`
@ -189,25 +130,16 @@ export class NetworkEventRecord {
*
* Required API for a NetworkObserver event owner.
*
* @param {object} response
* @param {object} responseContent
* An object which represents the response content.
* @param {object} responseInfo
* Additional meta data about the response.
*/
addResponseContent(response, responseInfo) {
// Update content-related sizes with the latest data from addResponseContent.
this.#responseData = {
...this.#responseData,
bodySize: response.bodySize,
bytesReceived: response.transferredSize,
content: {
size: response.decodedBodySize,
},
};
addResponseContent(responseContent, responseInfo) {
if (responseInfo.blockedReason) {
this.#emitFetchError();
} else {
this.#response.addResponseContent(responseContent);
this.#emitResponseCompleted();
}
}
@ -234,201 +166,37 @@ export class NetworkEventRecord {
this.#emitAuthRequired(authCallbacks);
}
/**
* 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;
}
#emitAuthRequired(authCallbacks) {
this.#updateDataFromTimedChannel();
this.#networkListener.emit("auth-required", {
authCallbacks,
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
responseChannel: this.#responseChannel,
responseData: this.#responseData,
timestamp: Date.now(),
request: this.#request,
response: this.#response,
});
}
#emitBeforeRequestSent() {
this.#updateDataFromTimedChannel();
this.#networkListener.emit("before-request-sent", {
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
timestamp: Date.now(),
request: this.#request,
});
}
#emitFetchError() {
this.#updateDataFromTimedChannel();
this.#networkListener.emit("fetch-error", {
contextId: this.#contextId,
// TODO: Update with a proper error text. Bug 1873037.
errorText: ChromeUtils.getXPCOMErrorName(this.#requestChannel.status),
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
timestamp: Date.now(),
request: this.#request,
});
}
#emitResponseCompleted() {
this.#updateDataFromTimedChannel();
this.#networkListener.emit("response-completed", {
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
responseChannel: this.#responseChannel,
responseData: this.#responseData,
timestamp: Date.now(),
request: this.#request,
response: this.#response,
});
}
#emitResponseStarted() {
this.#updateDataFromTimedChannel();
this.#networkListener.emit("response-started", {
contextId: this.#contextId,
isNavigationRequest: this.#isMainDocumentChannel,
redirectCount: this.#redirectCount,
requestChannel: this.#requestChannel,
requestData: this.#requestData,
responseChannel: this.#responseChannel,
responseData: this.#responseData,
timestamp: Date.now(),
request: this.#request,
response: this.#response,
});
}
#getBrowsingContext() {
const id = lazy.NetworkUtils.getChannelBrowsingContextID(
this.#requestChannel
);
return BrowsingContext.get(id);
}
/**
* Retrieve the navigable id for the current browsing context associated to
* the requests' channel. Network events are recorded in the parent process
* so we always expect to be able to use TabManager.getIdForBrowsingContext.
*
* @returns {string}
* The navigable id corresponding to the given browsing context.
*/
#getContextId() {
return lazy.TabManager.getIdForBrowsingContext(this.#getBrowsingContext());
}
#getMimeType() {
// TODO: DevTools NetworkObserver is computing a similar value in
// addResponseContent, but uses an inconsistent implementation in
// addResponseStart. This approach can only be used as early as in
// addResponseHeaders. We should move this logic to the NetworkObserver and
// expose mimeType in addResponseStart. Bug 1809670.
let mimeType = "";
try {
mimeType = this.#wrappedChannel.contentType;
const contentCharset = this.#requestChannel.contentCharset;
if (contentCharset) {
mimeType += `;charset=${contentCharset}`;
}
} catch (e) {
// Ignore exceptions when reading contentType/contentCharset
}
return mimeType;
}
#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
// timeOrigin for the global which initiated the request. This is not
// available in the parent process, so for now we will use 0.
const timeOrigin = 0;
return {
timeOrigin,
requestTime: this.#convertTimestamp(channelCreationTime, timeOrigin),
redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin),
redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin),
fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin),
dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin),
dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin),
connectStart: this.#convertTimestamp(connectStartTime, timeOrigin),
connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin),
tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
requestStart: this.#convertTimestamp(requestStartTime, timeOrigin),
responseStart: this.#convertTimestamp(responseStartTime, timeOrigin),
responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin),
};
}
/**
* Update the timings and the redirect count from the nsITimedChannel
* corresponding to the current channel. This should be called before emitting
* any event from this class.
*/
#updateDataFromTimedChannel() {
const timedChannel = this.#requestChannel.QueryInterface(
Ci.nsITimedChannel
);
this.#redirectCount = timedChannel.redirectCount;
this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel);
}
}

View File

@ -44,11 +44,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
export class NetworkListener {
#devtoolsNetworkObserver;
#listening;
#navigationManager;
constructor() {
constructor(navigationManager) {
lazy.EventEmitter.decorate(this);
this.#listening = false;
this.#navigationManager = navigationManager;
}
destroy() {
@ -104,6 +106,11 @@ export class NetworkListener {
};
#onNetworkEvent = (networkEvent, channel) => {
return new lazy.NetworkEventRecord(networkEvent, channel, this);
return new lazy.NetworkEventRecord(
networkEvent,
channel,
this,
this.#navigationManager
);
};
}

View File

@ -2,6 +2,9 @@
* 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 { NavigationManager } = ChromeUtils.importESModule(
"chrome://remote/content/shared/NavigationManager.sys.mjs"
);
const { NetworkListener } = ChromeUtils.importESModule(
"chrome://remote/content/shared/listeners/NetworkListener.sys.mjs"
);
@ -10,7 +13,10 @@ const { TabManager } = ChromeUtils.importESModule(
);
add_task(async function test_beforeRequestSent() {
const listener = new NetworkListener();
const navigationManager = new NavigationManager();
navigationManager.startMonitoring();
const listener = new NetworkListener(navigationManager);
const events = [];
const onEvent = (name, data) => events.push(data);
listener.on("before-request-sent", onEvent);
@ -54,10 +60,14 @@ add_task(async function test_beforeRequestSent() {
gBrowser.removeTab(tab2);
listener.off("before-request-sent", onEvent);
listener.destroy();
navigationManager.destroy();
});
add_task(async function test_beforeRequestSent_newTab() {
const listener = new NetworkListener();
const navigationManager = new NavigationManager();
navigationManager.startMonitoring();
const listener = new NetworkListener(navigationManager);
const onBeforeRequestSent = listener.once("before-request-sent");
listener.startListening();
@ -76,10 +86,14 @@ add_task(async function test_beforeRequestSent_newTab() {
"https://example.com/document-builder.sjs?html=tab"
);
gBrowser.removeTab(tab);
navigationManager.destroy();
});
add_task(async function test_fetchError() {
const listener = new NetworkListener();
const navigationManager = new NavigationManager();
navigationManager.startMonitoring();
const listener = new NetworkListener(navigationManager);
const onFetchError = listener.once("fetch-error");
listener.startListening();
@ -90,11 +104,16 @@ add_task(async function test_fetchError() {
const event = await onFetchError;
assertNetworkEvent(event, contextId, "https://not_a_valid_url/");
is(event.errorText, "NS_ERROR_UNKNOWN_HOST");
is(event.request.errorText, "NS_ERROR_UNKNOWN_HOST");
gBrowser.removeTab(tab);
navigationManager.destroy();
});
function assertNetworkEvent(event, expectedContextId, expectedUrl) {
is(event.contextId, expectedContextId, "Event has the expected context id");
is(event.requestData.url, expectedUrl, "Event has the expected url");
is(
event.request.contextId,
expectedContextId,
"Event has the expected context id"
);
is(event.request.serializedURL, expectedUrl, "Event has the expected url");
}

View File

@ -12,8 +12,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
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:
"chrome://remote/content/shared/listeners/NetworkListener.sys.mjs",
parseChallengeHeader:
@ -309,7 +307,9 @@ class NetworkModule extends Module {
// Set of event names which have active subscriptions
this.#subscribedEvents = new Set();
this.#networkListener = new lazy.NetworkListener();
this.#networkListener = new lazy.NetworkListener(
this.messageHandler.navigationManager
);
this.#networkListener.on("auth-required", this.#onAuthRequired);
this.#networkListener.on("before-request-sent", this.#onBeforeRequestSent);
this.#networkListener.on("fetch-error", this.#onFetchError);
@ -549,8 +549,7 @@ class NetworkModule extends Module {
);
}
const wrapper = ChannelWrapper.get(request);
wrapper.resume();
request.wrappedChannel.resume();
resolveBlockedEvent();
}
@ -684,8 +683,7 @@ class NetworkModule extends Module {
await authCallbacks.provideAuthCredentials();
}
} else {
const wrapper = ChannelWrapper.get(request);
wrapper.resume();
request.wrappedChannel.resume();
}
resolveBlockedEvent();
@ -803,9 +801,8 @@ class NetworkModule extends Module {
);
}
const wrapper = ChannelWrapper.get(request);
wrapper.resume();
wrapper.cancel(
request.wrappedChannel.resume();
request.wrappedChannel.cancel(
Cr.NS_ERROR_ABORT,
Ci.nsILoadInfo.BLOCKING_REASON_WEBDRIVER_BIDI
);
@ -933,8 +930,7 @@ class NetworkModule extends Module {
if (phase === InterceptPhase.AuthRequired) {
await authCallbacks.provideAuthCredentials();
} else {
const wrapper = ChannelWrapper.get(request);
wrapper.resume();
request.wrappedChannel.resume();
}
resolveBlockedEvent();
@ -987,11 +983,7 @@ class NetworkModule extends Module {
* The response channel.
*/
#addBlockedRequest(requestId, phase, options = {}) {
const {
authCallbacks,
requestChannel: request,
responseChannel: response,
} = options;
const { authCallbacks, request, response } = options;
const { promise: blockedEventPromise, resolve: resolveBlockedEvent } =
Promise.withResolvers();
@ -1117,14 +1109,14 @@ class NetworkModule extends Module {
}
}
#extractChallenges(responseData) {
#extractChallenges(response) {
let headerName;
// Using case-insensitive match for header names, so we use the lowercase
// version of the "WWW-Authenticate" / "Proxy-Authenticate" strings.
if (responseData.status === 401) {
if (response.status === 401) {
headerName = "www-authenticate";
} else if (responseData.status === 407) {
} else if (response.status === 407) {
headerName = "proxy-authenticate";
} else {
return null;
@ -1132,10 +1124,10 @@ class NetworkModule extends Module {
const challenges = [];
for (const header of responseData.headers) {
if (header.name.toLowerCase() === headerName) {
for (const [name, value] of response.getHeadersList()) {
if (name.toLowerCase() === headerName) {
// A single header can contain several challenges.
const headerChallenges = lazy.parseChallengeHeader(header.value);
const headerChallenges = lazy.parseChallengeHeader(value);
for (const headerChallenge of headerChallenges) {
const realmParam = headerChallenge.params.find(
param => param.name == "realm"
@ -1177,7 +1169,7 @@ class NetworkModule extends Module {
};
}
#getNetworkIntercepts(event, requestData, contextId) {
#getNetworkIntercepts(event, request, topContextId) {
const intercepts = [];
let phase;
@ -1197,17 +1189,11 @@ class NetworkModule extends Module {
return intercepts;
}
// Retrieve the top browsing context id for this network event.
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const topLevelContextId = lazy.TabManager.getIdForBrowsingContext(
browsingContext.top
);
const url = requestData.url;
const url = request.serializedURL;
for (const [interceptId, intercept] of this.#interceptMap) {
if (
intercept.contexts !== null &&
!intercept.contexts.includes(topLevelContextId)
!intercept.contexts.includes(topContextId)
) {
// Skip this intercept if the event's context does not match the list
// of contexts for this intercept.
@ -1228,31 +1214,96 @@ class NetworkModule extends Module {
return intercepts;
}
#getNavigationId(eventName, isNavigationRequest, browsingContext, url) {
if (!isNavigationRequest) {
// Not a navigation request return null.
return null;
#getRequestData(request) {
const requestId = request.requestId;
// "Let url be the result of running the URL serializer with requests URL"
// request.serializedURL is already serialized.
const url = request.serializedURL;
const method = request.method;
const bodySize = request.postDataSize;
const headersSize = request.headersSize;
const headers = [];
const cookies = [];
for (const [name, value] of request.getHeadersList()) {
headers.push(this.#serializeHeader(name, value));
if (name.toLowerCase() == "cookie") {
// TODO: Retrieve the actual cookies from the cookie store.
const headerCookies = value.split(";");
for (const cookie of headerCookies) {
const equal = cookie.indexOf("=");
const cookieName = cookie.substr(0, equal);
const cookieValue = cookie.substr(equal + 1);
const serializedCookie = this.#serializeHeader(
unescape(cookieName.trim()),
unescape(cookieValue.trim())
);
cookies.push(serializedCookie);
}
}
}
let navigation =
this.messageHandler.navigationManager.getNavigationForBrowsingContext(
browsingContext
);
const timings = request.getFetchTimings();
// `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 (
eventName === "network.beforeRequestSent" &&
(!navigation || navigation.finished)
) {
navigation = lazy.notifyNavigationStarted({
contextDetails: { context: browsingContext },
url,
});
return {
request: requestId,
url,
method,
bodySize,
headersSize,
headers,
cookies,
timings,
};
}
#getResponseContentInfo(response) {
return {
size: response.decodedBodySize,
};
}
#getResponseData(response) {
const url = response.serializedURL;
const protocol = response.protocol;
const status = response.status;
const statusText = response.statusMessage;
// TODO: Ideally we should have a `isCacheStateLocal` getter
// const fromCache = response.isCacheStateLocal();
const fromCache = response.fromCache;
const mimeType = response.getComputedMimeType();
const headers = [];
for (const [name, value] of response.getHeadersList()) {
headers.push(this.#serializeHeader(name, value));
}
return navigation ? navigation.navigationId : null;
const bytesReceived = response.totalTransmittedSize;
const headersSize = response.headersTransmittedSize;
const bodySize = response.encodedBodySize;
const content = this.#getResponseContentInfo(response);
const authChallenges = this.#extractChallenges(response);
const params = {
url,
protocol,
status,
statusText,
fromCache,
headers,
mimeType,
bytesReceived,
headersSize,
bodySize,
content,
};
if (authChallenges !== null) {
params.authChallenges = authChallenges;
}
return params;
}
#getSuspendMarkerText(requestData, phase) {
@ -1260,21 +1311,13 @@ class NetworkModule extends Module {
}
#onAuthRequired = (name, data) => {
const {
authCallbacks,
contextId,
isNavigationRequest,
redirectCount,
requestChannel,
requestData,
responseChannel,
responseData,
timestamp,
} = data;
const { authCallbacks, request, response } = data;
let isBlocked = false;
try {
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
@ -1283,18 +1326,9 @@ class NetworkModule extends Module {
const protocolEventName = "network.authRequired";
// Process the navigation to create potentially missing navigation ids
// before the early return below.
const navigation = this.#getNavigationId(
protocolEventName,
isNavigationRequest,
browsingContext,
requestData.url
);
const isListening = this.messageHandler.eventsDispatcher.hasListener(
protocolEventName,
{ contextId }
{ contextId: request.contextId }
);
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
@ -1302,23 +1336,16 @@ class NetworkModule extends Module {
return;
}
const baseParameters = this.#processNetworkEvent(protocolEventName, {
contextId,
navigation,
redirectCount,
requestData,
timestamp,
});
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
const authRequiredEvent = this.#serializeNetworkEvent({
const responseData = this.#getResponseData(response);
const authRequiredEvent = {
...baseParameters,
response: responseData,
});
const authChallenges = this.#extractChallenges(responseData);
// authChallenges should never be null for a request which triggered an
// authRequired event.
authRequiredEvent.response.authChallenges = authChallenges;
};
this.emitEvent(
protocolEventName,
@ -1337,8 +1364,8 @@ class NetworkModule extends Module {
InterceptPhase.AuthRequired,
{
authCallbacks,
requestChannel,
responseChannel,
request,
response,
}
);
}
@ -1352,16 +1379,11 @@ class NetworkModule extends Module {
};
#onBeforeRequestSent = (name, data) => {
const {
contextId,
isNavigationRequest,
redirectCount,
requestChannel,
requestData,
timestamp,
} = data;
const { request } = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
@ -1371,15 +1393,6 @@ class NetworkModule extends Module {
const internalEventName = "network._beforeRequestSent";
const protocolEventName = "network.beforeRequestSent";
// Process the navigation to create potentially missing navigation ids
// before the early return below.
const navigation = this.#getNavigationId(
protocolEventName,
isNavigationRequest,
browsingContext,
requestData.url
);
// Always emit internal events, they are used to support the browsingContext
// navigate command.
// Bug 1861922: Replace internal events with a Network listener helper
@ -1387,15 +1400,15 @@ class NetworkModule extends Module {
this.emitEvent(
internalEventName,
{
navigation,
url: requestData.url,
navigation: request.navigationId,
url: request.serializedURL,
},
this.#getContextInfo(browsingContext)
);
const isListening = this.messageHandler.eventsDispatcher.hasListener(
protocolEventName,
{ contextId }
{ contextId: request.contextId }
);
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
@ -1403,23 +1416,20 @@ class NetworkModule extends Module {
return;
}
const baseParameters = this.#processNetworkEvent(protocolEventName, {
contextId,
navigation,
redirectCount,
requestData,
timestamp,
});
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
// Bug 1805479: Handle the initiator, including stacktrace details.
const initiator = {
type: InitiatorType.Other,
};
const beforeRequestSentEvent = this.#serializeNetworkEvent({
const beforeRequestSentEvent = {
...baseParameters,
initiator,
});
};
this.emitEvent(
protocolEventName,
@ -1430,32 +1440,26 @@ class NetworkModule extends Module {
if (beforeRequestSentEvent.isBlocked) {
// TODO: Requests suspended in beforeRequestSent still reach the server at
// the moment. https://bugzilla.mozilla.org/show_bug.cgi?id=1849686
const wrapper = ChannelWrapper.get(requestChannel);
wrapper.suspend(
this.#getSuspendMarkerText(requestData, "beforeRequestSent")
request.wrappedChannel.suspend(
this.#getSuspendMarkerText(request, "beforeRequestSent")
);
this.#addBlockedRequest(
beforeRequestSentEvent.request.request,
InterceptPhase.BeforeRequestSent,
{
requestChannel,
request,
}
);
}
};
#onFetchError = (name, data) => {
const {
contextId,
errorText,
isNavigationRequest,
redirectCount,
requestData,
timestamp,
} = data;
const { request } = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
@ -1465,15 +1469,6 @@ class NetworkModule extends Module {
const internalEventName = "network._fetchError";
const protocolEventName = "network.fetchError";
// Process the navigation to create potentially missing navigation ids
// before the early return below.
const navigation = this.#getNavigationId(
protocolEventName,
isNavigationRequest,
browsingContext,
requestData.url
);
// Always emit internal events, they are used to support the browsingContext
// navigate command.
// Bug 1861922: Replace internal events with a Network listener helper
@ -1481,15 +1476,15 @@ class NetworkModule extends Module {
this.emitEvent(
internalEventName,
{
navigation,
url: requestData.url,
navigation: request.navigationId,
url: request.serializedURL,
},
this.#getContextInfo(browsingContext)
);
const isListening = this.messageHandler.eventsDispatcher.hasListener(
protocolEventName,
{ contextId }
{ contextId: request.contextId }
);
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
@ -1497,18 +1492,15 @@ class NetworkModule extends Module {
return;
}
const baseParameters = this.#processNetworkEvent(protocolEventName, {
contextId,
navigation,
redirectCount,
requestData,
timestamp,
});
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
const fetchErrorEvent = this.#serializeNetworkEvent({
const fetchErrorEvent = {
...baseParameters,
errorText,
});
errorText: request.errorText,
};
this.emitEvent(
protocolEventName,
@ -1518,18 +1510,11 @@ class NetworkModule extends Module {
};
#onResponseEvent = (name, data) => {
const {
contextId,
isNavigationRequest,
redirectCount,
requestChannel,
requestData,
responseChannel,
responseData,
timestamp,
} = data;
const { request, response } = data;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
const browsingContext = lazy.TabManager.getBrowsingContextById(
request.contextId
);
if (!browsingContext) {
// Do not emit events if the context id does not match any existing
// browsing context.
@ -1546,15 +1531,6 @@ class NetworkModule extends Module {
? "network._responseStarted"
: "network._responseCompleted";
// Process the navigation to create potentially missing navigation ids
// before the early return below.
const navigation = this.#getNavigationId(
protocolEventName,
isNavigationRequest,
browsingContext,
requestData.url
);
// Always emit internal events, they are used to support the browsingContext
// navigate command.
// Bug 1861922: Replace internal events with a Network listener helper
@ -1562,15 +1538,15 @@ class NetworkModule extends Module {
this.emitEvent(
internalEventName,
{
navigation,
url: requestData.url,
navigation: request.navigationId,
url: request.serializedURL,
},
this.#getContextInfo(browsingContext)
);
const isListening = this.messageHandler.eventsDispatcher.hasListener(
protocolEventName,
{ contextId }
{ contextId: request.contextId }
);
if (!isListening) {
// If there are no listeners subscribed to this event and this context,
@ -1578,23 +1554,17 @@ class NetworkModule extends Module {
return;
}
const baseParameters = this.#processNetworkEvent(protocolEventName, {
contextId,
navigation,
redirectCount,
requestData,
timestamp,
});
const baseParameters = this.#processNetworkEvent(
protocolEventName,
request
);
const responseEvent = this.#serializeNetworkEvent({
const responseData = this.#getResponseData(response);
const responseEvent = {
...baseParameters,
response: responseData,
});
const authChallenges = this.#extractChallenges(responseData);
if (authChallenges !== null) {
responseEvent.response.authChallenges = authChallenges;
}
};
this.emitEvent(
protocolEventName,
@ -1606,51 +1576,40 @@ class NetworkModule extends Module {
protocolEventName === "network.responseStarted" &&
responseEvent.isBlocked
) {
const wrapper = ChannelWrapper.get(requestChannel);
wrapper.suspend(
this.#getSuspendMarkerText(requestData, "responseStarted")
request.wrappedChannel.suspend(
this.#getSuspendMarkerText(request, "responseStarted")
);
this.#addBlockedRequest(
responseEvent.request.request,
InterceptPhase.ResponseStarted,
{
requestChannel,
responseChannel,
request,
response,
}
);
}
};
/**
* 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 {string|null} data.navigation
* The navigation id if this is a network event for 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, navigation, redirectCount, requestData, timestamp } =
data;
const intercepts = this.#getNetworkIntercepts(
eventName,
requestData,
contextId
);
const isBlocked = !!intercepts.length;
#processNetworkEvent(event, request) {
const requestData = this.#getRequestData(request);
const navigation = request.navigationId;
let contextId = null;
let topContextId = null;
if (request.contextId) {
// Retrieve the top browsing context id for this network event.
contextId = request.contextId;
const browsingContext = lazy.TabManager.getBrowsingContextById(contextId);
topContextId = lazy.TabManager.getIdForBrowsingContext(
browsingContext.top
);
}
const baseParameters = {
const intercepts = this.#getNetworkIntercepts(event, request, topContextId);
const redirectCount = request.redirectCount;
const timestamp = Date.now();
const isBlocked = !!intercepts.length;
const params = {
context: contextId,
isBlocked,
navigation,
@ -1660,51 +1619,17 @@ class NetworkModule extends Module {
};
if (isBlocked) {
baseParameters.intercepts = intercepts;
params.intercepts = intercepts;
}
return baseParameters;
return params;
}
#serializeHeadersOrCookies(headersOrCookies) {
return headersOrCookies.map(item => ({
name: item.name,
value: this.#serializeStringAsBytesValue(item.value),
}));
}
/**
* Serialize in-place all cookies and headers arrays found in a given network
* event payload.
*
* @param {object} networkEvent
* The network event parameters object to serialize.
* @returns {object}
* The serialized network event parameters.
*/
#serializeNetworkEvent(networkEvent) {
// Make a shallow copy of networkEvent before serializing the headers and
// cookies arrays in request/response.
const serialized = { ...networkEvent };
// Make a shallow copy of the request data.
serialized.request = { ...networkEvent.request };
serialized.request.cookies = this.#serializeHeadersOrCookies(
networkEvent.request.cookies
);
serialized.request.headers = this.#serializeHeadersOrCookies(
networkEvent.request.headers
);
if (networkEvent.response?.headers) {
// Make a shallow copy of the response data.
serialized.response = { ...networkEvent.response };
serialized.response.headers = this.#serializeHeadersOrCookies(
networkEvent.response.headers
);
}
return serialized;
#serializeHeader(name, value) {
return {
name,
value: this.#serializeStringAsBytesValue(value),
};
}
/**