diff --git a/browser_patches/firefox-beta/BUILD_NUMBER b/browser_patches/firefox-beta/BUILD_NUMBER index 5bcd068ab7..a76abc1bfc 100644 --- a/browser_patches/firefox-beta/BUILD_NUMBER +++ b/browser_patches/firefox-beta/BUILD_NUMBER @@ -1,2 +1,2 @@ -1274 -Changed: lushnikov@chromium.org Fri 06 Aug 2021 07:58:41 AM PDT +1275 +Changed: yurys@chromium.org Tue 10 Aug 2021 03:19:51 PM PDT diff --git a/browser_patches/firefox-beta/juggler/NetworkObserver.js b/browser_patches/firefox-beta/juggler/NetworkObserver.js index 1f3a11a3d4..68bb804f65 100644 --- a/browser_patches/firefox-beta/juggler/NetworkObserver.js +++ b/browser_patches/firefox-beta/juggler/NetworkObserver.js @@ -38,7 +38,7 @@ const SINK_CATEGORY_NAME = "net-channel-event-sinks"; const pageNetworkSymbol = Symbol('PageNetwork'); class PageNetwork { - static _forPageTarget(target) { + static forPageTarget(target) { let result = target[pageNetworkSymbol]; if (!result) { result = new PageNetwork(target); @@ -78,7 +78,7 @@ class PageNetwork { const intercepted = this._interceptedRequests.get(requestId); if (!intercepted) 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); 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 { constructor(networkObserver, httpChannel, redirectedFrom) { this._networkObserver = networkObserver; @@ -139,14 +274,18 @@ class NetworkRequest { // 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. 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._expectingResumedRequest = undefined; // { method, headers, postData } this._sentOnResponse = false; - const pageNetwork = this._activePageNetwork(); + const pageNetwork = this._pageNetwork; if (pageNetwork) { appendExtraHTTPHeaders(httpChannel, pageNetwork._target.browserContext().extraHTTPHeaders); appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders); @@ -156,7 +295,10 @@ class NetworkRequest { httpChannel.QueryInterface(Ci.nsITraceableChannel); 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 // calling into previous NetworkRequest. this._originalListener = redirectedFrom._originalListener; @@ -187,64 +329,6 @@ class NetworkRequest { 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. abort(errorCode) { const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE; @@ -255,11 +339,12 @@ class NetworkRequest { // Public interception API. fulfill(status, statusText, headers, base64body) { let body = base64body ? atob(base64body) : ''; - if (this._interceptedResponse) { - status = status || this._interceptedResponse.status; - statusText = statusText || this._interceptedResponse.statusText; - headers = headers || this._interceptedResponse.headers; - body = body || this._interceptedResponse.body; + const originalResponse = this._responseInterceptor?._interceptedResponse; + if (originalResponse) { + status = status || originalResponse.status; + statusText = statusText || originalResponse.statusText; + headers = headers || originalResponse.headers; + body = body || originalResponse.body; } this._interceptedChannel.synthesizeStatus(status, statusText); for (const header of headers) { @@ -308,11 +393,6 @@ class NetworkRequest { setPostData(this.httpChannel, postData, headers); } - // Instrumentation called by NetworkObserver. - _onResponse(fromCache) { - this._sendOnResponse(fromCache); - } - // nsIInterfaceRequestor getInterface(iid) { if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController)) @@ -354,7 +434,7 @@ class NetworkRequest { promptAuth(aChannel, level, authInfo) { if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) return false; - const pageNetwork = this._activePageNetwork(); + const pageNetwork = this._pageNetwork; if (!pageNetwork) return false; let credentials = null; @@ -421,7 +501,7 @@ class NetworkRequest { this._expectingInterception = false; this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel); - const pageNetwork = this._activePageNetwork(); + const pageNetwork = this._pageNetwork; if (!pageNetwork) { // Just in case we disabled instrumentation while intercepting, resume and forget. this.resume(); @@ -489,9 +569,13 @@ class NetworkRequest { // we do not get onResponse normally, but we do get nsIRequestObserver notifications. this._sendOnResponse(false); const body = this._responseBodyChunks.join(''); - const pageNetwork = this._activePageNetwork(); - if (pageNetwork) - pageNetwork._responseStorage.addResponseBody(this, this.httpChannel, body); + const pageNetwork = this._pageNetwork; + if (pageNetwork) { + if (this._responseInterceptor) + this._responseInterceptor.interceptAddResponseBody(this, body); + else + pageNetwork._responseStorage.addResponseBody(this, this.httpChannel, body); + } this._sendOnRequestFinished(); } else { this._sendOnRequestFailed(aStatusCode); @@ -501,7 +585,7 @@ class NetworkRequest { } _shouldIntercept() { - const pageNetwork = this._activePageNetwork(); + const pageNetwork = this._pageNetwork; if (!pageNetwork) return false; if (pageNetwork._requestInterceptionEnabled) @@ -520,26 +604,10 @@ class NetworkRequest { 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) { // Note: we call _sendOnRequest either after we intercepted the request, // or at the first moment we know that we are not going to intercept. - const pageNetwork = this._activePageNetwork(); + const pageNetwork = this._pageNetwork; if (!pageNetwork) return; const loadInfo = this.httpChannel.loadInfo; @@ -561,41 +629,49 @@ class NetworkRequest { } _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) { // We can come here twice because of internal redirects, e.g. service workers. return; } this._sentOnResponse = true; - const pageNetwork = this._activePageNetwork(); + const pageNetwork = this._pageNetwork; if (!pageNetwork) return; - this.httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); - this.httpChannel.QueryInterface(Ci.nsITimedChannel); + httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); + httpChannel.QueryInterface(Ci.nsITimedChannel); const timing = { - startTime: this.httpChannel.channelCreationTime, - domainLookupStart: this.httpChannel.domainLookupStartTime, - domainLookupEnd: this.httpChannel.domainLookupEndTime, - connectStart: this.httpChannel.connectStartTime, - secureConnectionStart: this.httpChannel.secureConnectionStartTime, - connectEnd: this.httpChannel.connectEndTime, - requestStart: this.httpChannel.requestStartTime, - responseStart: this.httpChannel.responseStartTime, + startTime: httpChannel.channelCreationTime, + domainLookupStart: httpChannel.domainLookupStartTime, + domainLookupEnd: httpChannel.domainLookupEndTime, + connectStart: httpChannel.connectStartTime, + secureConnectionStart: httpChannel.secureConnectionStartTime, + connectEnd: httpChannel.connectEndTime, + requestStart: httpChannel.requestStartTime, + 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 remotePort = undefined; try { - remoteIPAddress = this.httpChannel.remoteAddress; - remotePort = this.httpChannel.remotePort; + remoteIPAddress = httpChannel.remoteAddress; + remotePort = httpChannel.remotePort; } catch (e) { // remoteAddress is not defined for cached requests. } pageNetwork.emit(PageNetwork.Events.Response, { requestId: this.requestId, - securityDetails: getSecurityDetails(this.httpChannel), + securityDetails: getSecurityDetails(httpChannel), fromCache, headers, remoteIPAddress, @@ -607,7 +683,15 @@ class NetworkRequest { } _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) { pageNetwork.emit(PageNetwork.Events.RequestFailed, { requestId: this.requestId, @@ -618,15 +702,23 @@ class NetworkRequest { } _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) { pageNetwork.emit(PageNetwork.Events.RequestFinished, { requestId: this.requestId, - responseEndTime: this.httpChannel.responseEndTime, - transferSize: this.httpChannel.transferSize, + responseEndTime: httpChannel.responseEndTime, + transferSize: httpChannel.transferSize, }, this._frameId); } - this._networkObserver._channelToRequest.delete(this.httpChannel); + this._networkObserver._channelToRequest.delete(httpChannel); } } @@ -719,8 +811,14 @@ class NetworkObserver { } } - pageNetworkForTarget(target) { - return PageNetwork._forPageTarget(target); + _findPageNetwork(httpChannel) { + 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) { @@ -746,7 +844,7 @@ class NetworkObserver { _onResponse(fromCache, httpChannel, topic) { const request = this._channelToRequest.get(httpChannel); if (request) - request._onResponse(fromCache); + request._sendOnResponse(fromCache); } dispose() { diff --git a/browser_patches/firefox-beta/juggler/protocol/PageHandler.js b/browser_patches/firefox-beta/juggler/protocol/PageHandler.js index fa159246af..4754c38f25 100644 --- a/browser_patches/firefox-beta/juggler/protocol/PageHandler.js +++ b/browser_patches/firefox-beta/juggler/protocol/PageHandler.js @@ -72,7 +72,7 @@ class PageHandler { this._workers = new Map(); this._pageTarget = target; - this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); + this._pageNetwork = PageNetwork.forPageTarget(target); const emitProtocolEvent = eventName => { return (...args) => this._session.emitEvent(eventName, ...args);