mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 16:22:00 +00:00
e50991db5e
Differential Revision: https://phabricator.services.mozilla.com/D223377
469 lines
14 KiB
JavaScript
469 lines
14 KiB
JavaScript
/* 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, {
|
|
NetworkHelper:
|
|
"resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
|
|
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 {
|
|
#alreadyCompleted;
|
|
#channel;
|
|
#contextId;
|
|
#eventRecord;
|
|
#isDataURL;
|
|
#navigationId;
|
|
#navigationManager;
|
|
#rawHeaders;
|
|
#redirectCount;
|
|
#requestId;
|
|
#timedChannel;
|
|
#wrappedChannel;
|
|
|
|
/**
|
|
*
|
|
* @param {nsIChannel} channel
|
|
* The channel for the request.
|
|
* @param {object} params
|
|
* @param {NetworkEventRecord} params.networkEventRecord
|
|
* The NetworkEventRecord owning this NetworkRequest.
|
|
* @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 { eventRecord, navigationManager, rawHeaders = "" } = params;
|
|
|
|
this.#channel = channel;
|
|
this.#eventRecord = eventRecord;
|
|
this.#isDataURL = this.#channel instanceof Ci.nsIDataChannel;
|
|
this.#navigationManager = navigationManager;
|
|
this.#rawHeaders = rawHeaders;
|
|
|
|
// Platform timestamp is in microseconds.
|
|
const currentTimeStamp = Date.now() * 1000;
|
|
this.#timedChannel =
|
|
this.#channel instanceof Ci.nsITimedChannel
|
|
? this.#channel.QueryInterface(Ci.nsITimedChannel)
|
|
: {
|
|
redirectCount: 0,
|
|
asyncOpenTime: currentTimeStamp,
|
|
redirectStartTime: 0,
|
|
redirectEndTime: 0,
|
|
domainLookupStartTime: currentTimeStamp,
|
|
domainLookupEndTime: currentTimeStamp,
|
|
connectStartTime: currentTimeStamp,
|
|
connectEndTime: currentTimeStamp,
|
|
secureConnectionStartTime: currentTimeStamp,
|
|
requestStartTime: currentTimeStamp,
|
|
responseStartTime: currentTimeStamp,
|
|
responseEndTime: currentTimeStamp,
|
|
};
|
|
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 alreadyCompleted() {
|
|
return this.#alreadyCompleted;
|
|
}
|
|
|
|
get channel() {
|
|
return this.#channel;
|
|
}
|
|
|
|
get contextId() {
|
|
return this.#contextId;
|
|
}
|
|
|
|
get errorText() {
|
|
// TODO: Update with a proper error text. Bug 1873037.
|
|
return ChromeUtils.getXPCOMErrorName(this.#channel.status);
|
|
}
|
|
|
|
get headers() {
|
|
return this.#getHeadersList();
|
|
}
|
|
|
|
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 isHttpChannel() {
|
|
return this.#channel instanceof Ci.nsIHttpChannel;
|
|
}
|
|
|
|
get method() {
|
|
return this.#isDataURL ? "GET" : this.#channel.requestMethod;
|
|
}
|
|
|
|
get navigationId() {
|
|
return this.#navigationId;
|
|
}
|
|
|
|
get postDataSize() {
|
|
const charset = lazy.NetworkUtils.getCharset(this.#channel);
|
|
const sentBody = lazy.NetworkHelper.readPostTextFromRequest(
|
|
this.#channel,
|
|
charset
|
|
);
|
|
return sentBody ? sentBody.length : 0;
|
|
}
|
|
|
|
get redirectCount() {
|
|
return this.#redirectCount;
|
|
}
|
|
|
|
get requestId() {
|
|
return this.#requestId;
|
|
}
|
|
|
|
get serializedURL() {
|
|
return this.#channel.URI.spec;
|
|
}
|
|
|
|
get supportsInterception() {
|
|
// The request which doesn't have `wrappedChannel` can not be intercepted.
|
|
return !!this.#wrappedChannel;
|
|
}
|
|
|
|
get timings() {
|
|
return this.#getFetchTimings();
|
|
}
|
|
|
|
get wrappedChannel() {
|
|
return this.#wrappedChannel;
|
|
}
|
|
|
|
set alreadyCompleted(value) {
|
|
this.#alreadyCompleted = value;
|
|
}
|
|
|
|
/**
|
|
* Add information about raw headers, collected from NetworkObserver events.
|
|
*
|
|
* @param {string} rawHeaders
|
|
* The raw headers.
|
|
*/
|
|
addRawHeaders(rawHeaders) {
|
|
this.#rawHeaders = rawHeaders || "";
|
|
}
|
|
|
|
/**
|
|
* Clear a request header from the request's headers list.
|
|
*
|
|
* @param {string} name
|
|
* The header's name.
|
|
*/
|
|
clearRequestHeader(name) {
|
|
this.#channel.setRequestHeader(
|
|
name, // aName
|
|
"", // aValue="" as an empty value
|
|
false // aMerge=false to force clearing the header
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Redirect the request to another provided URL.
|
|
*
|
|
* @param {string} url
|
|
* The URL to redirect to.
|
|
*/
|
|
redirectTo(url) {
|
|
this.#channel.transparentRedirectTo(Services.io.newURI(url));
|
|
}
|
|
|
|
/**
|
|
* Set the request post body
|
|
*
|
|
* @param {string} body
|
|
* The body to set.
|
|
*/
|
|
setRequestBody(body) {
|
|
// Update the requestObserversCalled flag to allow modifying the request,
|
|
// and reset once done.
|
|
this.#channel.requestObserversCalled = false;
|
|
|
|
try {
|
|
this.#channel.QueryInterface(Ci.nsIUploadChannel2);
|
|
const bodyStream = Cc[
|
|
"@mozilla.org/io/string-input-stream;1"
|
|
].createInstance(Ci.nsIStringInputStream);
|
|
bodyStream.setData(body, body.length);
|
|
this.#channel.explicitSetUploadStream(
|
|
bodyStream,
|
|
null,
|
|
-1,
|
|
this.#channel.requestMethod,
|
|
false
|
|
);
|
|
} finally {
|
|
// Make sure to reset the flag once the modification was attempted.
|
|
this.#channel.requestObserversCalled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a request header
|
|
*
|
|
* @param {string} name
|
|
* The header's name.
|
|
* @param {string} value
|
|
* The header's value.
|
|
* @param {object} options
|
|
* @param {boolean} options.merge
|
|
* True if the value should be merged with the existing value, false if it
|
|
* should override it. Defaults to false.
|
|
*/
|
|
setRequestHeader(name, value, options) {
|
|
const { merge = false } = options;
|
|
this.#channel.setRequestHeader(name, value, merge);
|
|
}
|
|
|
|
/**
|
|
* Update the request's method.
|
|
*
|
|
* @param {string} method
|
|
* The method to set.
|
|
*/
|
|
setRequestMethod(method) {
|
|
// Update the requestObserversCalled flag to allow modifying the request,
|
|
// and reset once done.
|
|
this.#channel.requestObserversCalled = false;
|
|
|
|
try {
|
|
this.#channel.requestMethod = method;
|
|
} finally {
|
|
// Make sure to reset the flag once the modification was attempted.
|
|
this.#channel.requestObserversCalled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allows to bypass the actual network request and immediately respond with
|
|
* the provided nsIReplacedHttpResponse.
|
|
*
|
|
* @param {nsIReplacedHttpResponse} replacedHttpResponse
|
|
* The replaced response to use.
|
|
*/
|
|
setResponseOverride(replacedHttpResponse) {
|
|
this.wrappedChannel.channel
|
|
.QueryInterface(Ci.nsIHttpChannelInternal)
|
|
.setResponseOverride(replacedHttpResponse);
|
|
|
|
const rawHeaders = [];
|
|
replacedHttpResponse.visitResponseHeaders({
|
|
visitHeader(name, value) {
|
|
rawHeaders.push(`${name}: ${value}`);
|
|
},
|
|
});
|
|
|
|
// Setting an override bypasses the usual codepath for network responses.
|
|
// There will be no notification about receiving a response.
|
|
// However, there will be a notification about the end of the response.
|
|
// Therefore, simulate a addResponseStart here to make sure we handle
|
|
// addResponseContent properly.
|
|
this.#eventRecord.prepareResponseStart({
|
|
channel: this.#channel,
|
|
fromCache: false,
|
|
rawHeaders: rawHeaders.join("\n"),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return a static version of the class instance.
|
|
* This method is used to prepare the data to be sent with the events for cached resources
|
|
* generated from the content process but need to be sent to the parent.
|
|
*/
|
|
toJSON() {
|
|
return {
|
|
headers: this.headers,
|
|
headersSize: this.headersSize,
|
|
method: this.method,
|
|
navigationId: this.navigationId,
|
|
postDataSize: this.postDataSize,
|
|
redirectCount: this.redirectCount,
|
|
requestId: this.requestId,
|
|
serializedURL: this.serializedURL,
|
|
// Since this data is meant to be sent to the parent process
|
|
// it will not be possible to intercept such request.
|
|
supportsInterception: false,
|
|
timings: this.timings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* only expects high resolution timestamps (double in milliseconds) as inputs
|
|
* of this method, but since platform timestamps are integers in microseconds,
|
|
* they will be converted on the fly in this helper.
|
|
*
|
|
* @param {number} timing
|
|
* Platform TimeStamp for a request timing relative from the time origin
|
|
* in microseconds.
|
|
* @param {number} requestTime
|
|
* Platform TimeStamp for the request start time relative from the time
|
|
* origin, in microseconds.
|
|
*
|
|
* @returns {number}
|
|
* High resolution timestamp (https://www.w3.org/TR/hr-time-3/#dom-domhighrestimestamp)
|
|
* 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;
|
|
}
|
|
|
|
// Convert from platform timestamp to high resolution timestamp.
|
|
return (timing - requestTime) / 1000;
|
|
}
|
|
|
|
#getContextId() {
|
|
const id = lazy.NetworkUtils.getChannelBrowsingContextID(this.#channel);
|
|
const browsingContext = BrowsingContext.get(id);
|
|
return lazy.TabManager.getIdForBrowsingContext(browsingContext);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the Fetch timings for the NetworkRequest.
|
|
*
|
|
* @returns {object}
|
|
* Object with keys corresponding to fetch timing names, and their
|
|
* corresponding values.
|
|
*/
|
|
#getFetchTimings() {
|
|
const {
|
|
asyncOpenTime,
|
|
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(asyncOpenTime, 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 = [];
|
|
|
|
if (this.#channel instanceof Ci.nsIHttpChannel) {
|
|
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]);
|
|
},
|
|
});
|
|
}
|
|
|
|
if (this.#channel instanceof Ci.nsIDataChannel) {
|
|
// Data channels have no request headers.
|
|
return [];
|
|
}
|
|
|
|
if (this.#channel instanceof Ci.nsIFileChannel) {
|
|
// File channels have no request headers.
|
|
return [];
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
#getNavigationId() {
|
|
if (!this.#channel.isDocument) {
|
|
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.state !== "started") {
|
|
navigation = lazy.notifyNavigationStarted({
|
|
contextDetails: { context: browsingContext },
|
|
url: this.serializedURL,
|
|
});
|
|
}
|
|
|
|
return navigation ? navigation.navigationId : null;
|
|
}
|
|
}
|