From f628239ef33625be41312b3cb64360d62b323492 Mon Sep 17 00:00:00 2001 From: wuggy Date: Mon, 14 Sep 2026 15:06:27 -0700 Subject: [PATCH] finally finish webRequest --- browser/base/content/browser.js | 2 +- dom/webidl/ChannelWrapper.webidl | 2 +- .../webextensions/ext-webRequest.js | 23 ++--- .../test_webnavigation_created_target.js | 50 ++++++++++ .../test/xpcshell/test_webrequest_backend.js | 66 +++++++++++++ .../webextensions/test/xpcshell/xpcshell.ini | 2 + .../webextensions/webrequest/SecurityInfo.jsm | 6 +- .../webextensions/webrequest/WebRequest.jsm | 96 +++++++++++-------- .../webrequest/WebRequestUpload.jsm | 15 ++- 9 files changed, 195 insertions(+), 67 deletions(-) create mode 100644 toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js create mode 100644 toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 8c97910941..8e74dd1589 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5466,7 +5466,7 @@ function handleLinkClick(event, href, linkNode) { urlSecurityCheck(href, doc.nodePrincipal); let params = { charset: doc.characterSet, - currentBrowser: gBrowser.getBrowserForDocument(doc), + currentBrowser: gBrowser.getBrowserForContentWindow(doc.defaultView.top), frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils).outerWindowID, allowMixedContent: persistAllowMixedContentInChildTab, diff --git a/dom/webidl/ChannelWrapper.webidl b/dom/webidl/ChannelWrapper.webidl index 8a64dcde23..95b42f9111 100644 --- a/dom/webidl/ChannelWrapper.webidl +++ b/dom/webidl/ChannelWrapper.webidl @@ -38,7 +38,7 @@ enum MozContentPolicyType { * A thin wrapper around nsIChannel and nsIHttpChannel that allows JS * callers to access them without XPConnect overhead. */ -[ChromeOnly, Exposed=Window] +[ChromeOnly, Exposed=(Window,System)] interface ChannelWrapper : EventTarget { /** * Returns the wrapper instance for the given channel. The same wrapper is diff --git a/toolkit/components/webextensions/ext-webRequest.js b/toolkit/components/webextensions/ext-webRequest.js index 0ae9a28127..a5b924d497 100644 --- a/toolkit/components/webextensions/ext-webRequest.js +++ b/toolkit/components/webextensions/ext-webRequest.js @@ -4,8 +4,6 @@ var {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebRequest", "resource://gre/modules/WebRequest.jsm"); @@ -75,11 +73,16 @@ function WebRequestEventManager(context, eventName) { } } + if (data.registerTraceableChannel) { + let remoteTab = context.xulBrowser && context.xulBrowser.frameLoader + ? context.xulBrowser.frameLoader.remoteTab : null; + data.registerTraceableChannel({id: context.extension.id}, remoteTab); + } return context.runSafe(callback, data2); }; let filter2 = {}; - filter2.urls = new MatchPattern(filter.urls); + filter2.urls = filter.urls; if (filter.types) { filter2.types = filter.types; } @@ -105,7 +108,9 @@ function WebRequestEventManager(context, eventName) { } } - WebRequest[eventName].addListener(listener, filter2, info2); + WebRequest[eventName].addListener(listener, filter2, info2, { + policy: {id: context.extension.id, allowedOrigins: context.extension.whiteListedHosts}, + }); return () => { WebRequest[eventName].removeListener(listener); }; @@ -117,14 +122,6 @@ function WebRequestEventManager(context, eventName) { WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype); function makeWebRequestEvent(context, eventName) { - if (!(eventName in WebRequest) || !WebRequest[eventName]) { - let name = `webRequest.${eventName}`; - return new SingletonEventManager(context, name, () => { - Cu.reportError(`webRequest.${eventName} is not supported by this runtime.`); - return () => {}; - }).api(); - } - return new WebRequestEventManager(context, eventName).api(); } @@ -156,7 +153,7 @@ extensions.registerSchemaAPI("webRequest", "addon_parent", context => { return WebRequest.getSecurityInfo({ id: requestId, - policy: context.extension.policy, + policy: {id: context.extension.id}, remoteTab, options, }); diff --git a/toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js b/toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js new file mode 100644 index 0000000000..6acd376836 --- /dev/null +++ b/toolkit/components/webextensions/test/xpcshell/test_webnavigation_created_target.js @@ -0,0 +1,50 @@ +"use strict"; + +add_task(function* test_created_navigation_target_dispatch() { + let scope = {}; + Services.scriptloader.loadSubScript("resource://gre/modules/WebNavigation.jsm", scope); + let {Manager, WebNavigation} = scope; + let event = WebNavigation.onCreatedNavigationTarget; + let source = {}, target = {}; + let received = []; + let listener = data => received.push(data); + event.addListener(listener, {matches: url => url == "https://example.com/target"}); + do_register_cleanup(() => event.removeListener(listener)); + + for (let sourceFirst of [true, false]) { + let data = { + url: "https://example.com/target", sourceWindowId: 123, + createdWindowId: sourceFirst ? 456 : 789, + }; + Manager.onCreatedNavigationTarget(sourceFirst ? source : target, + Object.assign({isSourceTab: sourceFirst}, data)); + equal(received.length, 0, "wait for both browsers"); + Manager.onCreatedNavigationTarget(sourceFirst ? target : source, + Object.assign({isSourceTab: !sourceFirst}, data)); + equal(received.length, 1, "dispatch once, in either message order"); + equal(received[0].browser, target); + equal(received[0].sourceTabBrowser, source); + equal(received[0].sourceWindowId, 123); + received.length = 0; + } + + function notify(url) { + Services.obs.notifyObservers({wrappedJSObject: { + url, sourceTabBrowser: source, createdTabBrowser: target, + sourceFrameOuterWindowID: 123, + }}, "webNavigation-createdNavigationTarget", null); + } + notify("https://other.example/target"); + equal(received.length, 0, "filter applies to chrome-created targets too"); + notify("https://example.com/target"); + equal(received.length, 1); + Manager.onCreatedNavigationTarget(source, { + isSourceTab: true, createdWindowId: 1000, sourceWindowId: 123, + url: "https://example.com/target", + }); + event.removeListener(listener); + equal(Manager.createdNavigationTargetByOuterWindowId.size, 0, + "unregistering releases unmatched messages and timers"); + notify("https://example.com/target"); + equal(received.length, 1, "removed listeners are not invoked"); +}); diff --git a/toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js b/toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js new file mode 100644 index 0000000000..af32a38176 --- /dev/null +++ b/toolkit/components/webextensions/test/xpcshell/test_webrequest_backend.js @@ -0,0 +1,66 @@ +"use strict"; + +// Exercise real channels: a successful import alone does not prove that the +// observer, native wrapper, filters and blocking response are connected. +add_task(function* test_webrequest_backend() { + let {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {}); + let server = createHttpServer(); + let hits = 0; + server.registerPathHandler("/request", (request, response) => { + ++hits; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write("allowed"); + }); + let url = `http://localhost:${server.identity.primaryPort}/request`; + function request(system = false) { + let uri = Services.io.newURI(url, null, null); + let channel = NetUtil.newChannel({ + uri, + loadingPrincipal: system ? Services.scriptSecurityManager.getSystemPrincipal() + : Services.scriptSecurityManager.createCodebasePrincipal(uri, {}), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST, + }); + return new Promise(resolve => { + NetUtil.asyncFetch(channel, (stream, status) => resolve(status)); + }); + } + + let calls = 0; + let block = data => { + ++calls; + equal(data.url, url); + equal(data.type, "xmlhttprequest"); + equal(typeof data.requestId, "string"); + return {cancel: true}; + }; + let event = WebRequest.onBeforeRequest; + do_register_cleanup(() => event.removeListener(block)); + event.addListener(block, {urls: ["http://localhost/*"]}, ["blocking"]); + equal(yield request(), Cr.NS_ERROR_ABORT, "blocking listener cancels the channel"); + equal(calls, 1); + equal(hits, 0, "cancelled request never reaches the server"); + event.removeListener(block); + equal(yield request(), Cr.NS_OK, "removing the listener restores loading"); + + for (let filter of [ + {urls: ["http://example.org/*"]}, + {urls: [""], types: ["image"]}, + {urls: [""], incognito: true}, + {urls: [""], tabId: 123}, + ]) { + event.addListener(block, filter, ["blocking"]); + equal(yield request(), Cr.NS_OK, "nonmatching requests are untouched"); + event.removeListener(block); + } + event.addListener(block, {urls: [""]}, ["blocking"], { + policy: {id: "backend-test", allowedOrigins: new (Cu.import( + "resource://gre/modules/MatchPattern.jsm", {}).MatchPattern)(["http://example.org/*"])}, + }); + equal(yield request(), Cr.NS_OK, "host permissions constrain the listener"); + event.removeListener(block); + event.addListener(block, {urls: [""]}, ["blocking"]); + equal(yield request(true), Cr.NS_OK, "system requests are not exposed"); + event.removeListener(block); + equal(calls, 1, "only the matching content request was dispatched"); +}); diff --git a/toolkit/components/webextensions/test/xpcshell/xpcshell.ini b/toolkit/components/webextensions/test/xpcshell/xpcshell.ini index d2c6fd5d07..7abf9fe3cd 100644 --- a/toolkit/components/webextensions/test/xpcshell/xpcshell.ini +++ b/toolkit/components/webextensions/test/xpcshell/xpcshell.ini @@ -8,6 +8,8 @@ support-files = tags = webextensions [test_csp_custom_policies.js] +[test_webrequest_backend.js] +[test_webnavigation_created_target.js] [test_csp_validator.js] [test_ext_alarms.js] [test_ext_alarms_does_not_fire.js] diff --git a/toolkit/components/webextensions/webrequest/SecurityInfo.jsm b/toolkit/components/webextensions/webrequest/SecurityInfo.jsm index 4652aa28da..daa33fa406 100644 --- a/toolkit/components/webextensions/webrequest/SecurityInfo.jsm +++ b/toolkit/components/webextensions/webrequest/SecurityInfo.jsm @@ -4,11 +4,11 @@ "use strict"; +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + const EXPORTED_SYMBOLS = ["SecurityInfo"]; -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); const wpl = Ci.nsIWebProgressListener; XPCOMUtils.defineLazyServiceGetter( diff --git a/toolkit/components/webextensions/webrequest/WebRequest.jsm b/toolkit/components/webextensions/webrequest/WebRequest.jsm index 72fd9a3f26..b5eb39abcc 100644 --- a/toolkit/components/webextensions/webrequest/WebRequest.jsm +++ b/toolkit/components/webextensions/webrequest/WebRequest.jsm @@ -4,6 +4,8 @@ "use strict"; +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + const EXPORTED_SYMBOLS = ["WebRequest"]; /* exported WebRequest */ @@ -12,29 +14,36 @@ const EXPORTED_SYMBOLS = ["WebRequest"]; const { nsIHttpActivityObserver, nsISocketTransport } = Ci; -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); -XPCOMUtils.defineLazyModuleGetters(this, { - ExtensionParent: "resource://gre/modules/ExtensionParent.jsm", - ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm", - WebRequestUpload: "resource://gre/modules/WebRequestUpload.jsm", - SecurityInfo: "resource://gre/modules/SecurityInfo.jsm", -}); -// WebRequest.jsm's only consumer is ext-webRequest.js, so we can depend on -// the apiManager.global being initialized. -XPCOMUtils.defineLazyGetter(this, "tabTracker", () => { - return ExtensionParent.apiManager.global.tabTracker; -}); -XPCOMUtils.defineLazyGetter(this, "getCookieStoreIdForOriginAttributes", () => { - return ExtensionParent.apiManager.global.getCookieStoreIdForOriginAttributes; -}); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", "resource://gre/modules/ExtensionUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload", "resource://gre/modules/WebRequestUpload.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SecurityInfo", "resource://gre/modules/SecurityInfo.jsm"); + + +function getCookieStoreIdForOriginAttributes(attrs) { + if (attrs.privateBrowsingId) { + return "firefox-private"; + } + return attrs.userContextId ? "firefox-container-" + attrs.userContextId : "firefox-default"; +} + +XPCOMUtils.defineLazyServiceGetter(this, "categoryManager", + "@mozilla.org/categorymanager;1", "nsICategoryManager"); +XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", "resource://gre/modules/MatchPattern.jsm"); + +function matchesRequest(channel, opts, extraData) { + return !channel.isSystemLoad && + (!opts.urlPattern || opts.urlPattern.matches(channel.finalURI)) && + (!opts.policy || opts.policy.allowedOrigins.matches(channel.finalURI)) && + channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData); +} function runLater(job) { - Services.tm.dispatchToMainThread(job); + Services.tm.mainThread.dispatch(job, Ci.nsIThread.DISPATCH_NORMAL); } function parseFilter(filter) { @@ -165,7 +174,7 @@ class HeaderChanger { } const checkRestrictedHeaderValue = (value, opts = {}) => { - let uri = Services.io.newURI(`https://${value}/`); + let uri = Services.io.newURI(`https://${value}/`, null, null); let { policy } = opts; if (policy && !policy.allowedOrigins.matches(uri)) { @@ -281,7 +290,7 @@ var ChannelEventSink = { _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"), _contractID: "@mozilla.org/webrequest/channel-event-sink;1", - QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink, Ci.nsIFactory]), init() { Components.manager @@ -295,7 +304,7 @@ var ChannelEventSink = { }, register() { - Services.catMan.addCategoryEntry( + categoryManager.addCategoryEntry( "net-channel-event-sinks", this._contractID, this._contractID, @@ -305,7 +314,7 @@ var ChannelEventSink = { }, unregister() { - Services.catMan.deleteCategoryEntry( + categoryManager.deleteCategoryEntry( "net-channel-event-sinks", this._contractID, false @@ -460,7 +469,7 @@ class AuthRequestor { this.httpObserver.runChannelListener(wrapper, "onAuthRequired", data); return { - QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), + QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]), cancel() { try { callback.onAuthCancelled(context, false); @@ -474,10 +483,10 @@ class AuthRequestor { } } -AuthRequestor.prototype.QueryInterface = ChromeUtils.generateQI([ - "nsIInterfaceRequestor", - "nsIAuthPromptProvider", - "nsIAuthPrompt2", +AuthRequestor.prototype.QueryInterface = XPCOMUtils.generateQI([ + Ci.nsIInterfaceRequestor, + Ci.nsIAuthPromptProvider, + Ci.nsIAuthPrompt2, ]); // Most WebRequest events are implemented via the observer services, but @@ -563,21 +572,21 @@ HttpObserverManager = { this.listeners.onSendHeaders.size; if (needOpening && !this.openingInitialized) { this.openingInitialized = true; - Services.obs.addObserver(this, "http-on-modify-request"); + Services.obs.addObserver(this, "http-on-modify-request", false); } else if (!needOpening && this.openingInitialized) { this.openingInitialized = false; Services.obs.removeObserver(this, "http-on-modify-request"); } if (needBeforeConnect && !this.beforeConnectInitialized) { this.beforeConnectInitialized = true; - Services.obs.addObserver(this, "http-on-before-connect"); + Services.obs.addObserver(this, "http-on-before-connect", false); } else if (!needBeforeConnect && this.beforeConnectInitialized) { this.beforeConnectInitialized = false; Services.obs.removeObserver(this, "http-on-before-connect"); } let haveBlocking = Object.values(this.listeners).some(listeners => - Array.from(listeners.values()).some(listener => listener.blockingAllowed) + Array.from(listeners.values()).some(listener => listener.blocking) ); this.needTracing = @@ -593,9 +602,9 @@ HttpObserverManager = { if (needExamine && !this.examineInitialized) { this.examineInitialized = true; - Services.obs.addObserver(this, "http-on-examine-response"); - Services.obs.addObserver(this, "http-on-examine-cached-response"); - Services.obs.addObserver(this, "http-on-examine-merged-response"); + Services.obs.addObserver(this, "http-on-examine-response", false); + Services.obs.addObserver(this, "http-on-examine-cached-response", false); + Services.obs.addObserver(this, "http-on-examine-merged-response", false); } else if (!needExamine && this.examineInitialized) { this.examineInitialized = false; Services.obs.removeObserver(this, "http-on-examine-response"); @@ -692,7 +701,7 @@ HttpObserverManager = { // Make a trip through the event loop to make sure errors have a // chance to be processed before we fall back to a generic error // string. - Services.tm.dispatchToMainThread(() => { + runLater(() => { channel.errorCheck(); if (!channel.errorString) { this.runChannelListener(channel, "onErrorOccurred", { @@ -712,7 +721,7 @@ HttpObserverManager = { }, getRequestData(channel, extraData) { - let originAttributes = channel.loadInfo?.originAttributes; + let originAttributes = channel.channel.loadInfo?.originAttributes; let data = { requestId: String(channel.id), url: channel.finalURL, @@ -725,6 +734,9 @@ HttpObserverManager = { documentUrl: channel.documentURL || undefined, tabId: this.getBrowserData(channel).tabId, + browser: channel.browserElement, + windowId: channel.windowId, + isSystemPrincipal: channel.isSystemLoad, frameId: channel.windowId, parentWindowId: channel.parentWindowId, @@ -783,7 +795,8 @@ HttpObserverManager = { let browserData = wrapper._browserData; if (!browserData) { if (wrapper.browserElement) { - browserData = tabTracker.getBrowserData(wrapper.browserElement); + browserData = {}; + ExtensionParent.apiManager.emit("fill-browser-data", wrapper.browserElement, browserData); } else { browserData = { tabId: -1, windowId: -1 }; } @@ -815,7 +828,7 @@ HttpObserverManager = { return; } } - if (!channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData)) { + if (!matchesRequest(channel, opts, extraData)) { return; } @@ -957,7 +970,7 @@ HttpObserverManager = { if (result.redirectUrl) { try { channel.suspended = false; - channel.redirectTo(Services.io.newURI(result.redirectUrl)); + channel.redirectTo(Services.io.newURI(result.redirectUrl, null, null)); // Web Extensions using the WebRequest API are allowed // to redirect a channel to a data: URI, hence we mark @@ -967,7 +980,7 @@ HttpObserverManager = { // RedirectTo() implementation explicitly drops the flag // to avoid additional redirects not caused by the // Web Extension. - channel.loadInfo.allowInsecureRedirectToDataURI = true; + channel.channel.loadInfo.allowInsecureRedirectToDataURI = true; // To pass CORS checks, we pretend the current request's // response allows the triggering origin to access. @@ -1047,7 +1060,7 @@ HttpObserverManager = { } for (let opts of listener.values()) { - if (channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData)) { + if (matchesRequest(channel, opts, extraData)) { return true; } } @@ -1096,6 +1109,7 @@ HttpEvent.prototype = { addListener(callback, filter = null, options = null, optionsObject = null) { let opts = parseExtra(options, this.options, optionsObject); opts.filter = parseFilter(filter); + opts.urlPattern = opts.filter.urls ? new MatchPattern(opts.filter.urls) : null; HttpObserverManager.addListener(this.internalEvent, callback, opts); }, diff --git a/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm b/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm index eb8a2bc6b5..d5d862e3c5 100644 --- a/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm +++ b/toolkit/components/webextensions/webrequest/WebRequestUpload.jsm @@ -4,21 +4,19 @@ "use strict"; +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + const EXPORTED_SYMBOLS = ["WebRequestUpload"]; /* exported WebRequestUpload */ -const { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); -const { ExtensionUtils } = ChromeUtils.import( - "resource://gre/modules/ExtensionUtils.jsm" -); +const { ExtensionUtils } = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {}); const { DefaultMap } = ExtensionUtils; -XPCOMUtils.defineLazyGlobalGetters(this, ["TextEncoder"]); +Cu.importGlobalProperties(["TextEncoder"]); XPCOMUtils.defineLazyServiceGetter( this, @@ -475,7 +473,8 @@ function* getRawDataChunked( // the file, rather than its data. if ( unbuffered instanceof Ci.nsIFileInputStream || - unbuffered instanceof Ci.mozIRemoteLazyInputStream + ("mozIRemoteLazyInputStream" in Ci && + unbuffered instanceof Ci.mozIRemoteLazyInputStream) ) { // But this is not actually supported yet. yield { file: "" };