chore(firefox-beta): backport #8107 to ff beta (#8124)

This commit is contained in:
Yury Semikhatsky 2021-08-10 15:23:54 -07:00 committed by GitHub
parent 128bf7b4d0
commit eb7004781f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 220 additions and 122 deletions

View File

@ -1,2 +1,2 @@
1274 1275
Changed: lushnikov@chromium.org Fri 06 Aug 2021 07:58:41 AM PDT Changed: yurys@chromium.org Tue 10 Aug 2021 03:19:51 PM PDT

View File

@ -38,7 +38,7 @@ const SINK_CATEGORY_NAME = "net-channel-event-sinks";
const pageNetworkSymbol = Symbol('PageNetwork'); const pageNetworkSymbol = Symbol('PageNetwork');
class PageNetwork { class PageNetwork {
static _forPageTarget(target) { static forPageTarget(target) {
let result = target[pageNetworkSymbol]; let result = target[pageNetworkSymbol];
if (!result) { if (!result) {
result = new PageNetwork(target); result = new PageNetwork(target);
@ -78,7 +78,7 @@ class PageNetwork {
const intercepted = this._interceptedRequests.get(requestId); const intercepted = this._interceptedRequests.get(requestId);
if (!intercepted) if (!intercepted)
throw new Error(`Cannot find request "${requestId}"`); throw new Error(`Cannot find request "${requestId}"`);
return { response: await intercepted.interceptResponse(url, method, headers, postData) }; return { response: await new ResponseInterceptor(intercepted).interceptResponse(url, method, headers, postData) };
} }
this._takeIntercepted(requestId).resume(url, method, headers, postData); this._takeIntercepted(requestId).resume(url, method, headers, postData);
return {}; return {};
@ -107,6 +107,141 @@ class PageNetwork {
} }
} }
class ResponseInterceptor {
constructor(request) {
this._originalRequest = request;
this._finalRequest = request;
this._responseChannel = null;
this._interceptedResponse = null;
if (request._responseInterceptor)
throw new Error('Already intercepting response for this request');
request._responseInterceptor = this;
}
async interceptResponse(url, method, headers, postData) {
const httpChannel = this._originalRequest.httpChannel;
const uri = url ? Services.io.newURI(url) : httpChannel.URI;
const newChannel = NetUtil.newChannel({
uri,
loadingNode: httpChannel.loadInfo.loadingContext,
loadingPrincipal: httpChannel.loadInfo.loadingPrincipal || httpChannel.loadInfo.principalToInherit,
triggeringPrincipal: httpChannel.loadInfo.triggeringPrincipal,
securityFlags: httpChannel.loadInfo.securityFlags,
contentPolicyType: httpChannel.loadInfo.internalContentPolicyType,
}).QueryInterface(Ci.nsIRequest).QueryInterface(Ci.nsIHttpChannel);
newChannel.loadInfo = httpChannel.loadInfo;
newChannel.loadGroup = httpChannel.loadGroup;
for (const header of (headers || requestHeaders(httpChannel)))
newChannel.setRequestHeader(header.name, header.value, false /* merge */);
if (postData) {
setPostData(newChannel, postData, headers);
} else if (httpChannel instanceof Ci.nsIUploadChannel) {
newChannel.QueryInterface(Ci.nsIUploadChannel);
newChannel.setUploadStream(httpChannel.uploadStream, '', -1);
}
// We must set this after setting the upload stream, otherwise it
// will always be 'PUT'. (from another place in the source base)
newChannel.requestMethod = method || httpChannel.requestMethod;
this._responseChannel = newChannel;
const networkObserver = this._originalRequest._networkObserver;
networkObserver._responseInterceptionChannels.add(newChannel);
// We add {newChannerl -> original request} mapping so that if the alternative
// channel gets redirected we report the redirect on the original(paused)
// request.
networkObserver._channelToRequest.set(newChannel, this._originalRequest);
const body = await new Promise((resolve, reject) => {
NetUtil.asyncFetch(newChannel, (stream, status) => {
networkObserver._responseInterceptionChannels.delete(newChannel);
networkObserver._channelToRequest.delete(newChannel);
if (!Components.isSuccessCode(status)) {
reject(status);
return;
}
try {
resolve(NetUtil.readInputStreamToString(stream, stream.available()));
} catch (e) {
if (e.result == Cr.NS_BASE_STREAM_CLOSED) {
// The stream was empty.
resolve('');
} else {
reject(e);
}
} finally {
stream.close();
}
});
});
const finalRequest = this._finalRequest;
const responseChannel = finalRequest === this._originalRequest ? this._responseChannel : finalRequest.httpChannel;
const pageNetwork = this._originalRequest._pageNetwork;
if (pageNetwork)
pageNetwork._responseStorage.addResponseBody(finalRequest, responseChannel, body);
const response = responseHead(responseChannel);
this._interceptedResponse = Object.assign({ body }, response);
return response;
}
isInterceptedRequest(request) {
return this._originalRequest === request;
}
interceptOnResponse(request, fromCache, opt_statusCode, opt_statusText) {
const isFullfillingResponse = this._interceptedResponse;
if (isFullfillingResponse) {
if (request !== this._originalRequest)
dump(`ERROR: unexpected request ${request.requestId}, expected ${this._originalRequest.requestId}\n`);
this._finalRequest._sendOnResponseImpl(this._originalRequest.httpChannel, fromCache, opt_statusCode, opt_statusText);
return;
}
// When fetching original response first redirect is reported using original request id
// but with data received via the alternative response channel.
const httpChannel = (this._originalRequest === request) ? this._responseChannel : request.httpChannel;
if (this._isRedirectResponse(httpChannel))
request._sendOnResponseImpl(httpChannel, fromCache, opt_statusCode, opt_statusText);
// Drop non-redirect response on the floor, it will be fulfilled later.
}
interceptOnRequestFinished(request) {
const isFullfillingResponse = this._interceptedResponse;
if (isFullfillingResponse) {
if (request !== this._originalRequest)
dump(`ERROR: unexpected request ${request.requestId}, expected ${this._originalRequest.requestId}\n`);
this._finalRequest._sendOnRequestFinishedImpl(this._originalRequest.httpChannel);
return;
}
// When fetching original response first redirect is reported using original request id
// but with data received via the alternative response channel.
const httpChannel = (this._originalRequest === request) ? this._responseChannel : request.httpChannel;
if (this._isRedirectResponse(httpChannel))
request._sendOnRequestFinishedImpl(httpChannel);
// Drop non-redirect response on the floor, it will be fulfilled later.
}
interceptOnRequestFailed(error) {
this._finalRequest._sendOnRequestFailedImpl(error);
}
interceptAddResponseBody(request, body) {
const isFullfillingResponse = this._interceptedResponse;
let key = request;
if (isFullfillingResponse) {
if (request !== this._originalRequest)
dump(`ERROR: unexpected request ${request.requestId}, expected ${this._originalRequest.requestId}\n`);
key = this._finalRequest;
}
request._pageNetwork._responseStorage.addResponseBody(key, request.httpChannel, body);
}
_isRedirectResponse(httpChannel) {
const status = httpChannel.responseStatus;
return (300 <= status && status < 400);
}
}
class NetworkRequest { class NetworkRequest {
constructor(networkObserver, httpChannel, redirectedFrom) { constructor(networkObserver, httpChannel, redirectedFrom) {
this._networkObserver = networkObserver; this._networkObserver = networkObserver;
@ -139,14 +274,18 @@ class NetworkRequest {
// Finish previous request now. Since we inherit the listener, we could in theory // Finish previous request now. Since we inherit the listener, we could in theory
// use onStopRequest, but that will only happen after the last redirect has finished. // use onStopRequest, but that will only happen after the last redirect has finished.
redirectedFrom._sendOnRequestFinished(); redirectedFrom._sendOnRequestFinished();
if (redirectedFrom._responseInterceptor) {
this._responseInterceptor = redirectedFrom._responseInterceptor;
this._responseInterceptor._finalRequest = this;
}
} }
this._maybeInactivePageNetwork = this._findPageNetwork(); this._pageNetwork = redirectedFrom ? redirectedFrom._pageNetwork : networkObserver._findPageNetwork(httpChannel);
this._expectingInterception = false; this._expectingInterception = false;
this._expectingResumedRequest = undefined; // { method, headers, postData } this._expectingResumedRequest = undefined; // { method, headers, postData }
this._sentOnResponse = false; this._sentOnResponse = false;
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (pageNetwork) { if (pageNetwork) {
appendExtraHTTPHeaders(httpChannel, pageNetwork._target.browserContext().extraHTTPHeaders); appendExtraHTTPHeaders(httpChannel, pageNetwork._target.browserContext().extraHTTPHeaders);
appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders); appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders);
@ -156,7 +295,10 @@ class NetworkRequest {
httpChannel.QueryInterface(Ci.nsITraceableChannel); httpChannel.QueryInterface(Ci.nsITraceableChannel);
this._originalListener = httpChannel.setNewListener(this); this._originalListener = httpChannel.setNewListener(this);
if (redirectedFrom) { // When fetching original response ResponseInterceptor creates a new HttpChannel
// with custom listener which is different from the original request's listener.
// In that case we should not inherit the listener from the original request here.
if (redirectedFrom && !redirectedFrom._responseInterceptor?.isInterceptedRequest(redirectedFrom)) {
// Listener is inherited for regular redirects, so we'd like to avoid // Listener is inherited for regular redirects, so we'd like to avoid
// calling into previous NetworkRequest. // calling into previous NetworkRequest.
this._originalListener = redirectedFrom._originalListener; this._originalListener = redirectedFrom._originalListener;
@ -187,64 +329,6 @@ class NetworkRequest {
this._interceptedChannel = undefined; this._interceptedChannel = undefined;
} }
async interceptResponse(url, method, headers, postData) {
const uri = url ? Services.io.newURI(url) : this.httpChannel.URI;
const newChannel = NetUtil.newChannel({
uri,
loadingNode: this.httpChannel.loadInfo.loadingContext,
loadingPrincipal: this.httpChannel.loadInfo.loadingPrincipal || this._interceptedChannel.loadInfo.principalToInherit,
triggeringPrincipal: this.httpChannel.loadInfo.triggeringPrincipal,
securityFlags: this.httpChannel.loadInfo.securityFlags,
contentPolicyType: this.httpChannel.loadInfo.internalContentPolicyType,
}).QueryInterface(Ci.nsIRequest).QueryInterface(Ci.nsIHttpChannel);
newChannel.loadInfo = this.httpChannel.loadInfo;
newChannel.loadGroup = this.httpChannel.loadGroup;
for (const header of (headers || requestHeaders(this.httpChannel)))
newChannel.setRequestHeader(header.name, header.value, false /* merge */);
if (postData) {
setPostData(newChannel, postData, headers);
} else if (this.httpChannel instanceof Ci.nsIUploadChannel) {
newChannel.QueryInterface(Ci.nsIUploadChannel);
newChannel.setUploadStream(this.httpChannel.uploadStream, '', -1);
}
// We must set this after setting the upload stream, otherwise it
// will always be 'PUT'. (from another place in the source base)
newChannel.requestMethod = method || this.httpChannel.requestMethod;
this._networkObserver._responseInterceptionChannels.add(newChannel);
const body = await new Promise((resolve, reject) => {
NetUtil.asyncFetch(newChannel, (stream, status) => {
this._networkObserver._responseInterceptionChannels.delete(newChannel);
if (!Components.isSuccessCode(status)) {
reject(status);
return;
}
try {
resolve(NetUtil.readInputStreamToString(stream, stream.available()));
} catch (e) {
if (e.result == Cr.NS_BASE_STREAM_CLOSED) {
// The stream was empty.
resolve('');
} else {
reject(e);
}
} finally {
stream.close();
}
});
});
const pageNetwork = this._activePageNetwork();
if (pageNetwork)
pageNetwork._responseStorage.addResponseBody(this, newChannel, body);
const response = responseHead(newChannel);
this._interceptedResponse = Object.assign({ body }, response);
return response;
}
// Public interception API. // Public interception API.
abort(errorCode) { abort(errorCode) {
const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE; const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
@ -255,11 +339,12 @@ class NetworkRequest {
// Public interception API. // Public interception API.
fulfill(status, statusText, headers, base64body) { fulfill(status, statusText, headers, base64body) {
let body = base64body ? atob(base64body) : ''; let body = base64body ? atob(base64body) : '';
if (this._interceptedResponse) { const originalResponse = this._responseInterceptor?._interceptedResponse;
status = status || this._interceptedResponse.status; if (originalResponse) {
statusText = statusText || this._interceptedResponse.statusText; status = status || originalResponse.status;
headers = headers || this._interceptedResponse.headers; statusText = statusText || originalResponse.statusText;
body = body || this._interceptedResponse.body; headers = headers || originalResponse.headers;
body = body || originalResponse.body;
} }
this._interceptedChannel.synthesizeStatus(status, statusText); this._interceptedChannel.synthesizeStatus(status, statusText);
for (const header of headers) { for (const header of headers) {
@ -308,11 +393,6 @@ class NetworkRequest {
setPostData(this.httpChannel, postData, headers); setPostData(this.httpChannel, postData, headers);
} }
// Instrumentation called by NetworkObserver.
_onResponse(fromCache) {
this._sendOnResponse(fromCache);
}
// nsIInterfaceRequestor // nsIInterfaceRequestor
getInterface(iid) { getInterface(iid) {
if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController)) if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController))
@ -354,7 +434,7 @@ class NetworkRequest {
promptAuth(aChannel, level, authInfo) { promptAuth(aChannel, level, authInfo) {
if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
return false; return false;
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (!pageNetwork) if (!pageNetwork)
return false; return false;
let credentials = null; let credentials = null;
@ -421,7 +501,7 @@ class NetworkRequest {
this._expectingInterception = false; this._expectingInterception = false;
this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel); this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (!pageNetwork) { if (!pageNetwork) {
// Just in case we disabled instrumentation while intercepting, resume and forget. // Just in case we disabled instrumentation while intercepting, resume and forget.
this.resume(); this.resume();
@ -489,9 +569,13 @@ class NetworkRequest {
// we do not get onResponse normally, but we do get nsIRequestObserver notifications. // we do not get onResponse normally, but we do get nsIRequestObserver notifications.
this._sendOnResponse(false); this._sendOnResponse(false);
const body = this._responseBodyChunks.join(''); const body = this._responseBodyChunks.join('');
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (pageNetwork) if (pageNetwork) {
pageNetwork._responseStorage.addResponseBody(this, this.httpChannel, body); if (this._responseInterceptor)
this._responseInterceptor.interceptAddResponseBody(this, body);
else
pageNetwork._responseStorage.addResponseBody(this, this.httpChannel, body);
}
this._sendOnRequestFinished(); this._sendOnRequestFinished();
} else { } else {
this._sendOnRequestFailed(aStatusCode); this._sendOnRequestFailed(aStatusCode);
@ -501,7 +585,7 @@ class NetworkRequest {
} }
_shouldIntercept() { _shouldIntercept() {
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (!pageNetwork) if (!pageNetwork)
return false; return false;
if (pageNetwork._requestInterceptionEnabled) if (pageNetwork._requestInterceptionEnabled)
@ -520,26 +604,10 @@ class NetworkRequest {
return this._previousCallbacks.getInterface(Ci.nsINetworkInterceptController); return this._previousCallbacks.getInterface(Ci.nsINetworkInterceptController);
} }
_activePageNetwork() {
if (!this._maybeInactivePageNetwork)
return undefined;
return this._maybeInactivePageNetwork;
}
_findPageNetwork() {
let loadContext = helper.getLoadContext(this.httpChannel);
if (!loadContext)
return;
const target = this._networkObserver._targetRegistry.targetForBrowser(loadContext.topFrameElement);
if (!target)
return;
return PageNetwork._forPageTarget(target);
}
_sendOnRequest(isIntercepted) { _sendOnRequest(isIntercepted) {
// Note: we call _sendOnRequest either after we intercepted the request, // Note: we call _sendOnRequest either after we intercepted the request,
// or at the first moment we know that we are not going to intercept. // or at the first moment we know that we are not going to intercept.
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (!pageNetwork) if (!pageNetwork)
return; return;
const loadInfo = this.httpChannel.loadInfo; const loadInfo = this.httpChannel.loadInfo;
@ -561,41 +629,49 @@ class NetworkRequest {
} }
_sendOnResponse(fromCache, opt_statusCode, opt_statusText) { _sendOnResponse(fromCache, opt_statusCode, opt_statusText) {
if (this._responseInterceptor) {
this._responseInterceptor.interceptOnResponse(this, fromCache, opt_statusCode, opt_statusText)
return;
}
this._sendOnResponseImpl(this.httpChannel, fromCache, opt_statusCode, opt_statusText);
}
_sendOnResponseImpl(httpChannel, fromCache, opt_statusCode, opt_statusText) {
if (this._sentOnResponse) { if (this._sentOnResponse) {
// We can come here twice because of internal redirects, e.g. service workers. // We can come here twice because of internal redirects, e.g. service workers.
return; return;
} }
this._sentOnResponse = true; this._sentOnResponse = true;
const pageNetwork = this._activePageNetwork(); const pageNetwork = this._pageNetwork;
if (!pageNetwork) if (!pageNetwork)
return; return;
this.httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
this.httpChannel.QueryInterface(Ci.nsITimedChannel); httpChannel.QueryInterface(Ci.nsITimedChannel);
const timing = { const timing = {
startTime: this.httpChannel.channelCreationTime, startTime: httpChannel.channelCreationTime,
domainLookupStart: this.httpChannel.domainLookupStartTime, domainLookupStart: httpChannel.domainLookupStartTime,
domainLookupEnd: this.httpChannel.domainLookupEndTime, domainLookupEnd: httpChannel.domainLookupEndTime,
connectStart: this.httpChannel.connectStartTime, connectStart: httpChannel.connectStartTime,
secureConnectionStart: this.httpChannel.secureConnectionStartTime, secureConnectionStart: httpChannel.secureConnectionStartTime,
connectEnd: this.httpChannel.connectEndTime, connectEnd: httpChannel.connectEndTime,
requestStart: this.httpChannel.requestStartTime, requestStart: httpChannel.requestStartTime,
responseStart: this.httpChannel.responseStartTime, responseStart: httpChannel.responseStartTime,
}; };
const { status, statusText, headers } = responseHead(this.httpChannel, opt_statusCode, opt_statusText); const { status, statusText, headers } = responseHead(httpChannel, opt_statusCode, opt_statusText);
let remoteIPAddress = undefined; let remoteIPAddress = undefined;
let remotePort = undefined; let remotePort = undefined;
try { try {
remoteIPAddress = this.httpChannel.remoteAddress; remoteIPAddress = httpChannel.remoteAddress;
remotePort = this.httpChannel.remotePort; remotePort = httpChannel.remotePort;
} catch (e) { } catch (e) {
// remoteAddress is not defined for cached requests. // remoteAddress is not defined for cached requests.
} }
pageNetwork.emit(PageNetwork.Events.Response, { pageNetwork.emit(PageNetwork.Events.Response, {
requestId: this.requestId, requestId: this.requestId,
securityDetails: getSecurityDetails(this.httpChannel), securityDetails: getSecurityDetails(httpChannel),
fromCache, fromCache,
headers, headers,
remoteIPAddress, remoteIPAddress,
@ -607,7 +683,15 @@ class NetworkRequest {
} }
_sendOnRequestFailed(error) { _sendOnRequestFailed(error) {
const pageNetwork = this._activePageNetwork(); if (this._responseInterceptor) {
this._responseInterceptor.interceptOnRequestFailed(error);
return;
}
this._sendOnRequestFailedImpl(error);
}
_sendOnRequestFailedImpl(error) {
const pageNetwork = this._pageNetwork;
if (pageNetwork) { if (pageNetwork) {
pageNetwork.emit(PageNetwork.Events.RequestFailed, { pageNetwork.emit(PageNetwork.Events.RequestFailed, {
requestId: this.requestId, requestId: this.requestId,
@ -618,15 +702,23 @@ class NetworkRequest {
} }
_sendOnRequestFinished() { _sendOnRequestFinished() {
const pageNetwork = this._activePageNetwork(); if (this._responseInterceptor) {
this._responseInterceptor.interceptOnRequestFinished(this);
return;
}
this._sendOnRequestFinishedImpl(this.httpChannel);
}
_sendOnRequestFinishedImpl(httpChannel) {
const pageNetwork = this._pageNetwork;
if (pageNetwork) { if (pageNetwork) {
pageNetwork.emit(PageNetwork.Events.RequestFinished, { pageNetwork.emit(PageNetwork.Events.RequestFinished, {
requestId: this.requestId, requestId: this.requestId,
responseEndTime: this.httpChannel.responseEndTime, responseEndTime: httpChannel.responseEndTime,
transferSize: this.httpChannel.transferSize, transferSize: httpChannel.transferSize,
}, this._frameId); }, this._frameId);
} }
this._networkObserver._channelToRequest.delete(this.httpChannel); this._networkObserver._channelToRequest.delete(httpChannel);
} }
} }
@ -719,8 +811,14 @@ class NetworkObserver {
} }
} }
pageNetworkForTarget(target) { _findPageNetwork(httpChannel) {
return PageNetwork._forPageTarget(target); let loadContext = helper.getLoadContext(httpChannel);
if (!loadContext)
return;
const target = this._targetRegistry.targetForBrowser(loadContext.topFrameElement);
if (!target)
return;
return PageNetwork.forPageTarget(target);
} }
_onRequest(channel, topic) { _onRequest(channel, topic) {
@ -746,7 +844,7 @@ class NetworkObserver {
_onResponse(fromCache, httpChannel, topic) { _onResponse(fromCache, httpChannel, topic) {
const request = this._channelToRequest.get(httpChannel); const request = this._channelToRequest.get(httpChannel);
if (request) if (request)
request._onResponse(fromCache); request._sendOnResponse(fromCache);
} }
dispose() { dispose() {

View File

@ -72,7 +72,7 @@ class PageHandler {
this._workers = new Map(); this._workers = new Map();
this._pageTarget = target; this._pageTarget = target;
this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); this._pageNetwork = PageNetwork.forPageTarget(target);
const emitProtocolEvent = eventName => { const emitProtocolEvent = eventName => {
return (...args) => this._session.emitEvent(eventName, ...args); return (...args) => this._session.emitEvent(eventName, ...args);