"use strict"; /** * The ChannelWrapper API is part of the implementation of WebRequest, and not * really meant to be used in isolation. In practice, there are several in-tree * uses of ChannelWrapper, so this test serves as a sanity check that * ChannelWrapper behaves reasonable in the absence of WebRequest. */ const server = createHttpServer({ hosts: ["origin.example.net", "example.com"], }); server.registerPathHandler("/home", () => {}); server.registerPathHandler("/dummy", (request, response) => { response.setStatusLine(request.httpVersion, 200, "OK"); response.setHeader( "Access-Control-Allow-Origin", "http://origin.example.net" ); response.write("Server's reply"); }); // Some properties do not have a straightforward comparison, so we just verify // that the property is set (or not). const EXPECT_TRUTHY = Symbol("EXPECT_TRUTHY"); const EXPECT_FALSEY = Symbol("EXPECT_FALSEY"); // Properties in the order specified in ChannelWrapper.webidl const EXPECTATION_BASIC_FETCH = { id: EXPECT_TRUTHY, channel: EXPECT_TRUTHY, contentType: "", method: "GET", type: "xmlhttprequest", suspended: false, finalURI: EXPECT_TRUTHY, finalURL: "http://example.com/dummy", statusCode: 0, statusLine: "", errorString: null, onerror: null, onstart: null, onstop: null, proxyInfo: EXPECT_TRUTHY, // The xpcshell test server uses a proxy. remoteAddress: null, // Not set at start of request loadInfo: EXPECT_TRUTHY, isServiceWorkerScript: false, originURL: "http://origin.example.net/home", documentURL: "http://origin.example.net/home", originURI: EXPECT_TRUTHY, documentURI: EXPECT_TRUTHY, canModify: true, frameId: 0, // Top-level frame. parentFrameId: -1, browserElement: EXPECT_TRUTHY, frameAncestors: [], // Top-level frame does not have ancestors. urlClassification: { firstParty: [], thirdParty: [], }, thirdParty: true, // origin.example.net vs example.com is third-party. requestSize: 0, // Request not sent yet at start of request. responseSize: 0, // Response not received yet at start of request. }; const EXPECTATION_BASIC_FETCH_COMPLETED = { ...EXPECTATION_BASIC_FETCH, contentType: "text/plain", statusCode: 200, statusLine: "HTTP/1.1 200 OK", remoteAddress: "127.0.0.1", browserElement: EXPECT_FALSEY, requestSize: EXPECT_TRUTHY, responseSize: EXPECT_TRUTHY, }; const EXPECTATION_BASIC_FETCH_ABORTED = { ...EXPECTATION_BASIC_FETCH, errorString: "NS_ERROR_ABORT", browserElement: EXPECT_FALSEY, }; // We don't really care about the values; the main purpose of checking these // properties is to make sure that something reasonable happens. In particular, // that we are not hitting assertion failures or crashes. const EXPECTATION_INVALID_CHANNEL = { id: EXPECT_TRUTHY, channel: EXPECT_FALSEY, contentType: "", method: "", type: "other", suspended: false, finalURI: EXPECT_FALSEY, finalURL: "", statusCode: 0, statusLine: "", errorString: "NS_ERROR_UNEXPECTED", onerror: null, onstart: null, onstop: null, proxyInfo: null, remoteAddress: null, loadInfo: null, isServiceWorkerScript: false, originURL: "", documentURL: "", originURI: null, documentURI: null, canModify: false, frameId: 0, parentFrameId: -1, browserElement: EXPECT_FALSEY, frameAncestors: null, urlClassification: { firstParty: [], thirdParty: [], }, thirdParty: false, requestSize: 0, responseSize: 0, }; function channelWrapperEquals(channelWrapper, expectedProps) { for (let [k, v] of Object.entries(expectedProps)) { if (v === EXPECT_TRUTHY) { Assert.ok(channelWrapper[k], `ChannelWrapper.${k} is truthy`); } else if (v === EXPECT_FALSEY) { Assert.ok(!channelWrapper[k], `ChannelWrapper.${k} is falsey`); } else { Assert.deepEqual(channelWrapper[k], v, `ChannelWrapper.${k}`); } } } let gContentPage; async function forceChannelGC() { await Promise.resolve(); Cu.forceGC(); Cu.forceCC(); } function checkChannelWrapperMethodsAfterGC(channelWrapper) { // All methods in the order of appearance in ChannelWrapper.webidl. // The exact behavior does not matter, as long as it is somewhat reasonable, // and in particular does not trigger assertions or crashes. const dummyURI = Services.io.newURI("http://example.com/neverloaded"); const dummyPolicy = new WebExtensionPolicy({ id: "@dummyPolicy", mozExtensionHostname: "c3c73091-8fab-4229-83cf-84c061dd9ead", baseURL: "resource://modules/whatever_does_not_need_to_exist", allowedOrigins: new MatchPatternSet(["*://*/*"]), localizeCallback: () => "", }); Assert.throws( () => channelWrapper.cancel(0), /NS_ERROR_UNEXPECTED/, "channelWrapper.cancel() throws" ); Assert.throws( () => channelWrapper.redirectTo(dummyURI), /NS_ERROR_UNEXPECTED/, "channelWrapper.redirectTo() throws" ); Assert.throws( () => channelWrapper.upgradeToSecure(), /NS_ERROR_UNEXPECTED/, "channelWrapper.upgradeToSecure() throws" ); Assert.throws( () => channelWrapper.suspend(""), /NS_ERROR_UNEXPECTED/, "channelWrapper.suspend() throws" ); // resume() trivially returns because it is no-op when suspend() did not run. Assert.equal( channelWrapper.resume(), undefined, "channelWrapper.resume() returns" ); Assert.equal( channelWrapper.matches({}, null, {}), false, "channelWrapper.matches() returns" ); Assert.equal( channelWrapper.registerTraceableChannel(dummyPolicy, null), undefined, "registerTraceableChannel() returns" ); Assert.equal(channelWrapper.errorCheck(), undefined, "errorCheck() returns"); Assert.throws( () => channelWrapper.getRequestHeaders(), /NS_ERROR_UNEXPECTED/, "channelWrapper.getRequestHeaders() throws" ); Assert.throws( () => channelWrapper.getRequestHeader("Content-Type"), /NS_ERROR_UNEXPECTED/, "channelWrapper.getRequestHeader() throws" ); Assert.throws( () => channelWrapper.getResponseHeaders(), /NS_ERROR_UNEXPECTED/, "channelWrapper.getResponseHeaders() throws" ); Assert.throws( () => channelWrapper.setRequestHeader("Content-Type", ""), /NS_ERROR_UNEXPECTED/, "channelWrapper.setRequestHeader() throws" ); Assert.throws( () => channelWrapper.setResponseHeader("Content-Type", ""), /NS_ERROR_UNEXPECTED/, "channelWrapper.setResponseHeader() throws" ); } function createChannel(url) { const dummyPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); return Services.io.newChannelFromURI( Services.io.newURI(url), /* loadingNode */ null, /* loadingPrincipal */ dummyPrincipal, /* triggeringPrincipal */ dummyPrincipal, /* securityFlags */ 0, /* contentPolicyType */ Ci.nsIContentPolicy.TYPE_FETCH ); } function assertChannelWrapperUnsupportedForChannel(channel) { let channelWrapper = ChannelWrapper.get(channel); Assert.equal( channelWrapper.channel, null, `ChannelWrapper cannot wrap channel for ${channel.URI.spec}` ); channelWrapperEquals(channelWrapper, EXPECTATION_INVALID_CHANNEL); } add_setup(async function create_content_page() { gContentPage = await ExtensionTestUtils.loadContentPage( "http://origin.example.net/home" ); registerCleanupFunction(() => gContentPage.close()); }); add_task(async function during_basic_fetch() { let promise = TestUtils.topicObserved("http-on-modify-request", channel => { equal(channel.URI.spec, "http://example.com/dummy", "expected URL"); let channelWrapper = ChannelWrapper.get(channel); channelWrapperEquals(channelWrapper, EXPECTATION_BASIC_FETCH); return true; }); await gContentPage.spawn([], async () => { let res = await content.fetch("http://example.com/dummy"); Assert.equal(await res.text(), "Server's reply", "Got response"); }); await promise; }); add_task(async function after_basic_fetch() { let promise = TestUtils.topicObserved("http-on-modify-request"); await gContentPage.spawn([], async () => { let res = await content.fetch("http://example.com/dummy"); Assert.equal(await res.text(), "Server's reply", "Got response"); }); let [channel] = await promise; equal(channel.URI.spec, "http://example.com/dummy", "expected URL"); let channelWrapper = ChannelWrapper.get(channel); channelWrapperEquals(channelWrapper, EXPECTATION_BASIC_FETCH_COMPLETED); }); add_task(async function after_cancel_request() { let channelWrapper; let promise = TestUtils.topicObserved("http-on-modify-request", channel => { equal(channel.URI.spec, "http://example.com/dummy", "expected URL"); channelWrapper = ChannelWrapper.get(channel); channel.cancel(Cr.NS_ERROR_ABORT); return true; }); await gContentPage.spawn([], async () => { await Assert.rejects( content.fetch("http://example.com/dummy"), /NetworkError when attempting to fetch resource./, "Request should be aborted" ); }); await promise; channelWrapperEquals(channelWrapper, EXPECTATION_BASIC_FETCH_ABORTED); }); add_task(async function after_basic_fetch_and_gc() { let promise = TestUtils.topicObserved("http-on-modify-request"); await gContentPage.spawn([], async () => { let res = await content.fetch("http://example.com/dummy"); Assert.equal(await res.text(), "Server's reply", "Got response"); }); let [channel] = await promise; equal(channel.URI.spec, "http://example.com/dummy", "expected URL"); let channelWrapper = ChannelWrapper.get(channel); Assert.equal(channelWrapper.channel, channel, "channel not GC'd yet"); channel = promise = null; await forceChannelGC(); Assert.equal(channelWrapper.channel, null, "Channel has been GC'd"); channelWrapperEquals(channelWrapper, EXPECTATION_INVALID_CHANNEL); checkChannelWrapperMethodsAfterGC(channelWrapper); }); // getRegisteredChannel should be called before the response has started. In // this test case we instantiate ChannelWrapper after the request completed, // and confirm that there are no weird crashes or assertion failures. add_task(async function getRegisteredChannel_after_response_start() { const dummyPolicy = new WebExtensionPolicy({ id: "@dummyPolicy", mozExtensionHostname: "c3c73091-8fab-4229-83cf-84c061dd9ead", baseURL: "resource://modules/whatever_does_not_need_to_exist", allowedOrigins: new MatchPatternSet(["*://*/*"]), localizeCallback: () => "", }); let promise = TestUtils.topicObserved("http-on-modify-request"); await gContentPage.spawn([], async () => { let res = await content.fetch("http://example.com/dummy"); Assert.equal(await res.text(), "Server's reply", "Got response"); }); let [channel] = await promise; equal(channel.URI.spec, "http://example.com/dummy", "expected URL"); let channelWrapper = ChannelWrapper.get(channel); let channelId = channelWrapper.id; // NOTE: registerTraceableChannel() should return early when a channel is // past OnStartRequest, but if ChannelWrapper.get() is called after that, // then ChannelWrapper::mResponseStarted is not set, and the implementation // is unaware of the fact that the response has already started. While not // ideal, it may happen in practice, so confirm that there are no crashes or // assertion failures. channelWrapper.registerTraceableChannel(dummyPolicy, null); Assert.equal( ChannelWrapper.getRegisteredChannel(channelId, dummyPolicy, null), channelWrapper, "getRegisteredChannel() returns wrapper after registerTraceableChannel()" ); // Reset internal cached fields via ChannelWrapper::SetChannel. channelWrapper.channel = channel; channel = promise = null; await forceChannelGC(); Assert.equal(channelWrapper.channel, null, "Channel has been GC'd"); Assert.equal( ChannelWrapper.getRegisteredChannel(channelId, dummyPolicy, null), null, "getRegisteredChannel() returns nothing after channel was GC'd" ); channelWrapperEquals(channelWrapper, EXPECTATION_INVALID_CHANNEL); checkChannelWrapperMethodsAfterGC(channelWrapper); }); add_task(async function ChannelWrapper_https_url() { // https: and http: are the only channels supported by WebRequest and // ChannelWrapper. http: was tested with real requests before, here we also // test https: just by simulating a channel for a https:-URL. const channel = createChannel("https://example.com/dummyhttps"); let channelWrapper = ChannelWrapper.get(channel); Assert.equal( channelWrapper.channel, channel, "ChannelWrapper can wrap channel for https" ); // The following two expectations are identical. The expectations are repeated // twice, to make it easier to see what the difference is between https vs // invalid, and https vs the http:-test elsewhere. channelWrapperEquals(channelWrapper, { ...EXPECTATION_INVALID_CHANNEL, channel, method: "GET", type: "xmlhttprequest", finalURI: EXPECT_TRUTHY, finalURL: "https://example.com/dummyhttps", errorString: null, loadInfo: EXPECT_TRUTHY, canModify: true, thirdParty: true, // null principal from createChannel vs example.com. }); channelWrapperEquals(channelWrapper, { ...EXPECTATION_BASIC_FETCH, finalURL: "https://example.com/dummyhttps", proxyInfo: null, originURL: "", // triggeringPrincipal is null principal in createChannel. documentURL: "", // triggeringPrincipal is null principal in createChannel. originURI: null, documentURI: null, browserElement: null, // simulated load not associated with any . frameAncestors: null, // simulated load not associated with BrowsingContext. }); }); add_task(async function ChannelWrapper_moz_extension_url() { const xpi = AddonTestUtils.createTempWebExtensionFile({}); const dummyPolicy = new WebExtensionPolicy({ id: "@dummyPolicy", mozExtensionHostname: "e17d45dd-fe2a-4ece-8794-d487062cadf4", baseURL: `jar:${Services.io.newFileURI(xpi).spec}!/`, allowedOrigins: new MatchPatternSet(["*://*/*"]), localizeCallback: () => "", }); dummyPolicy.active = true; const channel = createChannel( "moz-extension://e17d45dd-fe2a-4ece-8794-d487062cadf4/manifest.json" ); Assert.ok(channel instanceof Ci.nsIJARChannel, "Is nsIJARChannel"); assertChannelWrapperUnsupportedForChannel(channel); dummyPolicy.active = false; }); add_task(async function ChannelWrapper_blob_url() { const blobUrl = await gContentPage.spawn([], () => { return content.URL.createObjectURL(new content.Blob(new content.Array())); }); const channel = createChannel(blobUrl); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function ChannelWrapper_data_url() { const channel = createChannel("data:,"); Assert.ok(channel instanceof Ci.nsIDataChannel, "Is nsIDataChannel"); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function ChannelWrapper_file_url() { // Note: "file://C:/" is a valid file:-URL across all platforms. const channel = createChannel("file://C:/"); Assert.ok(channel instanceof Ci.nsIFileChannel, "Is nsIFileChannel"); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function ChannelWrapper_about_blank_url() { const channel = createChannel("about:blank"); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function ChannelWrapper_javascript_url() { const channel = createChannel("javascript://"); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function ChannelWrapper_resource_url() { const channel = createChannel("resource://content-accessible/viewsource.css"); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function ChannelWrapper_chrome_url() { const channel = createChannel( "chrome://extensions/content/schemas/web_request.json" ); assertChannelWrapperUnsupportedForChannel(channel); }); add_task(async function sanity_check_expectations_complete() { const channelWrapper = ChannelWrapper.get(createChannel("http://whatever/")); channelWrapper.channel = null; const uncheckedKeys = new Set(Object.keys(ChannelWrapper.prototype)); const channelWrapperWithSpy = new Proxy(channelWrapper, { get(target, prop) { uncheckedKeys.delete(prop); let value = Reflect.get(target, prop, target); // Methods throw if not invoked on the ChannelWrapper interface, so bind // to target (=channelWrapper) instead of channelWrapperWithSpy. return typeof value == "function" ? value.bind(target) : value; }, }); channelWrapperEquals(channelWrapperWithSpy, EXPECTATION_INVALID_CHANNEL); checkChannelWrapperMethodsAfterGC(channelWrapperWithSpy); Assert.deepEqual( Array.from(uncheckedKeys), [], "All ChannelWrapper properties and methods have been checked" ); // The above channelWrapper(channelWrapperWithSpy, ...) call triggers a lookup // for each property listed in EXPECTATION_INVALID_CHANNEL as a way to verify // that all properties are accounted for. To make sure that the test case for // a valid channel also have complete property coverage, confirm that each // property is also present in EXPECTATION_BASIC_FETCH. Assert.deepEqual( Object.keys(EXPECTATION_BASIC_FETCH), Object.keys(EXPECTATION_INVALID_CHANNEL), "EXPECTATION_BASIC_FETCH has same properties as EXPECTATION_INVALID_CHANNEL" ); }); add_task(async function sanity_check_WebRequest_module_not_loaded() { // The purpose of this whole test file is to test the behavior of // ChannelWrapper, independently of the webRequest API implementation. // So as a sanity check, confirm that we have indeed not loaded that module. Assert.equal( Cu.isESModuleLoaded("resource://gre/modules/WebRequest.sys.mjs"), false, "WebRequest.sys.mjs should not have been loaded in this test" ); });