browser(firefox): support request interception (#572)

10558a7ead
This commit is contained in:
Dmitry Gozman 2020-01-22 17:21:25 -08:00 committed by Andrey Lushnikov
parent 869ffc8afd
commit 1c96d42a4b
2 changed files with 360 additions and 108 deletions

View file

@ -1 +1 @@
1016 1017

View file

@ -648,10 +648,10 @@ index 0000000000000000000000000000000000000000..673e93b0278a3502d94006696cea7e6e
+ +
diff --git a/testing/juggler/NetworkObserver.js b/testing/juggler/NetworkObserver.js diff --git a/testing/juggler/NetworkObserver.js b/testing/juggler/NetworkObserver.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca30a73e723 index 0000000000000000000000000000000000000000..e38c9b37b531de4eac67c2a138b68a34053b155b
--- /dev/null --- /dev/null
+++ b/testing/juggler/NetworkObserver.js +++ b/testing/juggler/NetworkObserver.js
@@ -0,0 +1,450 @@ @@ -0,0 +1,674 @@
+"use strict"; +"use strict";
+ +
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
@ -701,11 +701,14 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); + this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor);
+ this._activityDistributor.addObserver(this); + this._activityDistributor.addObserver(this);
+ +
+ this._redirectMap = new Map(); + this._redirectMap = new Map(); // oldId => newId
+ this._resumedRequestIdToHeaders = new Map(); // requestId => { headers }
+ this._postResumeChannelIdToRequestId = new Map(); // post-resume channel id => pre-resume request id
+
+ this._channelSink = { + this._channelSink = {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]), + QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
+ asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => { + asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
+ this._onRedirect(oldChannel, newChannel); + this._onRedirect(oldChannel, newChannel, flags);
+ callback.onRedirectVerifyCallback(Cr.NS_OK); + callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }, + },
+ }; + };
@ -718,10 +721,10 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory); + registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory);
+ Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true); + Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true);
+ +
+ // Request interception state. + this._browserInterceptors = new Map(); // Browser => (requestId => interceptor).
+ this._browserSuspendedChannels = new Map();
+ this._extraHTTPHeaders = new Map(); + this._extraHTTPHeaders = new Map();
+ this._browserResponseStorages = new Map(); + this._browserResponseStorages = new Map();
+ this._browserAuthCredentials = new Map();
+ +
+ this._eventListeners = [ + this._eventListeners = [
+ helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'), + helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'),
@ -739,36 +742,32 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ } + }
+ +
+ enableRequestInterception(browser) { + enableRequestInterception(browser) {
+ if (!this._browserSuspendedChannels.has(browser)) + if (!this._browserInterceptors.has(browser))
+ this._browserSuspendedChannels.set(browser, new Map()); + this._browserInterceptors.set(browser, new Map());
+ } + }
+ +
+ disableRequestInterception(browser) { + disableRequestInterception(browser) {
+ const suspendedChannels = this._browserSuspendedChannels.get(browser); + const interceptors = this._browserInterceptors.get(browser);
+ if (!suspendedChannels) + if (!interceptors)
+ return; + return;
+ this._browserSuspendedChannels.delete(browser); + this._browserInterceptors.delete(browser);
+ for (const channel of suspendedChannels.values()) + for (const interceptor of interceptors.values())
+ channel.resume(); + interceptor._resume();
+ } + }
+ +
+ resumeSuspendedRequest(browser, requestId, headers) { + _takeInterceptor(browser, requestId) {
+ const suspendedChannels = this._browserSuspendedChannels.get(browser); + const interceptors = this._browserInterceptors.get(browser);
+ if (!suspendedChannels) + if (!interceptors)
+ throw new Error(`Request interception is not enabled`); + throw new Error(`Request interception is not enabled`);
+ const httpChannel = suspendedChannels.get(requestId); + const interceptor = interceptors.get(requestId);
+ if (!httpChannel) + if (!interceptor)
+ throw new Error(`Cannot find request "${requestId}"`); + throw new Error(`Cannot find request "${requestId}"`);
+ if (headers) { + interceptors.delete(requestId);
+ // 1. Clear all previous headers. + return interceptor;
+ for (const header of requestHeaders(httpChannel)) + }
+ httpChannel.setRequestHeader(header.name, '', false /* merge */); +
+ // 2. Set new headers. + resumeInterceptedRequest(browser, requestId, headers) {
+ for (const header of headers) + this._takeInterceptor(browser, requestId)._resume(headers);
+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
+ }
+ suspendedChannels.delete(requestId);
+ httpChannel.resume();
+ } + }
+ +
+ getResponseBody(browser, requestId) { + getResponseBody(browser, requestId) {
@ -778,30 +777,50 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ return responseStorage.getBase64EncodedResponse(requestId); + return responseStorage.getBase64EncodedResponse(requestId);
+ } + }
+ +
+ abortSuspendedRequest(browser, aRequestId) { + fulfillInterceptedRequest(browser, requestId, status, statusText, headers, base64body) {
+ const suspendedChannels = this._browserSuspendedChannels.get(browser); + this._takeInterceptor(browser, requestId)._fulfill(status, statusText, headers, base64body);
+ if (!suspendedChannels)
+ throw new Error(`Request interception is not enabled`);
+ const httpChannel = suspendedChannels.get(aRequestId);
+ if (!httpChannel)
+ throw new Error(`Cannot find request "${aRequestId}"`);
+ suspendedChannels.delete(aRequestId);
+ httpChannel.cancel(Cr.NS_ERROR_FAILURE);
+ httpChannel.resume();
+ this.emit('requestfailed', httpChannel, {
+ requestId: requestId(httpChannel),
+ errorCode: helper.getNetworkErrorStatusText(httpChannel.status),
+ });
+ } + }
+ +
+ _onRedirect(oldChannel, newChannel) { + abortInterceptedRequest(browser, requestId, errorCode) {
+ if (!(oldChannel instanceof Ci.nsIHttpChannel)) + this._takeInterceptor(browser, requestId)._abort(errorCode);
+ }
+
+ setAuthCredentials(browser, username, password) {
+ this._browserAuthCredentials.set(browser, { username, password });
+ }
+
+ _requestId(httpChannel) {
+ const id = httpChannel.channelId + '';
+ return this._postResumeChannelIdToRequestId.get(id) || id;
+ }
+
+ _onRedirect(oldChannel, newChannel, flags) {
+ if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel))
+ return; + return;
+ const httpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel); + const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
+ const loadContext = getLoadContext(httpChannel); + const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) + const browser = this._getBrowserForChannel(oldHttpChannel);
+ if (!browser)
+ return; + return;
+ this._redirectMap.set(newChannel, oldChannel); + const oldRequestId = this._requestId(oldHttpChannel);
+ const newRequestId = this._requestId(newHttpChannel);
+ if (this._resumedRequestIdToHeaders.has(oldRequestId)) {
+ // When we call resetInterception on a request, we get a new "redirected" request for it.
+ const { headers } = this._resumedRequestIdToHeaders.get(oldRequestId);
+ if (headers) {
+ // Apply new request headers from interception resume.
+ for (const header of requestHeaders(newChannel))
+ newChannel.setRequestHeader(header.name, '', false /* merge */);
+ for (const header of headers)
+ newChannel.setRequestHeader(header.name, header.value, false /* merge */);
+ }
+ // Use the old request id for the new "redirected" request for protocol consistency.
+ this._resumedRequestIdToHeaders.delete(oldRequestId);
+ this._postResumeChannelIdToRequestId.set(newRequestId, oldRequestId);
+ } else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) {
+ // Regular (non-internal) redirect.
+ this._redirectMap.set(newRequestId, oldRequestId);
+ }
+ } + }
+ +
+ observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) { + observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) {
@ -810,56 +829,130 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ if (!(channel instanceof Ci.nsIHttpChannel)) + if (!(channel instanceof Ci.nsIHttpChannel))
+ return; + return;
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ const loadContext = getLoadContext(httpChannel); + const browser = this._getBrowserForChannel(httpChannel);
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) + if (!browser)
+ return; + return;
+ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) + if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
+ return; + return;
+ this.emit('requestfinished', httpChannel, { + if (this._isResumedChannel(httpChannel))
+ requestId: requestId(httpChannel), + return;
+ }); + this._sendOnRequestFinished(httpChannel);
+ }
+
+ _getBrowserForChannel(httpChannel) {
+ let loadContext = null;
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
+ return;
+ return loadContext.topFrameElement;
+ }
+
+ _isResumedChannel(httpChannel) {
+ return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + '');
+ } + }
+ +
+ _onRequest(channel, topic) { + _onRequest(channel, topic) {
+ if (!(channel instanceof Ci.nsIHttpChannel)) + if (!(channel instanceof Ci.nsIHttpChannel))
+ return; + return;
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ const loadContext = getLoadContext(httpChannel); + const browser = this._getBrowserForChannel(httpChannel);
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) + if (!browser)
+ return; + return;
+ const extraHeaders = this._extraHTTPHeaders.get(loadContext.topFrameElement); + if (this._isResumedChannel(httpChannel)) {
+ // Ignore onRequest for resumed requests, but listen to their response.
+ new ResponseBodyListener(this, browser, httpChannel);
+ return;
+ }
+ const extraHeaders = this._extraHTTPHeaders.get(browser);
+ if (extraHeaders) { + if (extraHeaders) {
+ for (const header of extraHeaders) + for (const header of extraHeaders)
+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */); + httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
+ } + }
+ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; + const requestId = this._requestId(httpChannel);
+ const suspendedChannels = this._browserSuspendedChannels.get(loadContext.topFrameElement); + const isRedirect = this._redirectMap.has(requestId);
+ if (suspendedChannels) { + const interceptors = this._browserInterceptors.get(browser);
+ httpChannel.suspend(); + if (!interceptors) {
+ suspendedChannels.set(requestId(httpChannel), httpChannel); + new NotificationCallbacks(this, browser, httpChannel, false);
+ this._sendOnRequest(httpChannel, false);
+ new ResponseBodyListener(this, browser, httpChannel);
+ } else if (isRedirect) {
+ // We pretend that redirect is interceptable in the protocol, although it's actually not
+ // and therefore we do not instantiate the interceptor.
+ // TODO: look into REDIRECT_MODE_MANUAL.
+ interceptors.set(requestId, {
+ _resume: () => {},
+ _abort: () => {},
+ _fulfill: () => {},
+ });
+ new NotificationCallbacks(this, browser, httpChannel, false);
+ this._sendOnRequest(httpChannel, true);
+ new ResponseBodyListener(this, browser, httpChannel);
+ } else {
+ new NotificationCallbacks(this, browser, httpChannel, true);
+ // We'll issue onRequest once it's intercepted.
+ } + }
+ const oldChannel = this._redirectMap.get(httpChannel); + }
+ this._redirectMap.delete(httpChannel);
+ +
+ // Install response body hooks. + _onIntercepted(httpChannel, interceptor) {
+ new ResponseBodyListener(this, loadContext.topFrameElement, httpChannel); + const browser = this._getBrowserForChannel(httpChannel);
+ if (!browser) {
+ interceptor._resume();
+ return;
+ }
+ const interceptors = this._browserInterceptors.get(browser);
+ this._sendOnRequest(httpChannel, !!interceptors);
+ if (interceptors)
+ interceptors.set(this._requestId(httpChannel), interceptor);
+ else
+ interceptor._resume();
+ }
+ +
+ _sendOnRequest(httpChannel, isIntercepted) {
+ const browser = this._getBrowserForChannel(httpChannel);
+ if (!browser)
+ return;
+ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
+ const requestId = this._requestId(httpChannel);
+ const redirectedFrom = this._redirectMap.get(requestId);
+ this._redirectMap.delete(requestId);
+ this.emit('request', httpChannel, { + this.emit('request', httpChannel, {
+ url: httpChannel.URI.spec, + url: httpChannel.URI.spec,
+ suspended: suspendedChannels ? true : undefined, + isIntercepted,
+ requestId: requestId(httpChannel), + requestId,
+ redirectedFrom: oldChannel ? requestId(oldChannel) : undefined, + redirectedFrom,
+ postData: readRequestPostData(httpChannel), + postData: readRequestPostData(httpChannel),
+ headers: requestHeaders(httpChannel), + headers: requestHeaders(httpChannel),
+ method: httpChannel.requestMethod, + method: httpChannel.requestMethod,
+ navigationId: httpChannel.isMainDocumentChannel ? requestId(httpChannel) : undefined, + navigationId: httpChannel.isMainDocumentChannel ? this._requestId(httpChannel) : undefined,
+ cause: causeTypeToString(causeType), + cause: causeTypeToString(causeType),
+ }); + });
+ } + }
+ +
+ _sendOnRequestFinished(httpChannel) {
+ this.emit('requestfinished', httpChannel, {
+ requestId: this._requestId(httpChannel),
+ });
+ this._postResumeChannelIdToRequestId.delete(httpChannel.channelId + '');
+ }
+
+ _sendOnRequestFailed(httpChannel, error) {
+ this.emit('requestfailed', httpChannel, {
+ requestId: this._requestId(httpChannel),
+ errorCode: helper.getNetworkErrorStatusText(error),
+ });
+ this._postResumeChannelIdToRequestId.delete(httpChannel.channelId + '');
+ }
+
+ _onResponse(fromCache, httpChannel, topic) { + _onResponse(fromCache, httpChannel, topic) {
+ const loadContext = getLoadContext(httpChannel); + const browser = this._getBrowserForChannel(httpChannel);
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement)) + if (!browser)
+ return; + return;
+ httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); + httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
+ const headers = []; + const headers = [];
@ -876,7 +969,7 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ // remoteAddress is not defined for cached requests. + // remoteAddress is not defined for cached requests.
+ } + }
+ this.emit('response', httpChannel, { + this.emit('response', httpChannel, {
+ requestId: requestId(httpChannel), + requestId: this._requestId(httpChannel),
+ securityDetails: getSecurityDetails(httpChannel), + securityDetails: getSecurityDetails(httpChannel),
+ fromCache, + fromCache,
+ headers, + headers,
@ -892,16 +985,14 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ if (!responseStorage) + if (!responseStorage)
+ return; + return;
+ responseStorage.addResponseBody(httpChannel, body); + responseStorage.addResponseBody(httpChannel, body);
+ this.emit('requestfinished', httpChannel, { + this._sendOnRequestFinished(httpChannel);
+ requestId: requestId(httpChannel),
+ });
+ } + }
+ +
+ startTrackingBrowserNetwork(browser) { + startTrackingBrowserNetwork(browser) {
+ const value = this._browserSessionCount.get(browser) || 0; + const value = this._browserSessionCount.get(browser) || 0;
+ this._browserSessionCount.set(browser, value + 1); + this._browserSessionCount.set(browser, value + 1);
+ if (value === 0) + if (value === 0)
+ this._browserResponseStorages.set(browser, new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10)); + this._browserResponseStorages.set(browser, new ResponseStorage(this, MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10));
+ return () => this.stopTrackingBrowserNetwork(browser); + return () => this.stopTrackingBrowserNetwork(browser);
+ } + }
+ +
@ -912,6 +1003,8 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ } else { + } else {
+ this._browserSessionCount.delete(browser); + this._browserSessionCount.delete(browser);
+ this._browserResponseStorages.delete(browser); + this._browserResponseStorages.delete(browser);
+ this._browserAuthCredentials.delete(browser);
+ this._browserInterceptors.delete(browser);
+ } + }
+ } + }
+ +
@ -982,23 +1075,6 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ return text; + return text;
+} +}
+ +
+function getLoadContext(httpChannel) {
+ let loadContext = null;
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ return loadContext;
+}
+
+function requestId(httpChannel) {
+ return httpChannel.channelId + '';
+}
+
+function requestHeaders(httpChannel) { +function requestHeaders(httpChannel) {
+ const headers = []; + const headers = [];
+ httpChannel.visitRequestHeaders({ + httpChannel.visitRequestHeaders({
@ -1016,7 +1092,8 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+} +}
+ +
+class ResponseStorage { +class ResponseStorage {
+ constructor(maxTotalSize, maxResponseSize) { + constructor(networkObserver, maxTotalSize, maxResponseSize) {
+ this._networkObserver = networkObserver;
+ this._totalSize = 0; + this._totalSize = 0;
+ this._maxResponseSize = maxResponseSize; + this._maxResponseSize = maxResponseSize;
+ this._maxTotalSize = maxTotalSize; + this._maxTotalSize = maxTotalSize;
@ -1036,7 +1113,7 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ const encodingHeader = httpChannel.getResponseHeader("Content-Encoding"); + const encodingHeader = httpChannel.getResponseHeader("Content-Encoding");
+ encodings = encodingHeader.split(/\s*\t*,\s*\t*/); + encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
+ } + }
+ this._responses.set(requestId(httpChannel), {body, encodings}); + this._responses.set(this._networkObserver._requestId(httpChannel), {body, encodings});
+ this._totalSize += body.length; + this._totalSize += body.length;
+ if (this._totalSize > this._maxTotalSize) { + if (this._totalSize > this._maxTotalSize) {
+ for (let [requestId, response] of this._responses) { + for (let [requestId, response] of this._responses) {
@ -1100,6 +1177,153 @@ index 0000000000000000000000000000000000000000..2afbc74a4170233e76dadd7e7b294ca3
+ } + }
+} +}
+ +
+class NotificationCallbacks {
+ constructor(networkObserver, browser, httpChannel, shouldIntercept) {
+ this._networkObserver = networkObserver;
+ this._browser = browser;
+ this._shouldIntercept = shouldIntercept;
+ this._httpChannel = httpChannel;
+ this._previousCallbacks = httpChannel.notificationCallbacks;
+ httpChannel.notificationCallbacks = this;
+
+ const qis = [
+ Ci.nsIAuthPrompt2,
+ Ci.nsIAuthPromptProvider,
+ Ci.nsIInterfaceRequestor,
+ ];
+ if (shouldIntercept)
+ qis.push(Ci.nsINetworkInterceptController);
+ this.QueryInterface = ChromeUtils.generateQI(qis);
+ }
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider))
+ return this;
+ if (this._shouldIntercept && iid.equals(Ci.nsINetworkInterceptController))
+ return this;
+ if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead.
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ if (this._previousCallbacks)
+ return this._previousCallbacks.getInterface(iid);
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+
+ _forward(iid, method, args) {
+ if (!this._previousCallbacks)
+ return;
+ try {
+ const impl = this._previousCallbacks.getInterface(iid);
+ impl[method].apply(impl, args);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NO_INTERFACE)
+ throw e;
+ }
+ }
+
+ // nsIAuthPromptProvider
+ getAuthPrompt(aPromptReason, iid) {
+ return this;
+ }
+
+ // nsIAuthPrompt2
+ asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) {
+ let canceled = false;
+ Promise.resolve().then(() => {
+ if (canceled)
+ return;
+ const hasAuth = this.promptAuth(aChannel, level, authInfo);
+ if (hasAuth)
+ aCallback.onAuthAvailable(aContext, authInfo);
+ else
+ aCallback.onAuthCancelled(aContext, true);
+ });
+ return {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
+ cancel: () => {
+ aCallback.onAuthCancelled(aContext, false);
+ canceled = true;
+ }
+ };
+ }
+
+ // nsIAuthPrompt2
+ promptAuth(aChannel, level, authInfo) {
+ if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
+ return false;
+ const credentials = this._networkObserver._browserAuthCredentials.get(this._browser);
+ if (!credentials || credentials.username === null)
+ return false;
+ authInfo.username = credentials.username;
+ authInfo.password = credentials.password;
+ return true;
+ }
+
+ // nsINetworkInterceptController
+ shouldPrepareForIntercept(aURI, channel) {
+ if (!(channel instanceof Ci.nsIHttpChannel))
+ return false;
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ return httpChannel.channelId === this._httpChannel.channelId;
+ }
+
+ // nsINetworkInterceptController
+ channelIntercepted(intercepted) {
+ this._intercepted = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
+ const httpChannel = this._intercepted.channel.QueryInterface(Ci.nsIHttpChannel);
+ this._networkObserver._onIntercepted(httpChannel, this);
+ }
+
+ _resume(headers) {
+ this._networkObserver._resumedRequestIdToHeaders.set(this._networkObserver._requestId(this._httpChannel), { headers });
+ this._intercepted.resetInterception();
+ }
+
+ _fulfill(status, statusText, headers, base64body) {
+ this._intercepted.synthesizeStatus(status, statusText);
+ for (const header of headers)
+ this._intercepted.synthesizeHeader(header.name, header.value);
+ const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ if (base64body)
+ synthesized.data = atob(base64body);
+ else
+ synthesized.data = '';
+ this._intercepted.startSynthesizedResponse(synthesized, null, null, '', false);
+ this._intercepted.finishSynthesizedResponse();
+ this._networkObserver.emit('response', this._httpChannel, {
+ requestId: this._networkObserver._requestId(this._httpChannel),
+ securityDetails: null,
+ fromCache: false,
+ headers,
+ status,
+ statusText,
+ });
+ this._networkObserver._sendOnRequestFinished(this._httpChannel);
+ }
+
+ _abort(errorCode) {
+ const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
+ this._intercepted.cancelInterception(error);
+ this._networkObserver._sendOnRequestFailed(this._httpChannel, error);
+ }
+}
+
+const errorMap = {
+ 'aborted': Cr.NS_ERROR_ABORT,
+ 'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
+ 'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST,
+ 'blockedbyclient': Cr.NS_ERROR_FAILURE,
+ 'blockedbyresponse': Cr.NS_ERROR_FAILURE,
+ 'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT,
+ 'connectionclosed': Cr.NS_ERROR_FAILURE,
+ 'connectionfailed': Cr.NS_ERROR_FAILURE,
+ 'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED,
+ 'connectionreset': Cr.NS_ERROR_NET_RESET,
+ 'internetdisconnected': Cr.NS_ERROR_OFFLINE,
+ 'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST,
+ 'timedout': Cr.NS_ERROR_NET_TIMEOUT,
+ 'failed': Cr.NS_ERROR_FAILURE,
+};
+
+var EXPORTED_SYMBOLS = ['NetworkObserver']; +var EXPORTED_SYMBOLS = ['NetworkObserver'];
+this.NetworkObserver = NetworkObserver; +this.NetworkObserver = NetworkObserver;
diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js diff --git a/testing/juggler/TargetRegistry.js b/testing/juggler/TargetRegistry.js
@ -3915,10 +4139,10 @@ index 0000000000000000000000000000000000000000..956988738079272be8d3998dcbbaa91a
+ +
diff --git a/testing/juggler/protocol/NetworkHandler.js b/testing/juggler/protocol/NetworkHandler.js diff --git a/testing/juggler/protocol/NetworkHandler.js b/testing/juggler/protocol/NetworkHandler.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878cccefa64 index 0000000000000000000000000000000000000000..22e7b4f9397e592f26ce447aafd6318398ad5b48
--- /dev/null --- /dev/null
+++ b/testing/juggler/protocol/NetworkHandler.js +++ b/testing/juggler/protocol/NetworkHandler.js
@@ -0,0 +1,154 @@ @@ -0,0 +1,166 @@
+"use strict"; +"use strict";
+ +
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
@ -3943,6 +4167,7 @@ index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878
+ this._requestInterception = false; + this._requestInterception = false;
+ this._eventListeners = []; + this._eventListeners = [];
+ this._pendingRequstWillBeSentEvents = new Set(); + this._pendingRequstWillBeSentEvents = new Set();
+ this._requestIdToFrameId = new Map();
+ } + }
+ +
+ async enable() { + async enable() {
@ -3976,12 +4201,20 @@ index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878
+ await Promise.all(Array.from(this._pendingRequstWillBeSentEvents)); + await Promise.all(Array.from(this._pendingRequstWillBeSentEvents));
+ } + }
+ +
+ async resumeSuspendedRequest({requestId, headers}) { + async resumeInterceptedRequest({requestId, headers}) {
+ this._networkObserver.resumeSuspendedRequest(this._browser, requestId, headers); + this._networkObserver.resumeInterceptedRequest(this._browser, requestId, headers);
+ } + }
+ +
+ async abortSuspendedRequest({requestId}) { + async abortInterceptedRequest({requestId, errorCode}) {
+ this._networkObserver.abortSuspendedRequest(this._browser, requestId); + this._networkObserver.abortInterceptedRequest(this._browser, requestId, errorCode);
+ }
+
+ async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) {
+ this._networkObserver.fulfillInterceptedRequest(this._browser, requestId, status, statusText, headers, base64body);
+ }
+
+ async setAuthCredentials({username, password}) {
+ this._networkObserver.setAuthCredentials(this._browser, username, password);
+ } + }
+ +
+ dispose() { + dispose() {
@ -4042,9 +4275,12 @@ index 0000000000000000000000000000000000000000..f5e7e919594b3778fd3046bf69d34878
+ return; + return;
+ } + }
+ } + }
+ // Inherit frameId for redirects when details are not available.
+ const frameId = details ? details.frameId : (eventDetails.redirectedFrom ? this._requestIdToFrameId.get(eventDetails.redirectedFrom) : undefined);
+ this._requestIdToFrameId.set(eventDetails.requestId, frameId);
+ const activity = this._ensureHTTPActivity(eventDetails.requestId); + const activity = this._ensureHTTPActivity(eventDetails.requestId);
+ activity.request = { + activity.request = {
+ frameId: details ? details.frameId : undefined, + frameId,
+ ...eventDetails, + ...eventDetails,
+ }; + };
+ this._reportHTTPAcitivityEvents(activity); + this._reportHTTPAcitivityEvents(activity);
@ -4515,10 +4751,10 @@ index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07
+this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; +this.EXPORTED_SYMBOLS = ['t', 'checkScheme'];
diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js diff --git a/testing/juggler/protocol/Protocol.js b/testing/juggler/protocol/Protocol.js
new file mode 100644 new file mode 100644
index 0000000000000000000000000000000000000000..1eecb6120f101cb7506fcf8d40c177089e62671b index 0000000000000000000000000000000000000000..a0913f7728931a938b850083213560a511b624a8
--- /dev/null --- /dev/null
+++ b/testing/juggler/protocol/Protocol.js +++ b/testing/juggler/protocol/Protocol.js
@@ -0,0 +1,731 @@ @@ -0,0 +1,747 @@
+const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
+ +
+// Protocol-specific types. +// Protocol-specific types.
@ -4815,7 +5051,7 @@ index 0000000000000000000000000000000000000000..1eecb6120f101cb7506fcf8d40c17708
+ redirectedFrom: t.Optional(t.String), + redirectedFrom: t.Optional(t.String),
+ postData: t.Optional(t.String), + postData: t.Optional(t.String),
+ headers: t.Array(networkTypes.HTTPHeader), + headers: t.Array(networkTypes.HTTPHeader),
+ suspended: t.Optional(t.Boolean), + isIntercepted: t.Boolean,
+ url: t.String, + url: t.String,
+ method: t.String, + method: t.String,
+ navigationId: t.Optional(t.String), + navigationId: t.Optional(t.String),
@ -4851,17 +5087,27 @@ index 0000000000000000000000000000000000000000..1eecb6120f101cb7506fcf8d40c17708
+ headers: t.Array(networkTypes.HTTPHeader), + headers: t.Array(networkTypes.HTTPHeader),
+ }, + },
+ }, + },
+ 'abortSuspendedRequest': { + 'abortInterceptedRequest': {
+ params: { + params: {
+ requestId: t.String, + requestId: t.String,
+ errorCode: t.String,
+ }, + },
+ }, + },
+ 'resumeSuspendedRequest': { + 'resumeInterceptedRequest': {
+ params: { + params: {
+ requestId: t.String, + requestId: t.String,
+ headers: t.Optional(t.Array(networkTypes.HTTPHeader)), + headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
+ }, + },
+ }, + },
+ 'fulfillInterceptedRequest': {
+ params: {
+ requestId: t.String,
+ status: t.Number,
+ statusText: t.String,
+ headers: t.Array(networkTypes.HTTPHeader),
+ base64body: t.Optional(t.String), // base64-encoded
+ },
+ },
+ 'getResponseBody': { + 'getResponseBody': {
+ params: { + params: {
+ requestId: t.String, + requestId: t.String,
@ -4871,6 +5117,12 @@ index 0000000000000000000000000000000000000000..1eecb6120f101cb7506fcf8d40c17708
+ evicted: t.Optional(t.Boolean), + evicted: t.Optional(t.Boolean),
+ }, + },
+ }, + },
+ 'setAuthCredentials': {
+ params: {
+ username: t.Nullable(t.String),
+ password: t.Nullable(t.String),
+ },
+ },
+ }, + },
+}; +};
+ +